node.jsreact-nativedetoxmetro-bundler

How to programmatically start/stop Metro Bundler


I'm trying to setup Continuous Integration for a React-Native project and run into some problems with the end to end testing, notably around the Metro bundler.

It seems that using the react-native script is not reliable in this case:

I would like to write a custom script that can start Metro, run the tests once the server is ready, and finally stop the server to cleanup the environment.


Solution

  • The metro bundler must run as a separate process to be able to serve requests. The way to do that is by using Child Process : Spawn and keep the returned object to properly cleanup.

    Here's a basic script that launches both Metro and Gradle in parallel and wait until both are ready based on their logging output.

    'use strict';
    
    const cp = require('child_process');
    const fs = require('fs');
    const readline = require('readline');
    
    // List of sub processes kept for proper cleanup
    const children = {};
    
    async function asyncPoint(ms, callback = () => {}) {
      return await new Promise(resolve => setTimeout(() => {
        resolve(callback());
      }, ms));
    }
    
    async function fork(name, cmd, args, {readyRegex, timeout} = {}) {
    
      return new Promise((resolve) => {
    
        const close = () => {
          delete children[name];
          resolve(false);
        };
    
        if(timeout) {
          setTimeout(() => close, timeout);
        }
    
        const child = cp.spawn(
          cmd,
          args,
          {
            silent: false,
            stdio: [null, 'pipe', 'pipe'],
          },
        );
    
        child.on('close', close);
        child.on('exit', close);
        child.on('error', close);
    
        const output = fs.createWriteStream(`./volatile-build-${name}.log`);
    
        const lineCb = (line) => {
          console.log(`[${name}] ${line}`);
          output.write(line+'\n');
          if (readyRegex && line.match(readyRegex)) {
            resolve(true);
          }
        };
    
        readline.createInterface({
          input: child.stdout,
        }).on('line', lineCb);
    
        readline.createInterface({
          input: child.stderr,
        }).on('line', lineCb);
    
        children[name] = child;
      });
    }
    
    async function sighandle() {
      console.log('\nClosing...');
      Object.values(children).forEach(child => child.kill('SIGTERM'));
      await asyncPoint(1000);
      process.exit(0);
    }
    
    function setSigHandler() {
      process.on('SIGINT', sighandle);
      process.on('SIGTERM', sighandle);
    }
    
    async function main() {
    
      setSigHandler();
    
      // Metro Bundler
      const metroSync = fork(
        'metro',
        process.argv0,
        [ // args
          './node_modules/react-native/local-cli/cli.js', 
          'start',
        ],
        { // options
          readyRegex: /Loading dependency graph, done./,
          timeout: 60000,
        }
      );
    
      // Build APK
      const buildSync = fork(
        'gradle',
        './android/gradlew', 
        [ // args
          `--project-dir=${__dirname}/android`,
          'assembleDebug',
        ],
        { // options
          readyRegex: /BUILD SUCCESSFUL/,
          timeout: 300000,
        }
      );
    
      if (await metroSync && await buildSync) {
    
        // TODO: Run tests here
    
      }
    
      sighandle();
    }
    
    main();