1import spawnAsync from '@expo/spawn-async'; 2import chalk from 'chalk'; 3import { TaskQueue } from 'cwait'; 4import fs from 'fs-extra'; 5import glob from 'glob-promise'; 6import inquirer from 'inquirer'; 7import path from 'path'; 8import semver from 'semver'; 9 10import { EXPO_DIR, IOS_DIR, VERSIONED_RN_IOS_DIR } from '../../Constants'; 11import logger from '../../Logger'; 12import { getListOfPackagesAsync, Package } from '../../Packages'; 13import { renderExpoKitPodspecAsync } from '../../dynamic-macros/IosMacrosGenerator'; 14import { runTransformPipelineAsync } from './transforms'; 15import { injectMacros } from './transforms/injectMacros'; 16import { kernelFilesTransforms } from './transforms/kernelFilesTransforms'; 17import { podspecTransforms } from './transforms/podspecTransforms'; 18import { postTransforms } from './transforms/postTransforms'; 19import { getVersionedDirectory, getVersionedExpoKitPath } from './utils'; 20import { versionExpoModulesAsync } from './versionExpoModules'; 21import { 22 MODULES_PROVIDER_POD_NAME, 23 versionExpoModulesProviderAsync, 24} from './versionExpoModulesProvider'; 25import { runReactNativeCodegenAsync } from './versionReactNative'; 26import { 27 versionVendoredModulesAsync, 28 removeVersionedVendoredModulesAsync, 29} from './versionVendoredModules'; 30 31export { versionVendoredModulesAsync, versionExpoModulesAsync }; 32 33const UNVERSIONED_PLACEHOLDER = '__UNVERSIONED__'; 34const RELATIVE_RN_PATH = './react-native-lab/react-native'; 35 36const EXTERNAL_REACT_ABI_DEPENDENCIES = [ 37 'Amplitude', 38 'Analytics', 39 'AppAuth', 40 'FBAudienceNetwork', 41 'FBSDKCoreKit', 42 'GoogleSignIn', 43 'GoogleMaps', 44 'Google-Maps-iOS-Utils', 45 'lottie-ios', 46 'JKBigInteger2', 47 'Branch', 48 'Google-Mobile-Ads-SDK', 49 'RCT-Folly', 50]; 51 52/** 53 * Transform and rename the given react native source code files. 54 * @param filenames list of files to transform 55 * @param versionPrefix A version-specific prefix to apply to all symbols in the code, e.g. 56 * RCTSomeClass becomes {versionPrefix}RCTSomeClass 57 * @param versionedPodNames mapping from unversioned cocoapods names to versioned cocoapods names, 58 * e.g. React -> ReactABI99_0_0 59 */ 60async function namespaceReactNativeFilesAsync(filenames, versionPrefix, versionedPodNames) { 61 const reactPodName = versionedPodNames.React; 62 const transformRules = _getReactNativeTransformRules(versionPrefix, reactPodName); 63 const taskQueue = new TaskQueue(Promise, 4); // Transform up to 4 files simultaneously. 64 const transformRulesCache = {}; 65 66 const transformSingleFile = taskQueue.wrap(async (filename) => { 67 if (_isDirectory(filename)) { 68 return; 69 } 70 // protect contents of EX_UNVERSIONED macro 71 let unversionedCaptures: string[] = []; 72 await _transformFileContentsAsync(filename, (fileString) => { 73 let pattern = /EX_UNVERSIONED\((.*?)\)/g; 74 let match = pattern.exec(fileString); 75 while (match != null) { 76 unversionedCaptures.push(match[1]); 77 match = pattern.exec(fileString); 78 } 79 if (unversionedCaptures.length) { 80 return fileString.replace(pattern, UNVERSIONED_PLACEHOLDER); 81 } 82 return null; 83 }); 84 85 // rename file 86 const dirname = path.dirname(filename); 87 const basename = path.basename(filename); 88 const targetPath = path.join(dirname, `${versionPrefix}${basename}`); 89 90 // filter transformRules to patterns which apply to this dirname 91 const filteredTransformRules = 92 transformRulesCache[dirname] || _getTransformRulesForDirname(transformRules, dirname); 93 transformRulesCache[dirname] = transformRules; 94 95 // Perform sed find & replace. 96 for (const rule of filteredTransformRules) { 97 await spawnAsync('sed', [rule.flags || '-i', '--', rule.pattern, filename]); 98 } 99 100 // Rename file to be prefixed. 101 await fs.move(filename, targetPath); 102 103 // perform transforms that sed can't express 104 await _transformFileContentsAsync(targetPath, async (fileString) => { 105 // rename misc imports, e.g. Layout.h 106 fileString = fileString.replace( 107 /#(include|import)\s+"((?:[^"\/]+\/)?)([^"]+\.h)"/g, 108 (match, p1, p2, p3) => { 109 return p3.startsWith(versionPrefix) ? match : `#${p1} "${p2}${versionPrefix}${p3}"`; 110 } 111 ); 112 113 // restore EX_UNVERSIONED contents 114 if (unversionedCaptures) { 115 let index = 0; 116 do { 117 fileString = fileString.replace(UNVERSIONED_PLACEHOLDER, unversionedCaptures[index]); 118 index++; 119 } while (fileString.indexOf(UNVERSIONED_PLACEHOLDER) !== -1); 120 } 121 122 const injectedMacrosOutput = await runTransformPipelineAsync({ 123 pipeline: injectMacros(versionPrefix), 124 input: fileString, 125 targetPath, 126 }); 127 128 return await runTransformPipelineAsync({ 129 pipeline: postTransforms(versionPrefix), 130 input: injectedMacrosOutput, 131 targetPath, 132 }); 133 }); 134 // process `filename` 135 }); 136 137 await Promise.all(filenames.map(transformSingleFile)); 138} 139 140/** 141 * Transform and rename all code files we care about under `rnPath` 142 */ 143async function transformReactNativeAsync(rnPath, versionName, versionedPodNames) { 144 let filenameQueries = [`${rnPath}/**/*.[hmSc]`, `${rnPath}/**/*.mm`, `${rnPath}/**/*.cpp`]; 145 let filenames: string[] = []; 146 await Promise.all( 147 filenameQueries.map(async (query) => { 148 let queryFilenames = (await glob(query)) as string[]; 149 if (queryFilenames) { 150 filenames = filenames.concat(queryFilenames); 151 } 152 }) 153 ); 154 155 return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames); 156} 157 158/** 159 * For all files matching the given glob query, namespace and rename them 160 * with the given version number. This utility is mainly useful for backporting 161 * small changes into an existing SDK. To create a new SDK version, use `addVersionAsync` 162 * instead. 163 * @param globQuery a string to pass to glob which matches some file paths 164 * @param versionNumber Exponent SDK version, e.g. 42.0.0 165 */ 166export async function versionReactNativeIOSFilesAsync(globQuery, versionNumber) { 167 let filenames = await glob(globQuery); 168 if (!filenames || !filenames.length) { 169 throw new Error(`No files matched the given pattern: ${globQuery}`); 170 } 171 let { versionName, versionedPodNames } = await getConfigsFromArguments(versionNumber); 172 console.log(`Versioning ${filenames.length} files with SDK version ${versionNumber}...`); 173 return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames); 174} 175 176async function generateVersionedReactNativeAsync(versionName: string): Promise<void> { 177 const versionedReactNativePath = getVersionedReactNativePath(versionName); 178 179 await fs.mkdirs(versionedReactNativePath); 180 181 // Clone react native latest version 182 console.log(`Copying files from ${chalk.magenta(RELATIVE_RN_PATH)} ...`); 183 184 await fs.copy( 185 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'React'), 186 path.join(versionedReactNativePath, 'React') 187 ); 188 await fs.copy( 189 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'Libraries'), 190 path.join(versionedReactNativePath, 'Libraries') 191 ); 192 await fs.copy( 193 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'React.podspec'), 194 path.join(versionedReactNativePath, 'React.podspec') 195 ); 196 await fs.copy( 197 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'React-Core.podspec'), 198 path.join(versionedReactNativePath, 'React-Core.podspec') 199 ); 200 await fs.copy( 201 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'ReactCommon', 'ReactCommon.podspec'), 202 path.join(versionedReactNativePath, 'ReactCommon', 'ReactCommon.podspec') 203 ); 204 await fs.copy( 205 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'ReactCommon', 'React-Fabric.podspec'), 206 path.join(versionedReactNativePath, 'ReactCommon', 'React-Fabric.podspec') 207 ); 208 await fs.copy( 209 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'package.json'), 210 path.join(versionedReactNativePath, 'package.json') 211 ); 212 213 console.log(`Removing unnecessary ${chalk.magenta('*.js')} files ...`); 214 215 const jsFiles = (await glob(path.join(versionedReactNativePath, '**', '*.js'))) as string[]; 216 217 for (const jsFile of jsFiles) { 218 await fs.remove(jsFile); 219 } 220 await Promise.all(jsFiles.map((jsFile) => fs.remove(jsFile))); 221 222 console.log('Running react-native-codegen'); 223 await runReactNativeCodegenAsync(path.join(EXPO_DIR, RELATIVE_RN_PATH), versionedReactNativePath); 224 225 console.log( 226 `Copying cpp libraries from ${chalk.magenta(path.join(RELATIVE_RN_PATH, 'ReactCommon'))} ...` 227 ); 228 const cppLibraries = getCppLibrariesToVersion(); 229 230 await fs.mkdirs(path.join(versionedReactNativePath, 'ReactCommon')); 231 232 for (const library of cppLibraries) { 233 await fs.copy( 234 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'ReactCommon', library.libName), 235 path.join(versionedReactNativePath, 'ReactCommon', library.libName) 236 ); 237 } 238 239 await generateReactNativePodScriptAsync(versionedReactNativePath, versionName); 240 await generateReactNativePodspecsAsync(versionedReactNativePath, versionName); 241} 242 243/** 244 * There are some kernel files that unfortunately have to call versioned code directly. 245 * This function applies the specified changes in the kernel codebase. 246 * The nature of kernel modifications is that they are temporary and at one point these have to be rollbacked. 247 * @param versionName SDK version, e.g. 21.0.0, 37.0.0, etc. 248 * @param rollback flag indicating whether to invoke rollbacking modification. 249 */ 250async function modifyKernelFilesAsync( 251 versionName: string, 252 rollback: boolean = false 253): Promise<void> { 254 const kernelFilesPath = path.join(IOS_DIR, 'Exponent/kernel'); 255 const filenameQueries = [`${kernelFilesPath}/**/EXAppViewController.m`]; 256 let filenames: string[] = []; 257 await Promise.all( 258 filenameQueries.map(async (query) => { 259 let queryFilenames = (await glob(query)) as string[]; 260 if (queryFilenames) { 261 filenames = filenames.concat(queryFilenames); 262 } 263 }) 264 ); 265 await Promise.all( 266 filenames.map(async (filename) => { 267 console.log(`Modifying ${chalk.magenta(path.relative(EXPO_DIR, filename))}:`); 268 await _transformFileContentsAsync(filename, (fileContents) => 269 runTransformPipelineAsync({ 270 pipeline: kernelFilesTransforms(versionName, rollback), 271 targetPath: filename, 272 input: fileContents, 273 }) 274 ); 275 }) 276 ); 277} 278/** 279 * - Copies `scripts/react_native_pods.rb` script into versioned ReactNative directory. 280 * - Removes pods installed from third-party-podspecs (we don't version them). 281 * - Versions `use_react_native` method and all pods it declares. 282 */ 283async function generateReactNativePodScriptAsync( 284 versionedReactNativePath: string, 285 versionName: string 286): Promise<void> { 287 const targetAutolinkPath = path.join(versionedReactNativePath, 'scripts', 'react_native_pods.rb'); 288 289 await fs.copy( 290 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'scripts', 'react_native_pods.rb'), 291 targetAutolinkPath 292 ); 293 294 const targetSource = (await fs.readFile(targetAutolinkPath, 'utf8')) 295 .replace('def use_react_native!', `def use_react_native_${versionName}!`) 296 .replace('def use_react_native_codegen!', `def use_react_native_codegen_${versionName}!`) 297 .replace(/(\bpod\s+([^\n]+)\/third-party-podspecs\/([^\n]+))/g, '# $1') 298 .replace(/\bpod\s+'([^\']+)'/g, `pod '${versionName}$1'`) 299 .replace(/(:path => "[^"]+")/g, `$1, :project_name => '${versionName}'`); 300 301 await fs.writeFile(targetAutolinkPath, targetSource); 302} 303 304async function generateReactNativePodspecsAsync( 305 versionedReactNativePath: string, 306 versionName: string 307): Promise<void> { 308 const podspecFiles = await glob(path.join(versionedReactNativePath, '**', '*.podspec')); 309 310 for (const podspecFile of podspecFiles) { 311 const basename = path.basename(podspecFile, '.podspec'); 312 313 if (/^react$/i.test(basename)) { 314 continue; 315 } 316 317 console.log( 318 `Generating podspec for ${chalk.green(basename)} at ${chalk.magenta( 319 path.relative(versionedReactNativePath, podspecFile) 320 )} ...` 321 ); 322 323 const podspecSource = await fs.readFile(podspecFile, 'utf8'); 324 325 const podspecOutput = await runTransformPipelineAsync({ 326 pipeline: podspecTransforms(versionName), 327 input: podspecSource, 328 targetPath: podspecFile, 329 }); 330 331 // Write transformed podspec output to the prefixed file. 332 await fs.writeFile( 333 path.join(path.dirname(podspecFile), `${versionName}${basename}.podspec`), 334 podspecOutput 335 ); 336 337 // Remove original and unprefixed podspec. 338 await fs.remove(podspecFile); 339 } 340 341 await generateReactPodspecAsync(versionedReactNativePath, versionName); 342} 343 344/** 345 * @param versionName Version prefix (e.g. `ABI43_0_0`) 346 * @param sdkNumber Major version of the SDK 347 */ 348async function generateVersionedExpoAsync(versionName: string, sdkNumber: number): Promise<void> { 349 const versionedExpoKitPath = getVersionedExpoKitPath(versionName); 350 const versionedUnimodulePods = await getVersionedUnimodulePodsAsync(versionName); 351 352 await fs.mkdirs(versionedExpoKitPath); 353 354 // Copy versioned exponent modules into the clone 355 console.log(`Copying versioned native modules into the new Pod...`); 356 357 await fs.copy(path.join(IOS_DIR, 'Exponent', 'Versioned'), versionedExpoKitPath); 358 359 await fs.copy( 360 path.join(EXPO_DIR, 'ios', 'ExpoKit.podspec'), 361 path.join(versionedExpoKitPath, 'ExpoKit.podspec') 362 ); 363 364 console.log(`Generating podspec for ${chalk.green('ExpoKit')} ...`); 365 366 await generateExpoKitPodspecAsync( 367 versionedExpoKitPath, 368 versionedUnimodulePods, 369 versionName, 370 `${sdkNumber}.0.0` 371 ); 372 373 logger.info(' Generating Swift modules provider'); 374 375 await versionExpoModulesProviderAsync(sdkNumber); 376} 377 378/** 379 * Transforms ExpoKit.podspec, versioning Expo namespace, React pod name, replacing original ExpoKit podspecs 380 * with Expo and ExpoOptional. 381 * @param specfilePath location of ExpoKit.podspec to modify, e.g. /versioned-react-native/someversion/ 382 * @param versionedReactPodName name of the new pod (and podfile) 383 * @param universalModulesPodNames versioned names of universal modules 384 * @param versionNumber "XX.X.X" 385 */ 386async function generateExpoKitPodspecAsync( 387 specfilePath: string, 388 universalModulesPodNames: { [key: string]: string }, 389 versionName: string, 390 versionNumber: string 391): Promise<void> { 392 const versionedReactPodName = getVersionedReactPodName(versionName); 393 const versionedExpoKitPodName = getVersionedExpoKitPodName(versionName); 394 const specFilename = path.join(specfilePath, 'ExpoKit.podspec'); 395 const excludedPodNames = getExcludedPodNames(); 396 397 // rename spec to newPodName 398 const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"ExpoKit"/\\1"${versionedExpoKitPodName}"/g`; 399 400 await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]); 401 402 // further processing that sed can't do very well 403 await _transformFileContentsAsync(specFilename, async (fileString) => { 404 // `universalModulesPodNames` contains only versioned unimodules, 405 // so we fall back to the original name if the module is not there 406 const universalModulesDependencies = (await getListOfPackagesAsync()) 407 .filter( 408 (pkg) => 409 pkg.isIncludedInExpoClientOnPlatform('ios') && 410 pkg.podspecName && 411 !excludedPodNames.includes(pkg.podspecName) 412 ) 413 .map( 414 ({ podspecName }) => 415 `ss.dependency "${universalModulesPodNames[podspecName!] || podspecName}"` 416 ).join(` 417 `); 418 const externalDependencies = EXTERNAL_REACT_ABI_DEPENDENCIES.map( 419 (podName) => `ss.dependency "${podName}"` 420 ).join(` 421 `); 422 let subspec = `s.subspec "Expo" do |ss| 423 ss.source_files = "Core/**/*.{h,m,mm,cpp}" 424 425 ss.dependency "${versionedReactPodName}-Core" 426 ss.dependency "${versionedReactPodName}-Core/DevSupport" 427 ss.dependency "${versionedReactPodName}Common" 428 ${universalModulesDependencies} 429 ${externalDependencies} 430 ss.dependency "${versionName}${MODULES_PROVIDER_POD_NAME}" 431 end 432 433 s.subspec "ExpoOptional" do |ss| 434 ss.dependency "${versionedExpoKitPodName}/Expo" 435 ss.source_files = "Optional/**/*.{h,m,mm}" 436 end`; 437 fileString = fileString.replace( 438 /(s\.subspec ".+?"[\S\s]+?(?=end\b)end\b[\s]+)+/g, 439 `${subspec}\n` 440 ); 441 442 // correct version number 443 fileString = fileString.replace(/(?<=s.version = ").*?(?=")/g, versionNumber); 444 445 // add Reanimated V2 RCT-Folly dependency 446 fileString = fileString 447 .replace( 448 /(?=Pod::Spec.new do \|s\|)/, 449 ` 450folly_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1' 451folly_compiler_flags = folly_flags + ' ' + '-Wno-comma -Wno-shorten-64-to-32' 452boost_compiler_flags = '-Wno-documentation'\n\n` 453 ) 454 .replace( 455 /(?= s.subspec "Expo" do \|ss\|)/g, 456 ` 457 s.pod_target_xcconfig = { 458 "USE_HEADERMAP" => "YES", 459 "HEADER_SEARCH_PATHS" => "\\"$(PODS_TARGET_SRCROOT)/ReactCommon\\" \\"$(PODS_TARGET_SRCROOT)\\" \\"$(PODS_ROOT)/RCT-Folly\\" \\"$(PODS_ROOT)/boost-for-react-native\\" \\"$(PODS_ROOT)/DoubleConversion\\" \\"$(PODS_ROOT)/Headers/Private/React-Core\\" " 460 } 461 s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags 462 s.xcconfig = { 463 "HEADER_SEARCH_PATHS" => "\\"$(PODS_ROOT)/boost-for-react-native\\" \\"$(PODS_ROOT)/glog\\" \\"$(PODS_ROOT)/RCT-Folly\\" \\"$(PODS_ROOT)/Headers/Private/${versionName}React-Core\\"", 464 "OTHER_CFLAGS" => "$(inherited)" + " " + folly_flags 465 }\n\n` 466 ); 467 468 return fileString; 469 }); 470 471 // move podspec to ${versionedExpoKitPodName}.podspec 472 await fs.move(specFilename, path.join(specfilePath, `${versionedExpoKitPodName}.podspec`)); 473} 474 475/** 476 * @param specfilePath location of React.podspec to modify, e.g. /versioned-react-native/someversion/ 477 * @param versionedReactPodName name of the new pod (and podfile) 478 */ 479async function generateReactPodspecAsync(versionedReactNativePath, versionName) { 480 const versionedReactPodName = getVersionedReactPodName(versionName); 481 const versionedYogaPodName = getVersionedYogaPodName(versionName); 482 const versionedJSIPodName = getVersionedJSIPodName(versionName); 483 const specFilename = path.join(versionedReactNativePath, 'React.podspec'); 484 485 // rename spec to newPodName 486 const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"React"/\\1"${versionedReactPodName}"/g`; 487 await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]); 488 489 // rename header_dir 490 await spawnAsync('sed', [ 491 '-i', 492 '--', 493 `s/^\\(.*header_dir.*\\)React\\(.*\\)$/\\1${versionedReactPodName}\\2/`, 494 specFilename, 495 ]); 496 await spawnAsync('sed', [ 497 '-i', 498 '--', 499 `s/^\\(.*header_dir.*\\)jsireact\\(.*\\)$/\\1${versionedJSIPodName}\\2/`, 500 specFilename, 501 ]); 502 503 // point source at . 504 const newPodSource = `{ :path => "." }`; 505 await spawnAsync('sed', [ 506 '-i', 507 '--', 508 `s/\\(s\\.source[[:space:]]*=[[:space:]]\\).*/\\1${newPodSource}/g`, 509 specFilename, 510 ]); 511 512 // further processing that sed can't do very well 513 await _transformFileContentsAsync(specFilename, (fileString) => { 514 // replace React/* dependency with ${versionedReactPodName}/* 515 fileString = fileString.replace( 516 /(\.dependency\s+)"React([^"]+)"/g, 517 `$1"${versionedReactPodName}$2"` 518 ); 519 520 fileString = fileString.replace('/RCTTV', `/${versionName}RCTTV`); 521 522 // namespace cpp libraries 523 const cppLibraries = getCppLibrariesToVersion(); 524 cppLibraries.forEach(({ libName }) => { 525 fileString = fileString.replace( 526 new RegExp(`([^A-Za-z0-9_])${libName}([^A-Za-z0-9_])`, 'g'), 527 `$1${getVersionedLibraryName(libName, versionName)}$2` 528 ); 529 }); 530 531 // fix wrong Yoga pod name 532 fileString = fileString.replace( 533 /^(.*dependency.*["']).*yoga.*?(["'].*)$/m, 534 `$1${versionedYogaPodName}$2` 535 ); 536 537 return fileString; 538 }); 539 540 // move podspec to ${versionedReactPodName}.podspec 541 await fs.move( 542 specFilename, 543 path.join(versionedReactNativePath, `${versionedReactPodName}.podspec`) 544 ); 545} 546 547function getCFlagsToPrefixGlobals(prefix, globals) { 548 return globals.map((val) => `-D${val}=${prefix}${val}`); 549} 550 551/** 552 * Generates `dependencies.rb` and `postinstalls.rb` files for versioned code. 553 * @param versionNumber Semver-compliant version of the SDK/ABI 554 * @param versionName Version prefix used for versioned files, e.g. ABI99_0_0 555 * @param versionedPodNames mapping from pod names to versioned pod names, e.g. React -> ReactABI99_0_0 556 * @param versionedReactPodPath path of the new react pod 557 */ 558async function generatePodfileSubscriptsAsync( 559 versionNumber: string, 560 versionName: string, 561 versionedPodNames: Record<string, string>, 562 versionedReactPodPath: string 563) { 564 if (!versionedPodNames.React) { 565 throw new Error( 566 'Tried to add generate pod dependencies, but missing a name for the versioned library.' 567 ); 568 } 569 570 const relativeReactNativePath = path.relative(IOS_DIR, getVersionedReactNativePath(versionName)); 571 const relativeExpoKitPath = path.relative(IOS_DIR, getVersionedExpoKitPath(versionName)); 572 573 // Add a dependency on newPodName 574 const dependenciesContent = `# @generated by expotools 575 576require './${relativeReactNativePath}/scripts/react_native_pods.rb' 577 578use_react_native_${versionName}! path: './${relativeReactNativePath}' 579 580pod '${getVersionedExpoKitPodName(versionName)}', 581 :path => './${relativeExpoKitPath}', 582 :project_name => '${versionName}', 583 :subspecs => ['Expo', 'ExpoOptional'] 584 585use_pods! '{versioned,vendored}/sdk${semver.major( 586 versionNumber 587 )}/*/*.podspec.json', '${versionName}' 588`; 589 590 await fs.writeFile(path.join(versionedReactPodPath, 'dependencies.rb'), dependenciesContent); 591 592 // Add postinstall. 593 // In particular, resolve conflicting globals from React by redefining them. 594 let globals = { 595 React: [ 596 // RCTNavigator 597 'kNeverRequested', 598 'kNeverProgressed', 599 // react-native-maps 600 'kSMCalloutViewRepositionDelayForUIScrollView', 601 'regionAsJSON', 602 'unionRect', 603 // jschelpers 604 'JSNoBytecodeFileFormatVersion', 605 'JSSamplingProfilerEnabled', 606 // RCTInspectorPackagerConnection 607 'RECONNECT_DELAY_MS', 608 // RCTSpringAnimation 609 'MAX_DELTA_TIME', 610 ], 611 yoga: [ 612 'gCurrentGenerationCount', 613 'gPrintSkips', 614 'gPrintChanges', 615 'layoutNodeInternal', 616 'gDepth', 617 'gPrintTree', 618 'isUndefined', 619 'gNodeInstanceCount', 620 ], 621 }; 622 let configValues = getCFlagsToPrefixGlobals( 623 versionedPodNames.React, 624 globals.React.concat(globals.yoga) 625 ); 626 const indent = ' '.repeat(3); 627 const config = `# @generated by expotools 628 629if pod_name.start_with?('${versionedPodNames.React}') || pod_name == '${versionedPodNames.ExpoKit}' 630 target_installation_result.native_target.build_configurations.each do |config| 631 config.build_settings['OTHER_CFLAGS'] = %w[ 632 ${configValues.join(`\n${indent}`)} 633 ] 634 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] 635 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV=1' 636 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_ENABLE_INSPECTOR=0' 637 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}ENABLE_PACKAGER_CONNECTION=0' 638 # Enable Google Maps support 639 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS=1' 640 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS_UTILS=1' 641 end 642end 643`; 644 await fs.writeFile(path.join(versionedReactPodPath, 'postinstalls.rb'), config); 645} 646 647/** 648 * @param transformConfig function that takes a config dict and returns a new config dict. 649 */ 650async function modifyVersionConfigAsync(configPath, transformConfig) { 651 let jsConfigFilename = `${configPath}/sdkVersions.json`; 652 await _transformFileContentsAsync(jsConfigFilename, (jsConfigContents) => { 653 let jsConfig; 654 655 // read the existing json config and add the new version to the sdkVersions array 656 try { 657 jsConfig = JSON.parse(jsConfigContents); 658 } catch (e) { 659 console.log('Error parsing existing sdkVersions.json file, writing a new one...', e); 660 console.log('The erroneous file contents was:', jsConfigContents); 661 jsConfig = { 662 sdkVersions: [], 663 }; 664 } 665 // apply changes 666 jsConfig = transformConfig(jsConfig); 667 return JSON.stringify(jsConfig); 668 }); 669 670 // convert json config to plist for iOS 671 await spawnAsync('plutil', [ 672 '-convert', 673 'xml1', 674 jsConfigFilename, 675 '-o', 676 path.join(configPath, 'EXSDKVersions.plist'), 677 ]); 678} 679 680function validateAddVersionDirectories(rootPath, newVersionPath) { 681 // Make sure the paths we want to read are available 682 let relativePathsToCheck = [ 683 RELATIVE_RN_PATH, 684 'ios/versioned-react-native', 685 'ios/Exponent', 686 'ios/Exponent/Versioned', 687 ]; 688 let isValid = true; 689 relativePathsToCheck.forEach((path) => { 690 try { 691 fs.accessSync(`${rootPath}/${path}`, fs.constants.F_OK); 692 } catch (e) { 693 console.log(`${rootPath}/${path} does not exist or is otherwise inaccessible`); 694 isValid = false; 695 } 696 }); 697 // Also, make sure the version we're about to write doesn't already exist 698 try { 699 // we want this to fail 700 fs.accessSync(newVersionPath, fs.constants.F_OK); 701 console.log(`${newVersionPath} already exists, will not overwrite`); 702 isValid = false; 703 } catch (e) {} 704 705 return isValid; 706} 707 708function validateRemoveVersionDirectories(rootPath, newVersionPath) { 709 let pathsToCheck = [ 710 `${rootPath}/ios/versioned-react-native`, 711 `${rootPath}/ios/Exponent`, 712 newVersionPath, 713 ]; 714 let isValid = true; 715 pathsToCheck.forEach((path) => { 716 try { 717 fs.accessSync(path, fs.constants.F_OK); 718 } catch (e) { 719 console.log(`${path} does not exist or is otherwise inaccessible`); 720 isValid = false; 721 } 722 }); 723 return isValid; 724} 725 726async function getConfigsFromArguments(versionNumber) { 727 let versionComponents = versionNumber.split('.'); 728 versionComponents = versionComponents.map((number) => parseInt(number, 10)); 729 let versionName = 'ABI' + versionNumber.replace(/\./g, '_'); 730 let rootPathComponents = EXPO_DIR.split('/'); 731 let versionPathComponents = path.join('ios', 'versioned-react-native', versionName).split('/'); 732 let newVersionPath = rootPathComponents.concat(versionPathComponents).join('/'); 733 734 let versionedPodNames = { 735 React: getVersionedReactPodName(versionName), 736 yoga: getVersionedYogaPodName(versionName), 737 ExpoKit: getVersionedExpoKitPodName(versionName), 738 jsireact: getVersionedJSIPodName(versionName), 739 }; 740 741 return { 742 sdkNumber: semver.major(versionNumber), 743 versionName, 744 newVersionPath, 745 versionedPodNames, 746 versionComponents, 747 }; 748} 749 750async function getVersionedUnimodulePodsAsync( 751 versionName: string 752): Promise<{ [key: string]: string }> { 753 const versionedUnimodulePods = {}; 754 const packages = await getListOfPackagesAsync(); 755 const excludedPodNames = getExcludedPodNames(); 756 757 packages.forEach((pkg) => { 758 const podName = pkg.podspecName; 759 if (podName && pkg.isVersionableOnPlatform('ios') && !excludedPodNames.includes(podName)) { 760 versionedUnimodulePods[podName] = `${versionName}${podName}`; 761 } 762 }); 763 764 return versionedUnimodulePods; 765} 766 767function getVersionedReactPodName(versionName: string): string { 768 return getVersionedLibraryName('React', versionName); 769} 770 771function getVersionedYogaPodName(versionName: string): string { 772 return getVersionedLibraryName('Yoga', versionName); 773} 774 775function getVersionedJSIPodName(versionName: string): string { 776 return getVersionedLibraryName('jsiReact', versionName); 777} 778 779function getVersionedExpoKitPodName(versionName: string): string { 780 return getVersionedLibraryName('ExpoKit', versionName); 781} 782 783function getVersionedLibraryName(libraryName: string, versionName: string): string { 784 return `${versionName}${libraryName}`; 785} 786 787function getVersionedReactNativePath(versionName: string): string { 788 return path.join(VERSIONED_RN_IOS_DIR, versionName, 'ReactNative'); 789} 790 791function getVersionedExpoPath(versionName: string): string { 792 return path.join(VERSIONED_RN_IOS_DIR, versionName, 'Expo'); 793} 794 795function getCppLibrariesToVersion() { 796 return [ 797 { 798 libName: 'cxxreact', 799 }, 800 { 801 libName: 'jsi', 802 }, 803 { 804 libName: 'jsiexecutor', 805 customHeaderDir: 'jsireact', 806 }, 807 { 808 libName: 'jsinspector', 809 }, 810 { 811 libName: 'yoga', 812 }, 813 { 814 libName: 'react', 815 }, 816 { 817 libName: 'callinvoker', 818 customHeaderDir: 'ReactCommon', 819 }, 820 { 821 libName: 'reactperflogger', 822 }, 823 { 824 libName: 'runtimeexecutor', 825 }, 826 ]; 827} 828 829function getExcludedPodNames() { 830 // we don't want Payments in Expo Client versions for now 831 return ['EXPaymentsStripe']; 832} 833 834export async function addVersionAsync(versionNumber: string, packages: Package[]) { 835 const { 836 sdkNumber, 837 versionName, 838 newVersionPath, 839 versionedPodNames, 840 } = await getConfigsFromArguments(versionNumber); 841 842 // Validate the directories we need before doing anything 843 console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`); 844 let isFilesystemReady = validateAddVersionDirectories(EXPO_DIR, newVersionPath); 845 if (!isFilesystemReady) { 846 throw new Error('Aborting: At least one directory we need is not available'); 847 } 848 849 if (!versionedPodNames.React) { 850 throw new Error('Missing name for versioned pod dependency.'); 851 } 852 853 // Create ABIXX_0_0 directory. 854 console.log( 855 `Creating new ABI version ${chalk.cyan(versionNumber)} at ${chalk.magenta( 856 path.relative(EXPO_DIR, newVersionPath) 857 )}` 858 ); 859 await fs.mkdirs(newVersionPath); 860 861 // Generate new Podspec from the existing React.podspec 862 console.log('Generating versioned ReactNative directory...'); 863 await generateVersionedReactNativeAsync(versionName); 864 865 console.log( 866 `Generating ${chalk.magenta( 867 path.relative(EXPO_DIR, getVersionedExpoPath(versionName)) 868 )} directory...` 869 ); 870 await generateVersionedExpoAsync(versionName, sdkNumber); 871 872 await versionExpoModulesAsync(sdkNumber, packages); 873 874 // Generate versioned Swift modules provider 875 await versionExpoModulesProviderAsync(sdkNumber); 876 877 // Namespace the new React clone 878 console.log('Namespacing/transforming files...'); 879 await transformReactNativeAsync(newVersionPath, versionName, versionedPodNames); 880 881 // Generate Ruby scripts with versioned dependencies and postinstall actions that will be evaluated in the Expo client's Podfile. 882 console.log('Adding dependency to root Podfile...'); 883 await generatePodfileSubscriptsAsync( 884 versionNumber, 885 versionName, 886 versionedPodNames, 887 newVersionPath 888 ); 889 890 // Add the new version to the iOS config list of available versions 891 console.log('Registering new version under sdkVersions config...'); 892 const addVersionToConfig = (config, versionNumber) => { 893 config.sdkVersions.push(versionNumber); 894 return config; 895 }; 896 await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) => 897 addVersionToConfig(config, versionNumber) 898 ); 899 await modifyVersionConfigAsync( 900 path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'), 901 (config) => addVersionToConfig(config, versionNumber) 902 ); 903 904 // Modifying kernel files 905 console.log(`Modifying ${chalk.bold('kernel files')} to incorporate new SDK version...`); 906 await modifyKernelFilesAsync(versionName); 907 908 console.log('Removing any `filename--` files from the new pod ...'); 909 910 try { 911 const minusMinusFiles = await glob(path.join(newVersionPath, '**', '*--')); 912 for (const minusMinusFile of minusMinusFiles) { 913 await fs.remove(minusMinusFile); 914 } 915 } catch (error) { 916 console.warn( 917 "The script wasn't able to remove any possible `filename--` files created by sed. Please ensure there are no such files manually." 918 ); 919 } 920 921 console.log('Finished creating new version.'); 922} 923 924async function askToReinstallPodsAsync(): Promise<boolean> { 925 if (process.env.CI) { 926 // If we're on the CI, let's regenerate Pods by default. 927 return true; 928 } 929 const { result } = await inquirer.prompt<{ result: boolean }>([ 930 { 931 type: 'confirm', 932 name: 'result', 933 message: 'Do you want to reinstall pods?', 934 default: true, 935 }, 936 ]); 937 return result; 938} 939 940export async function reinstallPodsAsync(force?: boolean, preventReinstall?: boolean) { 941 if ( 942 preventReinstall !== true && 943 (force || (force !== false && (await askToReinstallPodsAsync()))) 944 ) { 945 await spawnAsync('pod', ['install'], { stdio: 'inherit', cwd: IOS_DIR }); 946 console.log( 947 'Regenerated Podfile and installed new pods. You can now try to build the project in Xcode.' 948 ); 949 } else { 950 console.log( 951 'Skipped pods regeneration. You might want to run `et ios-generate-dynamic-macros`, then `pod install` in `ios` to configure Xcode project.' 952 ); 953 } 954} 955 956export async function removeVersionAsync(versionNumber: string) { 957 const { 958 sdkNumber, 959 newVersionPath, 960 versionedPodNames, 961 versionName, 962 } = await getConfigsFromArguments(versionNumber); 963 964 console.log( 965 `Removing SDK version ${chalk.cyan(versionNumber)} from ${chalk.magenta( 966 path.relative(EXPO_DIR, newVersionPath) 967 )} with Pod name ${chalk.green(versionedPodNames.React)}` 968 ); 969 970 // Validate the directories we need before doing anything 971 console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`); 972 let isFilesystemReady = validateRemoveVersionDirectories(EXPO_DIR, newVersionPath); 973 if (!isFilesystemReady) { 974 console.log('Aborting: At least one directory we expect is not available'); 975 return; 976 } 977 978 // remove directory 979 console.log( 980 `Removing versioned files under ${chalk.magenta(path.relative(EXPO_DIR, newVersionPath))}...` 981 ); 982 await fs.remove(newVersionPath); 983 await fs.remove(getVersionedDirectory(sdkNumber)); 984 985 console.log('Removing vendored libraries...'); 986 await removeVersionedVendoredModulesAsync(semver.major(versionNumber)); 987 988 // remove dep from main podfile 989 console.log(`Removing ${chalk.green(versionedPodNames.React)} dependency from root Podfile...`); 990 991 // remove from sdkVersions.json 992 console.log('Unregistering version from sdkVersions config...'); 993 const removeVersionFromConfig = (config, versionNumber) => { 994 let index = config.sdkVersions.indexOf(versionNumber); 995 if (index > -1) { 996 // modify in place 997 config.sdkVersions.splice(index, 1); 998 } 999 return config; 1000 }; 1001 await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) => 1002 removeVersionFromConfig(config, versionNumber) 1003 ); 1004 await modifyVersionConfigAsync( 1005 path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'), 1006 (config) => removeVersionFromConfig(config, versionNumber) 1007 ); 1008 1009 // modify kernel files 1010 console.log('Rollbacking SDK modifications from kernel files...'); 1011 await modifyKernelFilesAsync(versionName, true); 1012 1013 // Update `ios/ExpoKit.podspec` with the newest SDK version 1014 logger.info(' Updating ExpoKit podspec'); 1015 await renderExpoKitPodspecAsync(EXPO_DIR, path.join(EXPO_DIR, 'template-files')); 1016 1017 await reinstallPodsAsync(); 1018} 1019 1020/** 1021 * @return an array of objects representing react native transform rules. 1022 * objects must contain 'pattern' and may optionally contain 'paths' to limit 1023 * the transform to certain file paths. 1024 * 1025 * the rules are applied in order! 1026 */ 1027function _getReactNativeTransformRules(versionPrefix, reactPodName) { 1028 const cppLibraries = getCppLibrariesToVersion().map((lib) => lib.customHeaderDir || lib.libName); 1029 const versionedLibs = [...cppLibraries, 'React', 'FBLazyVector', 'FBReactNativeSpec']; 1030 1031 return [ 1032 { 1033 // Change Obj-C symbols prefix 1034 pattern: `s/RCT/${versionPrefix}RCT/g`, 1035 }, 1036 { 1037 pattern: `s/^EX/${versionPrefix}EX/g`, 1038 // paths: 'EX', 1039 }, 1040 { 1041 pattern: `s/^UM/${versionPrefix}UM/g`, 1042 // paths: 'EX', 1043 }, 1044 { 1045 pattern: `s/\\([^\\<\\/"]\\)YG/\\1${versionPrefix}YG/g`, 1046 }, 1047 { 1048 pattern: `s/\\([\\<,]\\)YG/\\1${versionPrefix}YG/g`, 1049 }, 1050 { 1051 pattern: `s/^YG/${versionPrefix}YG/g`, 1052 }, 1053 { 1054 paths: 'Components', 1055 pattern: `s/\\([^+]\\)AIR/\\1${versionPrefix}AIR/g`, 1056 }, 1057 { 1058 pattern: `s/\\([^A-Za-z0-9_]\\)EX/\\1${versionPrefix}EX/g`, 1059 }, 1060 { 1061 pattern: `s/\\([^A-Za-z0-9_]\\)UM/\\1${versionPrefix}UM/g`, 1062 }, 1063 { 1064 pattern: `s/\\([^A-Za-z0-9_+]\\)ART/\\1${versionPrefix}ART/g`, 1065 }, 1066 { 1067 pattern: `s/ENABLE_PACKAGER_CONNECTION/${versionPrefix}ENABLE_PACKAGER_CONNECTION/g`, 1068 }, 1069 { 1070 paths: 'Components', 1071 pattern: `s/\\([^A-Za-z0-9_+]\\)SM/\\1${versionPrefix}SM/g`, 1072 }, 1073 { 1074 paths: 'Core/Api', 1075 pattern: `s/\\([^A-Za-z0-9_+]\\)RN/\\1${versionPrefix}RN/g`, 1076 }, 1077 { 1078 paths: 'Core/Api', 1079 pattern: `s/^RN/${versionPrefix}RN/g`, 1080 }, 1081 { 1082 paths: 'Core/Api', 1083 pattern: `s/HAVE_GOOGLE_MAPS/${versionPrefix}HAVE_GOOGLE_MAPS/g`, 1084 }, 1085 { 1086 paths: 'Core/Api', 1087 pattern: `s/#import "Branch/#import "${versionPrefix}Branch/g`, 1088 }, 1089 { 1090 paths: 'Core/Api', 1091 pattern: `s/#import "NSObject+RNBranch/#import "${versionPrefix}NSObject+RNBranch/g`, 1092 }, 1093 { 1094 // React will be prefixed in a moment 1095 pattern: `s/#import <${versionPrefix}RCTAnimation/#import <React/g`, 1096 }, 1097 { 1098 paths: 'Core/Api/Reanimated', 1099 pattern: `s/\\([^A-Za-z0-9_+]\\)REA/\\1${versionPrefix}REA/g`, 1100 }, 1101 { 1102 pattern: `s/^REA/${versionPrefix}REA/g`, 1103 paths: 'Core/Api/Reanimated', 1104 }, 1105 { 1106 // Prefixes all direct references to objects under `reanimated` namespace. 1107 // It must be applied before versioning `namespace reanimated` so 1108 // `using namespace reanimated::` don't get versioned twice. 1109 pattern: `s/reanimated::/${versionPrefix}reanimated::/g`, 1110 }, 1111 { 1112 // Prefixes reanimated namespace. 1113 pattern: `s/namespace reanimated/namespace ${versionPrefix}reanimated/g`, 1114 }, 1115 { 1116 // Fix imports in C++ libs in ReactCommon. 1117 // Extended syntax (-E) is required to use (a|b). 1118 flags: '-Ei', 1119 pattern: `s/([<"])(${versionedLibs.join( 1120 '|' 1121 )})\\//\\1${versionPrefix}\\2\\/${versionPrefix}/g`, 1122 }, 1123 { 1124 // Change React -> new pod name 1125 // e.g. threads and queues namespaced to com.facebook.react, 1126 // file paths beginning with the lib name, 1127 // the cpp facebook::react namespace, 1128 // iOS categories ending in +React 1129 flags: '-Ei', 1130 pattern: `s/[Rr]eact/${reactPodName}/g`, 1131 }, 1132 { 1133 // Imports from cxxreact and jsireact got prefixed twice. 1134 flags: '-Ei', 1135 pattern: `s/([<"])(${versionPrefix})(cxx|jsi)${versionPrefix}React/\\1\\2\\3react/g`, 1136 }, 1137 { 1138 // Fix imports from files like `UIView+React.*`. 1139 flags: '-Ei', 1140 pattern: `s/\\+${versionPrefix}React/\\+React/g`, 1141 }, 1142 { 1143 // Prefixes all direct references to objects under `facebook` namespace. 1144 // It must be applied before versioning `namespace facebook` so 1145 // `using namespace facebook::` don't get versioned twice. 1146 pattern: `s/facebook::/${versionPrefix}facebook::/g`, 1147 }, 1148 { 1149 // Prefixes facebook namespace. 1150 pattern: `s/namespace facebook/namespace ${versionPrefix}facebook/g`, 1151 }, 1152 { 1153 // For UMReactNativeAdapter 1154 // Fix names with 'React' substring occurring twice - only first one should be prefixed 1155 flags: '-Ei', 1156 pattern: `s/${versionPrefix}UM([[:alpha:]]*)${reactPodName}/${versionPrefix}UM\\1React/g`, 1157 }, 1158 { 1159 // For EXReactNativeAdapter 1160 pattern: `s/${versionPrefix}EX${reactPodName}/${versionPrefix}EXReact/g`, 1161 }, 1162 { 1163 // For EXConstants and EXNotifications so that when their migrators 1164 // try to access legacy storage for UUID migration, they access the proper value. 1165 pattern: `s/${versionPrefix}EXDeviceInstallUUIDKey/EXDeviceInstallUUIDKey/g`, 1166 paths: 'Expo', 1167 }, 1168 { 1169 // For EXConstants and EXNotifications so that the installation ID 1170 // stays the same between different SDK versions. (https://github.com/expo/expo/issues/11008#issuecomment-726370187) 1171 pattern: `s/${versionPrefix}EXDeviceInstallationUUIDKey/EXDeviceInstallationUUIDKey/g`, 1172 paths: 'Expo', 1173 }, 1174 { 1175 // RCTPlatform exports version of React Native 1176 pattern: `s/${reactPodName}NativeVersion/reactNativeVersion/g`, 1177 }, 1178 { 1179 pattern: `s/@"${versionPrefix}RCT"/@"RCT"/g`, 1180 }, 1181 { 1182 // Unversion EXGL_CPP imports: `<ABI37_0_0EXGL_CPP/` => `<EXGL_CPP/` 1183 pattern: `s/<${versionPrefix}EXGL_CPP\\//<EXGL_CPP\\//g`, 1184 }, 1185 { 1186 // Unprefix everything that got prefixed twice or more times. 1187 flags: '-Ei', 1188 pattern: `s/(${versionPrefix}){2,}/\\1/g`, 1189 }, 1190 { 1191 flags: '-Ei', 1192 pattern: `s/#import <(Expo|RNReanimated)/#import <${versionPrefix}\\1/g`, 1193 }, 1194 ]; 1195} 1196 1197function _getTransformRulesForDirname(transformRules, dirname) { 1198 return transformRules.filter((rule) => { 1199 return ( 1200 // no paths specified, so apply rule to everything 1201 !rule.paths || 1202 // otherwise, limit this rule to paths specified 1203 dirname.indexOf(rule.paths) !== -1 1204 ); 1205 }); 1206} 1207 1208// TODO: use the one in XDL 1209function _isDirectory(dir) { 1210 try { 1211 if (fs.statSync(dir).isDirectory()) { 1212 return true; 1213 } 1214 1215 return false; 1216 } catch (e) { 1217 return false; 1218 } 1219} 1220 1221// TODO: use the one in XDL 1222async function _transformFileContentsAsync( 1223 filename: string, 1224 transform: (fileString: string) => Promise<string> | string | null 1225) { 1226 let fileString = await fs.readFile(filename, 'utf8'); 1227 let newFileString = await transform(fileString); 1228 if (newFileString !== null) { 1229 await fs.writeFile(filename, newFileString); 1230 } 1231} 1232