/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow * @format * @oncall react_native */ 'use strict'; /*:: import type {ProjectInfo} from '../utils/monorepo'; */ const {retry} = require('../circleci/retry'); const {REPO_ROOT} = require('../consts'); const {getPackages} = require('../utils/monorepo'); const { VERDACCIO_SERVER_URL, VERDACCIO_STORAGE_PATH, setupVerdaccio, } = require('./utils/verdaccio'); const {parseArgs} = require('@pkgjs/parseargs'); const chalk = require('chalk'); const {execSync} = require('child_process'); const path = require('path'); const {popd, pushd} = require('shelljs'); const fs = require('fs'); const config = { options: { projectName: {type: 'string'}, directory: {type: 'string'}, currentBranch: {type: 'string'}, verbose: {type: 'boolean', default: false}, help: {type: 'boolean'}, }, }; async function main() { const { values: {help, ...options}, } = parseArgs(config); if (help) { console.log(` Usage: node ./scripts/e2e/init-template-e2e.js [OPTIONS] Bootstraps and runs \`react-native init\`, using the currently checked out repository as the source of truth for the react-native package and dependencies. - Configures and starts a local npm proxy (Verdaccio). - Builds and publishes all in-repo dependencies to the local npm proxy. - Runs \`react-native init\` with the local npm proxy configured. - Does NOT install CocoaPods dependencies. Note: This script will mutate the contents of some package files, which should not be committed. Options: --projectName The name of the new React Native project. --currentBranch The current branch to checkout. --directory The absolute path to the target project directory. --pathToLocalReactNative The absolute path to the local react-native package. --verbose Print additional output. Default: false. `); return; } await initNewProjectFromSource(options); // TODO(T179377112): Fix memory leak from `spawn` in `setupVerdaccio` (above // kill command does not wait for kill success). process.exit(0); } async function initNewProjectFromSource( { projectName, directory, currentBranch, pathToLocalReactNative = null, verbose = false, } /*: {projectName: string, directory: string, currentBranch: string, pathToLocalReactNative?: ?string, verbose?: boolean} */, ) { console.log('Starting local npm proxy (Verdaccio)'); const verdaccioPid = setupVerdaccio(); console.log('Done ✅'); try { execSync('node ./scripts/build/build.js', { cwd: REPO_ROOT, stdio: 'inherit', }); console.log('\nDone ✅'); const packages = await getPackages({ includeReactNative: false, includePrivate: false, }); // packages are updated in a lockstep, let's get the version of the first one const version = packages[Object.keys(packages)[0]].packageJson.version; console.log('Publishing packages to local npm proxy\n'); for (const {path: packagePath, packageJson} of Object.values(packages)) { const desc = `${packageJson.name} (${path.relative( REPO_ROOT, packagePath, )})`; process.stdout.write( `${desc} ${chalk.dim('.').repeat(Math.max(0, 72 - desc.length))} `, ); execSync( `npm publish --registry ${VERDACCIO_SERVER_URL} --access public`, { cwd: packagePath, stdio: verbose ? 'inherit' : [process.stderr], }, ); process.stdout.write(chalk.reset.inverse.bold.green(' DONE ') + '\n'); } console.log('\nDone ✅'); const pathToTemplate = _prepareTemplate( version, pathToLocalReactNative, currentBranch, ); console.log('Running react-native init without install'); execSync( `npx @react-native-community/cli@next init ${projectName} \ --directory ${directory} \ --template file://${pathToTemplate} \ --verbose \ --pm npm \ --skip-install`, { // Avoid loading packages/react-native/react-native.config.js cwd: REPO_ROOT, stdio: 'inherit', }, ); console.log('\nDone ✅'); console.log('Installing project dependencies'); await installProjectUsingProxy(directory); console.log('Done ✅'); } catch (e) { console.log('Failed ❌'); throw e; } finally { console.log(`Cleanup: Killing Verdaccio process (PID: ${verdaccioPid})`); try { execSync(`kill -9 ${verdaccioPid}`); console.log('Done ✅'); } catch { console.warn('Failed to kill Verdaccio process'); } console.log('Cleanup: Removing Verdaccio storage directory'); execSync(`rm -rf ${VERDACCIO_STORAGE_PATH}`); console.log('Done ✅'); } } async function installProjectUsingProxy(cwd /*: string */) { const execOptions = { cwd, stdio: 'inherit', }; // TODO(huntie): Review pre-existing retry limit const success = await retry('npm', execOptions, 3, 500, [ 'install', '--registry', VERDACCIO_SERVER_URL, ]); if (!success) { throw new Error('Failed to install project dependencies'); } } function _updateScopedPackages( packages /*: ProjectInfo */, directory /*: string */, version /*: string */, ) { console.log( 'Updating the scoped packagesto match the version published in Verdaccio', ); // Update scoped packages which starts with @react-native const appPackageJsonPath = path.join(directory, 'package.json'); const appPackageJson = JSON.parse( fs.readFileSync(appPackageJsonPath, 'utf8'), ); for (const [key, _] of Object.entries(appPackageJson.dependencies)) { if (key.startsWith('@react-native')) { appPackageJson.dependencies[key] = version; } } for (const [key, _] of Object.entries(appPackageJson.devDependencies)) { if (key.startsWith('@react-native')) { appPackageJson.devDependencies[key] = version; } } fs.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2)); console.log('Done ✅'); } function _prepareTemplate( version /*: string */, pathToLocalReactNative /*: ?string*/, currentBranch /*: string*/, ) { console.log('Prepare template locally'); const templateCloneBaseFolder = '/tmp/react-native-tmp/template'; execSync(`rm -rf ${templateCloneBaseFolder}`); const templateCloneFolder = path.join(templateCloneBaseFolder, 'template'); execSync( `git clone https://github.com/react-native-community/template ${templateCloneBaseFolder}`, ); pushd(templateCloneBaseFolder); execSync(`git checkout ${currentBranch}`); pushd('template'); // read the package.json const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); // and update the dependencies and devDependencies of packages scoped as @react-native // to the version passed as parameter for (const [key, _] of Object.entries(packageJson.dependencies)) { if (key.startsWith('@react-native')) { packageJson.dependencies[key] = version; } } for (const [key, _] of Object.entries(packageJson.devDependencies)) { if (key.startsWith('@react-native')) { packageJson.devDependencies[key] = version; } } if (pathToLocalReactNative != null) { packageJson.dependencies['react-native'] = `file:${pathToLocalReactNative}`; } // write the package.json to disk fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2)); popd(); const templateTgz = execSync(`npm pack`).toString().trim(); popd(); console.log('Done ✅'); return path.join(templateCloneBaseFolder, templateTgz); } module.exports = { initNewProjectFromSource, }; if (require.main === module) { // eslint-disable-next-line no-void void main(); }