mAAdhaTTah/brookjs

View on GitHub
packages/brookjs-cli/src/cli/App.tsx

Summary

Maintainability
B
4 hrs
Test Coverage
import path from 'path';
import vm from 'vm';
import React from 'react';
import { defaultLoaders, cosmiconfigSync } from 'cosmiconfig';
import { render, RenderOptions } from 'ink';
import { TransformOptions, transformFileSync } from '@babel/core';
import * as t from 'io-ts';
import resolve from 'resolve';
import { BabelRC } from '../babel';
import { Command } from './Command';
import Commands from './Commands';
import ErrorBoundary, {
  CommandValidationError,
  LoadDirError,
  Root,
} from './components';

const RC = t.partial({
  babel: BabelRC,
});

type RC = t.Type<typeof RC>;

export class App {
  private rc?: unknown;

  static create(name: string, commands?: Commands) {
    return new App(name, commands);
  }

  static resolve(target: string, basedir?: string): string {
    return resolve.sync(target, {
      basedir,
      extensions: ['.js', '.jsx', '.mjs', '.ts', '.tsx'],
    });
  }

  static load(target: string) {
    const filename = App.resolve(target);

    const oldNodeEnv = process.env.NODE_ENV;
    process.env.NODE_ENV = 'production';
    const result = transformFileSync(filename, {
      babelrc: false,
      configFile: false,
      presets: [
        [
          require.resolve('babel-preset-brookjs'),
          {
            useESModules: false,
            helpers: false,
          },
        ],
      ],
      plugins: [require.resolve('@babel/plugin-transform-modules-commonjs')],
    });
    process.env.NODE_ENV = oldNodeEnv;

    if (result?.code == null) {
      throw new Error('No code returned from transform');
    }

    const exports = {};
    const module = { exports };

    vm.compileFunction(
      result.code,
      ['module', 'exports', '__dirname', 'require'],
      {
        filename,
      },
    )(module, exports, path.dirname(filename), (target: string) => {
      const resolvedTarget = App.resolve(target, path.dirname(filename));

      if (
        resolvedTarget.includes('node_modules') ||
        !resolvedTarget.includes(path.sep)
      ) {
        return require(resolvedTarget);
      }

      return this.load(resolvedTarget);
    });

    return module.exports;
  }

  private constructor(
    private name: string,
    private commands: Commands = new Commands(),
    private errors: React.ReactNode[] = [],
  ) {}

  addCommand(name: string, cmd: unknown): App {
    return Command.decode(cmd).fold(
      errors =>
        new App(this.name, this.commands, [
          ...this.errors,
          <CommandValidationError
            key={this.errors.length}
            name={name}
            errors={errors}
          />,
        ]),
      cmd =>
        new App(this.name, this.commands.add(cmd as Command<any>), this.errors),
    );
  }

  loadCommandsFrom(target: string) {
    try {
      return Object.entries(App.load(target)).reduce<App>(
        (app, [name, cmd]) => app.addCommand(name, cmd),
        this,
      );
    } catch (error) {
      return new App(this.name, this.commands, [
        ...this.errors,
        <LoadDirError key={this.errors.length} error={error} dir={target} />,
      ]);
    }
  }

  getRC(): unknown {
    if (this.rc !== undefined) {
      return this.rc;
    }

    return (this.rc =
      cosmiconfigSync(this.name, {
        loaders: {
          ...defaultLoaders,
          '.js': (filename: string) => App.load(filename),
          '.ts': (filename: string) => App.load(filename),
          '.tsx': (filename: string) => App.load(filename),
        },
      }).search()?.config ?? null);
  }

  getBabelConfig(base: TransformOptions): TransformOptions {
    return (
      RC.decode(this.getRC()).getOrElse({})?.babel?.modifier?.(base) ?? base
    );
  }

  run(
    argv: string[],
    { cwd = process.cwd(), ...opts }: RenderOptions & { cwd?: string } = {},
  ) {
    const rc = this.getRC();

    const { command, args } = this.commands.get(argv);

    const instance = render(
      <ErrorBoundary>
        <Root {...{ command, argv, args, cwd, rc, errors: this.errors }} />
      </ErrorBoundary>,
      opts,
    );

    return {
      waitUntilExit: instance.waitUntilExit,
    };
  }
}