#!/bin/bash set -e # Flow migration fix script (0.131 -> 0.299) # This script is idempotent - it can be run multiple times safely. # It fixes Flow type errors after upgrading from Flow 0.131 to 0.299. # # NOTE: We DO run "flow codemod annotate-exports", then normalize the # generated modern syntax (component()/renders/as-casts) back to # Babel-compatible Flow syntax. # # Usage: cd newIDE/app && bash scripts/fix-flow-errors.sh SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" export APP_DIR cd "$APP_DIR" echo "=== Flow Migration Fix Script ===" echo "Working directory: $APP_DIR" ############################################################################### # STEP 1: Ensure flow-libs directory exists ############################################################################### echo "" echo "--- Step 1: Ensuring flow-libs are in place ---" if [ ! -d "$APP_DIR/flow-libs" ]; then echo "ERROR: flow-libs directory not found." exit 1 fi echo " OK." ############################################################################### # STEP 1b: Update flow-bin version to 0.299.0 in package.json and npm install ############################################################################### echo "" echo "--- Step 1b: Updating flow-bin version to 0.299.0 ---" sed -i 's/"flow-bin": "0.131.0"/"flow-bin": "0.299.0"/' "$APP_DIR/package.json" echo " Updated package.json. Running npm install..." npm install echo " Done." ############################################################################### # STEP 2: Update .flowconfig ############################################################################### echo "" echo "--- Step 2: Updating .flowconfig ---" if ! grep -q 'flow-libs' "$APP_DIR/.flowconfig"; then sed -i '/^\[libs\]/a flow-libs' "$APP_DIR/.flowconfig" fi if ! grep -q '/flow-typed/\.\*' "$APP_DIR/.flowconfig"; then sed -i '/^\[declarations\]/a /flow-typed/.*' "$APP_DIR/.flowconfig" fi if ! grep -q 'GDevelop.js/types/\.\*' "$APP_DIR/.flowconfig"; then sed -i '/^\[declarations\]/a /../../GDevelop.js/types/.*' "$APP_DIR/.flowconfig" fi if ! grep -q '/flow-libs/\.\*' "$APP_DIR/.flowconfig"; then sed -i '/^\[declarations\]/a /flow-libs/.*' "$APP_DIR/.flowconfig" fi if ! grep -q 'fbjs' "$APP_DIR/.flowconfig"; then sed -i '/^\[declarations\]/a /node_modules/fbjs/.*' "$APP_DIR/.flowconfig" fi if ! grep -q '^\[lints\]' "$APP_DIR/.flowconfig"; then echo -e "\n[lints]" >> "$APP_DIR/.flowconfig" fi echo " Done." ############################################################################### # STEP 3: Fix deprecated syntax in src/ files ############################################################################### echo "" echo "--- Step 3: Fixing deprecated syntax ---" # Fix existential type * -> any find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/(<[^>]*?)\*([,>])/${1}any${2}/g; s/(<[^>]*?)\*([,>])/${1}any${2}/g; s/& \*\b/\& any/g; ' {} + # Fix %checks (removed in new Flow) find "$APP_DIR/src" -name "*.js" -type f -exec sed -i 's/ %checks//g' {} + # Fix React$ type names -> React. namespace find "$APP_DIR/src" -name "*.js" -type f -exec sed -i \ -e 's/\bReact\$Element\b/React.Element/g' \ -e 's/\bReact\$Component\b/React.Component/g' \ -e 's/\bReact\$Node\b/React.Node/g' \ -e 's/\bReact\$Context\b/React.Context/g' \ -e 's/\bReact\$Ref\b/React.Ref/g' \ -e 's/\bReact\$Key\b/React.Key/g' \ -e 's/\bReact\$ElementRef\b/React.ElementRef/g' \ -e 's/\bReact\$ElementConfig\b/React.ElementConfig/g' \ -e 's/\bReact\$ComponentType\b/React.ComponentType/g' \ {} + # Fix $PropertyType -> indexed access type find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e " s/\\\$PropertyType<([^,]+),\s*'([^']+)'>/\$1['\$2']/g; s/\\\$PropertyType<([^,]+),\s*\"([^\"]+)\">/\$1[\"\$2\"]/g; " {} + # Fix $FlowExpectedError -> $FlowFixMe[incompatible-type] find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/\$FlowExpectedError(?!\[)/\$FlowFixMe[incompatible-type]/g; s/\$FlowExpectedError\[([^\]]+)\]/\$FlowFixMe[$1]/g; ' {} + # Fix old $FlowFixMe without error codes find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/\$FlowFixMe(?!\[)/\$FlowFixMe[incompatible-type]/g; ' {} + echo " Done." ############################################################################### # STEP 4: Run annotate-exports and normalize syntax ############################################################################### echo "" echo "--- Step 4: Running Flow codemod annotate-exports ---" npx flow stop 2>/dev/null || true for i in $(seq 1 5); do echo " Codemod iteration $i..." set +e CODEMOD_OUTPUT=$(npx flow codemod annotate-exports --write --default-any src 2>&1) CODEMOD_EXIT=$? set -e echo "$CODEMOD_OUTPUT" | grep -E \ "Files changed:|Number of annotations added:|Number of sig\. ver\. errors:|Number of annotations required:" || true if [ $CODEMOD_EXIT -ne 0 ]; then echo " WARNING: annotate-exports exited with status $CODEMOD_EXIT" break fi FILES_CHANGED=$(echo "$CODEMOD_OUTPUT" | sed -n 's/.*Files changed:[[:space:]]*\([0-9][0-9]*\).*/\1/p' | head -n 1) if [ -z "$FILES_CHANGED" ]; then echo " WARNING: Could not parse codemod output." break fi if [ "$FILES_CHANGED" -eq 0 ]; then echo " Codemod converged." break fi done echo " Normalizing codemod syntax to Babel-compatible Flow..." python3 "$SCRIPT_DIR/fix-codemod-syntax.py" # Fix arrow-function inline object return types that prettier 1.15 can't parse. # The codemod may add return type annotations like: # const foo = (): { prop: Type } => { ... } # Prettier 1.15 can't parse the `: { ... } =>` pattern. # Fix: extract the inline type into a named type alias, preserving typing. echo " Fixing arrow-function return types for prettier compatibility..." python3 "$SCRIPT_DIR/fix-arrow-return-types.py" echo " Done." ############################################################################### # STEP 4b: Fix codemod artifacts that cause parse errors or Flow issues ############################################################################### echo "" echo "--- Step 4b: Fixing codemod artifacts ---" # 0. Remove stale FlowFixMe[prop-missing] comments that were previously added # for invalid React.AbstractComponent output from older codemod runs. find "$APP_DIR/src" -name "*.js" -type f -exec perl -0pi -e ' s/^[ \t]*\/\/ \$FlowFixMe\[prop-missing\]\n(?=(?:[ \t]*\/\/ \$FlowFixMe\[[^\]]+\]\n|[ \t]*\n)*[^\n]*React\.ComponentType<)//mg; ' {} + # 1. Fix missing commas after ([]: Array) in object literals. # The codemod turns "[]," into "([]: Array)" but loses the comma. find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/\(\[\]: Array\)(\s*\/\/.*)$/([]: Array),$1/g; ' {} + # 2. Fix "new Array(n)" -> "new Array(n)" to avoid underconstrained type find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/new Array\(([^)]+)\)\.fill\(0\)/new Array($1).fill(0)/g; ' {} + # 3. Fix !== null comparisons on number types -> != null # (Flow 0.299 is strict about comparing non-nullable numbers to null) find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/selectedCompletionIndex !== null/selectedCompletionIndex != null/g; ' {} + # 5. Generic type params like new Set(), new Map(), React.createRef() # are NOT parseable by prettier 1.15 (it treats < as comparison operator). # These will get FlowFixMe[underconstrained-implicit-instantiation] automatically. # 6. new Array(n) patterns beyond .fill(0) also can't use with prettier 1.15. # 7. axios calls with type params - skip, prettier 1.15 mangles method() syntax. # These will get FlowFixMe[underconstrained-implicit-instantiation] automatically. # 8. jest.fn() - leave as-is, prettier 1.15 can't parse jest.fn() # These will get FlowFixMe[underconstrained-implicit-instantiation] automatically. # 9. Fix method-unbinding for common patterns: .push.apply -> spread syntax # Handles both single-line and multi-line push.apply patterns. python3 << 'PYEOF' import os, re src_dir = os.path.join(os.environ.get('APP_DIR', os.getcwd()), 'src') fixed = 0 for root, dirs, files in os.walk(src_dir): for fname in files: if not fname.endswith('.js'): continue filepath = os.path.join(root, fname) with open(filepath) as f: content = f.read() original = content # Multi-line pattern: arr.push.apply(\n arr,\n items\n) content = re.sub( r'(\w+)\.push\.apply\(\s*\1\s*,\s*([^)]+?)\s*\)', lambda m: f'{m.group(1)}.push(...{m.group(2).strip()})', content, flags=re.DOTALL) if content != original: with open(filepath, 'w') as f: f.write(content) fixed += 1 print(f" Converted {fixed} push.apply to spread syntax") PYEOF # 10. Fix import-type-as-value: convert value imports of type-only symbols to import type. # I18n from @lingui/core is a type (interface) used only in type positions. find "$APP_DIR/src" -name "*.js" -type f -exec perl -pi -e ' s/^(\/\/ \$FlowFixMe\[import-type-as-value\]\n)?import \{ I18n as I18nType \} from/import type { I18n as I18nType } from/g; ' {} + # Fix TreeViewItemContent import-type-as-value: these are interfaces (type-only) python3 << 'PYEOF' import os, re app_dir = os.environ.get('APP_DIR', os.getcwd()) src_dir = os.path.join(app_dir, 'src') fixed = 0 for root, dirs, files in os.walk(src_dir): for fname in files: if not fname.endswith('.js'): continue filepath = os.path.join(root, fname) with open(filepath) as f: content = f.read() original = content # Remove FlowFixMe[import-type-as-value] above import lines where # we can safely convert to import type lines = content.split('\n') new_lines = [] i = 0 while i < len(lines): line = lines[i] # Skip FlowFixMe[import-type-as-value] if the next line is an import we'll fix if '$FlowFixMe[import-type-as-value]' in line: # Check if this is a standalone FlowFixMe comment line stripped = line.lstrip() if stripped.startswith('// $FlowFixMe[import-type-as-value]'): # Check if next line is an import statement if i + 1 < len(lines) and 'import' in lines[i + 1]: # Skip this FlowFixMe comment i += 1 continue # Check if this is inside an import block (destructured import) # Look backwards for the import statement is_in_import = False for j in range(i - 1, max(i - 10, -1), -1): if 'import' in lines[j] and '{' in lines[j]: is_in_import = True break if '}' in lines[j] and 'from' in lines[j]: break if is_in_import: # Skip this FlowFixMe i += 1 continue new_lines.append(line) i += 1 content = '\n'.join(new_lines) # Convert import { I18n as I18nType } to import type content = re.sub( r"import \{ I18n as I18nType \} from '@lingui/core'", "import type { I18n as I18nType } from '@lingui/core'", content) if content != original: with open(filepath, 'w') as f: f.write(content) fixed += 1 print(f" Fixed {fixed} import-type-as-value issues") PYEOF # 4. Fix inline object return types on multi-line arrow function params # that fix-arrow-return-types.py couldn't handle (nested parens in destructured args). # Pattern: ): { ... } => { (where the object return type causes prettier parse error) python3 << 'PYEOF' import os, re, subprocess app_dir = os.environ.get('APP_DIR', os.getcwd()) # Find files with prettier parse errors result = subprocess.run( ['npx', 'prettier', '--list-different', 'src/!(locales)/**/*.js'], capture_output=True, text=True, cwd=app_dir, timeout=120 ) all_output = result.stdout + '\n' + result.stderr error_files = set() for line in all_output.split('\n'): m = re.match(r'\[error\]\s+(src/\S+\.js):\s+SyntaxError', line) if m: error_files.add(os.path.join(app_dir, m.group(1))) fixed = 0 for filepath in error_files: if not os.path.isfile(filepath): continue with open(filepath) as f: content = f.read() original = content # Find ): { ... } => patterns (multi-line object return types) # This regex works on the whole content to find the ): { ... } => pattern # We need to use a balanced-brace approach pos = 0 replacements = [] while True: idx = content.find('):', pos) if idx == -1: break # Check if followed by whitespace then { rest = content[idx+2:].lstrip() if not rest.startswith('{'): pos = idx + 2 continue brace_start = content.index('{', idx + 2) # Find matching close brace depth = 0 i = brace_start while i < len(content): if content[i] == '{': depth += 1 elif content[i] == '}': depth -= 1 if depth == 0: break elif content[i] in ('"', "'", '`'): q = content[i] i += 1 while i < len(content) and content[i] != q: if content[i] == '\\': i += 1 i += 1 i += 1 if depth != 0: pos = idx + 2 continue brace_end = i # Check if followed by => (arrow) after = content[brace_end+1:].lstrip() if not after.startswith('=>'): pos = idx + 2 continue type_text = content[brace_start:brace_end+1] if '\n' not in type_text: pos = idx + 2 continue # Find function name for type alias line_start = content.rfind('\n', 0, idx) if line_start == -1: line_start = 0 # Find export const/let/var name = ... pattern preceding = content[max(0, line_start-200):idx+2] name_match = re.search(r'(?:const|let|var|function)\s+(\w+)', preceding) name = name_match.group(1) if name_match else 'Func' type_name = f'_{name[0].upper()}{name[1:]}ReturnType' # Check if type alias already exists if f'type {type_name}' in content: pos = idx + 2 continue replacements.append((brace_start, brace_end+1, type_name, type_text)) pos = brace_end + 1 if not replacements: continue # Apply in reverse for brace_start, brace_end, type_name, type_text in reversed(replacements): content = content[:brace_start] + type_name + content[brace_end:] # Insert type declarations at the top (after imports) insert_lines = [] for _, _, type_name, type_text in replacements: insert_lines.append(f'type {type_name} = {type_text};\n') # Find insertion point (after last import) last_import = 0 for m in re.finditer(r'^(?:import\s|from\s)', content, re.MULTILINE): eol = content.find('\n', m.start()) # Handle multi-line imports if '{' in content[m.start():eol] and '}' not in content[m.start():eol]: eol = content.find('}', eol) eol = content.find('\n', eol) # Handle from '...' on next line next_line_start = eol + 1 if next_line_start < len(content): next_stripped = content[next_line_start:].lstrip() if next_stripped.startswith("from ") or next_stripped.startswith("} from "): eol = content.find('\n', next_line_start) if eol > last_import: last_import = eol if last_import > 0: insert_pos = last_import + 1 else: insert_pos = 0 insert_text = '\n' + ''.join(insert_lines) + '\n' content = content[:insert_pos] + insert_text + content[insert_pos:] if content != original: with open(filepath, 'w') as f: f.write(content) fixed += 1 print(f" Fixed {fixed} additional arrow-return-type files") PYEOF echo " Done." ############################################################################### # STEP 5: Fix specific known issues ############################################################################### echo "" echo "--- Step 5: Fixing specific known issues ---" # Fix GDevelop.js type files (if they exist and don't have FlowFixMe) GD_TYPES="$APP_DIR/../../GDevelop.js/types" if [ -d "$GD_TYPES" ]; then python3 << 'PYEOF' import os, re gd_types = os.environ.get('GD_TYPES', '../../GDevelop.js/types') for fname in ['gdbehaviorjsimplementation.js', 'gdbehaviorshareddatajsimplementation.js']: filepath = os.path.join(gd_types, fname) if not os.path.isfile(filepath): continue with open(filepath) as f: content = f.read() if '$FlowFixMe' in content: continue for method in ['getProperties(', 'updateProperty(', 'initializeContent(']: content = content.replace(f' {method}', f' // $FlowFixMe[incompatible-type]\n {method}') with open(filepath, 'w') as f: f.write(content) filepath = os.path.join(gd_types, 'libgdevelop.js') if os.path.isfile(filepath): with open(filepath) as f: lines = f.readlines() if '$FlowFixMe' not in ''.join(lines): fixes = {19: 'cannot-resolve-name', 20: 'cannot-resolve-name', 39: 'cannot-resolve-name', 297: 'cannot-resolve-name'} new_lines = [] for i, line in enumerate(lines): if i + 1 in fixes: new_lines.append(f' // $FlowFixMe[{fixes[i+1]}]\n') new_lines.append(line) with open(filepath, 'w') as f: f.writelines(new_lines) print(" Fixed GDevelop.js types") PYEOF fi # Fix Theme/index.js - $PropertyType replacement may leave empty type or # indexed access type (Theme['gdevelopTheme']) that prettier 1.15 can't parse. # Use typeof DefaultLightTheme.gdevelopTheme which both Flow 0.299 and prettier understand. THEME_FILE="$APP_DIR/src/UI/Theme/index.js" python3 << 'PYEOF' import os, re theme_file = os.path.join(os.environ.get('APP_DIR', os.getcwd()), 'src/UI/Theme/index.js') if os.path.isfile(theme_file): with open(theme_file) as f: content = f.read() original = content # Fix empty type from $PropertyType removal content = content.replace("export type GDevelopTheme = ;", "export type GDevelopTheme = typeof DefaultLightTheme.gdevelopTheme;") # Fix indexed access type that prettier 1.15 can't parse content = re.sub( r"export type GDevelopTheme = Theme\['gdevelopTheme'\];", "export type GDevelopTheme = typeof DefaultLightTheme.gdevelopTheme;", content) # Remove any stray FlowFixMe above the type export content = re.sub( r'(?://\s*\$FlowFixMe\[incompatible-type\]\s*\n|\{/\*\s*\$FlowFixMe\[incompatible-type\]\s*\*/\}\s*\n)' r'(export type GDevelopTheme)', r'\1', content) # Remove any stale prettier-ignore before the line content = re.sub( r'// prettier-ignore\n(export type GDevelopTheme)', r'\1', content) if content != original: with open(theme_file, 'w') as f: f.write(content) print(" Fixed Theme/index.js") else: print(" Theme/index.js already correct") PYEOF # Fix known annotate-exports gaps and formatting-displacement issues. python3 << 'PYEOF' import os import re app_dir = os.environ.get('APP_DIR', os.getcwd()) # Regex-based replacements for signature-verification gaps replacements = [ ( os.path.join(app_dir, 'src/EventsSheet/index.js'), r'onResourceExternallyChanged = resourceInfo => \{', 'onResourceExternallyChanged = (resourceInfo: any) => {', ), ( os.path.join(app_dir, 'src/Leaderboard/LeaderboardContext.js'), r'deleteLeaderboardEntry: async entryId => \{\},', 'deleteLeaderboardEntry: async (entryId: any) => {},', ), ] fixed = 0 for filepath, pattern, replacement in replacements: if not os.path.isfile(filepath): continue with open(filepath) as f: content = f.read() updated = re.sub(pattern, replacement, content) if updated != content: with open(filepath, 'w') as f: f.write(updated) fixed += 1 # Fix PrivateGameTemplateStoreContext: add type annotation to empty object # that prettier splits across lines (causing FlowFixMe displacement). pgts_file = os.path.join(app_dir, 'src/AssetStore/PrivateGameTemplates/PrivateGameTemplateStoreContext.js') if os.path.isfile(pgts_file): with open(pgts_file) as f: content = f.read() # Add type annotation to the untyped empty object updated = content.replace( 'const privateGameTemplateListingDatasById = {};', 'const privateGameTemplateListingDatasById: {[string]: any} = {};') # Remove any stacked FlowFixMe[prop-missing] that accumulated from previous runs updated = re.sub( r'(\s*// \$FlowFixMe\[prop-missing\]\n)+(\s*privateGameTemplateListingDatasById\[)', r'\2', updated) # Also remove FlowFixMe[invalid-computed-prop] for the typed object (no longer needed) updated = re.sub( r'\s*// \$FlowFixMe\[invalid-computed-prop\]\n(\s*if \(privateGameTemplateListingDatasById\[)', r'\n\1', updated) if updated != content: with open(pgts_file, 'w') as f: f.write(updated) fixed += 1 # Fix ConditionsActionsColumns.js - renderActionsList type needs style prop cac_file = os.path.join(app_dir, 'src/EventsSheet/EventsTree/ConditionsActionsColumns.js') if os.path.isfile(cac_file): with open(cac_file) as f: content = f.read() updated = content.replace( 'renderActionsList: ({ className: string }) => React.Node,', 'renderActionsList: ({ style?: Object, className: string }) => React.Node,') if updated != content: with open(cac_file, 'w') as f: f.write(updated) fixed += 1 # Fix PreferencesContext.js - annotate missing params for signature-verification pref_file = os.path.join(app_dir, 'src/MainFrame/Preferences/PreferencesContext.js') if os.path.isfile(pref_file): with open(pref_file) as f: content = f.read() updated = content updated = updated.replace( 'getRecentProjectFiles: options => [],', 'getRecentProjectFiles: (options: any): any => [],') updated = updated.replace( 'getEditorStateForProject: projectId => {},', 'getEditorStateForProject: (projectId: any): any => {},') if updated != content: with open(pref_file, 'w') as f: f.write(updated) fixed += 1 # Fix ShortcutsList.js - annotate commandName parameter sc_file = os.path.join(app_dir, 'src/KeyboardShortcuts/ShortcutsList.js') if os.path.isfile(sc_file): with open(sc_file) as f: content = f.read() updated = content.replace( 'areaWiseCommands[areaName].map(commandName =>', 'areaWiseCommands[areaName].map((commandName: string) =>') if updated != content: with open(sc_file, 'w') as f: f.write(updated) fixed += 1 # Fix ProjectManager/index.js - useState(null) needs type param for ?gdLayout pm_file = os.path.join(app_dir, 'src/ProjectManager/index.js') if os.path.isfile(pm_file): with open(pm_file) as f: content = f.read() updated = content # Add type annotation to useState calls that should be ?gdLayout updated = re.sub( r'const \[editedPropertiesLayout, setEditedPropertiesLayout\] = React\.useState\(\s*\n\s*null\s*\n\s*\)', 'const [editedPropertiesLayout, setEditedPropertiesLayout] = React.useState(\n null\n )', updated) updated = re.sub( r'const \[editedVariablesLayout, setEditedVariablesLayout\] = React\.useState\(\s*\n\s*null\s*\n\s*\)', 'const [editedVariablesLayout, setEditedVariablesLayout] = React.useState(\n null\n )', updated) if updated != content: with open(pm_file, 'w') as f: f.write(updated) fixed += 1 # Fix CollisionMasksPreview.js - use type cast for inexact/exact mismatch cmp_file = os.path.join(app_dir, 'src/ObjectEditor/Editors/SpriteEditor/CollisionMasksEditor/CollisionMasksPreview.js') if os.path.isfile(cmp_file): with open(cmp_file) as f: content = f.read() # Cast polygons to any to bypass inexact/exact mismatch updated = content.replace( 'mapVector(polygons,', 'mapVector((polygons: any),') if updated != content: with open(cmp_file, 'w') as f: f.write(updated) fixed += 1 print(f" Fixed {fixed} known annotate-exports signature gaps and type issues") PYEOF echo " Done." ############################################################################### # STEP 6: Run prettier formatting (pre-FlowFixMe) ############################################################################### echo "" echo "--- Step 6: Running prettier formatting (pre-FlowFixMe) ---" # Format all source files before adding FlowFixMe. # This ensures FlowFixMe comments will be placed at the correct post-format # line numbers. npm run format 2>&1 || true echo " Done." ############################################################################### # STEP 7: Add $FlowFixMe for all remaining errors (iterative) ############################################################################### echo "" echo "--- Step 7: Adding \$FlowFixMe for all errors (iterative) ---" npx flow stop 2>/dev/null || true PREV_ERROR_COUNT="" for i in $(seq 1 15); do echo " Iteration $i..." # Add FlowFixMe comments for errors FLOWFIXME_OUTPUT=$(python3 "$SCRIPT_DIR/add-flow-fixme.py" 2>&1 || true) echo "$FLOWFIXME_OUTPUT" | grep -E "Added|Found|No" || true # Convert // FlowFixMe in JSX context to {/* FlowFixMe */} JSX_CONVERT_OUTPUT=$(python3 "$SCRIPT_DIR/convert-jsx-flowfixme.py" 2>&1 || true) echo "$JSX_CONVERT_OUTPUT" | grep -E "Converted|Reverted" || true npx flow stop 2>/dev/null || true # Check remaining errors set +e ERROR_OUTPUT=$(npx flow 2>&1) FLOW_EXIT=$? set -e if echo "$ERROR_OUTPUT" | grep -q "No errors"; then echo " No more errors!" break fi ERROR_COUNT=$(echo "$ERROR_OUTPUT" | grep "Found" | grep -o '[0-9]*' | head -1) echo " Remaining errors: ${ERROR_COUNT:-unknown} (flow exit: $FLOW_EXIT)" if echo "$FLOWFIXME_OUTPUT" | grep -q "Added 0 \$FlowFixMe comments" && \ [ -n "$ERROR_COUNT" ] && [ "$ERROR_COUNT" = "$PREV_ERROR_COUNT" ]; then echo " No automatic progress in this iteration; stopping early." break fi PREV_ERROR_COUNT="$ERROR_COUNT" npx flow stop 2>/dev/null || true done ############################################################################### # STEP 8: Post-FlowFixMe format + fix cycle ############################################################################### echo "" echo "--- Step 8: Post-FlowFixMe format and fix cycle ---" # After adding FlowFixMe comments, formatting may introduce new errors # (e.g., FlowFixMe comments getting displaced from error lines). # Run format + flow check + FlowFixMe in a cycle until stable. for j in $(seq 1 3); do echo " Format-fix cycle iteration $j..." npm run format 2>&1 || true npx flow stop 2>/dev/null || true set +e CYCLE_CHECK=$(npx flow 2>&1) set -e if echo "$CYCLE_CHECK" | grep -q "No errors"; then echo " No errors after formatting - cycle complete!" break fi CYCLE_ERRORS=$(echo "$CYCLE_CHECK" | grep "Found" | grep -o '[0-9]*' | head -1) echo " $CYCLE_ERRORS errors after formatting, adding FlowFixMe..." python3 "$SCRIPT_DIR/add-flow-fixme.py" 2>&1 || true python3 "$SCRIPT_DIR/convert-jsx-flowfixme.py" 2>&1 || true done echo " Done." ############################################################################### # STEP 9: Final format verification ############################################################################### echo "" echo "--- Step 9: Final format verification ---" npm run format 2>&1 || true npx flow stop 2>/dev/null || true set +e FINAL_CHECK=$(npx flow 2>&1) set -e if echo "$FINAL_CHECK" | grep -q "No errors"; then echo " All clean!" else echo " WARNING: Still have errors after all cycles:" echo "$FINAL_CHECK" | grep "Found" || true fi echo " Done." ############################################################################### # STEP 10: Fix eslint-disable-next-line ordering with $FlowFixMe ############################################################################### echo "" echo "--- Step 10: Fixing eslint/FlowFixMe comment ordering ---" # When $FlowFixMe is inserted between eslint-disable-next-line and the code, # eslint-disable no longer applies (it targets the FlowFixMe comment instead). # Fix: wrap the code with eslint-disable/eslint-enable block comments, so that # both the FlowFixMe suppression and the eslint suppression work correctly. python3 << 'PYEOF' import os, re src_dir = os.path.join(os.environ.get('APP_DIR', os.getcwd()), 'src') fixed = 0 for root, dirs, files in os.walk(src_dir): for fname in files: if not fname.endswith('.js'): continue filepath = os.path.join(root, fname) with open(filepath) as f: lines = f.readlines() modified = False new_lines = [] i = 0 while i < len(lines): # Pattern: eslint-disable-next-line, then one or more $FlowFixMe[...], then code if (i + 2 < len(lines) and 'eslint-disable-next-line' in lines[i] and '$FlowFixMe[' in lines[i + 1]): m = re.search(r'eslint-disable-next-line\s+(.+)', lines[i]) if m: rule = m.group(1).strip() # Collect all consecutive FlowFixMe lines fixme_lines = [] j = i + 1 while j < len(lines) and '$FlowFixMe[' in lines[j]: fixme_lines.append(lines[j]) j += 1 if j < len(lines): code_line = lines[j] indent = '' for ch in code_line: if ch in (' ', '\t'): indent += ch else: break # Use eslint-disable/enable block wrapping FlowFixMe + code new_lines.append(f'{indent}/* eslint-disable {rule} */\n') for fl in fixme_lines: new_lines.append(fl) new_lines.append(code_line) new_lines.append(f'{indent}/* eslint-enable {rule} */\n') i = j + 1 modified = True fixed += 1 continue # Pattern: one or more $FlowFixMe[...], then eslint-disable-next-line, then code elif (i + 2 < len(lines) and '$FlowFixMe[' in lines[i] and 'eslint-disable-next-line' in lines[i + 1]): # Find where the FlowFixMe block starts fixme_start = i # The eslint line is after one FlowFixMe; collect FlowFixMe after it too m = re.search(r'eslint-disable-next-line\s+(.+)', lines[i + 1]) if m: rule = m.group(1).strip() j = i + 2 # Code line is right after eslint-disable-next-line if j < len(lines): code_line = lines[j] indent = '' for ch in code_line: if ch in (' ', '\t'): indent += ch else: break new_lines.append(f'{indent}/* eslint-disable {rule} */\n') new_lines.append(lines[i]) # Keep $FlowFixMe new_lines.append(code_line) new_lines.append(f'{indent}/* eslint-enable {rule} */\n') i = j + 1 modified = True fixed += 1 continue new_lines.append(lines[i]) i += 1 if modified: with open(filepath, 'w') as f: f.writelines(new_lines) print(f" Fixed {fixed} eslint/FlowFixMe orderings") PYEOF ############################################################################### # STEP 11: Final format after all fixes ############################################################################### echo "" echo "--- Step 11: Final format pass ---" npm run format 2>&1 || true echo " Done." echo "" echo "=== Flow Migration Fix Script Complete ===" echo "Run 'npm run flow' and 'npm run build' to verify."