desktop/scripts/tsc-plugins.tsx (134 lines of code) (raw):
/**
 * 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.
 *
 * @format
 */
/* eslint-disable flipper/no-console-error-without-context */
import fs from 'fs-extra';
import path from 'path';
import {exec} from 'child_process';
import {EOL} from 'os';
import pmap from 'p-map';
import {rootDir} from './paths';
import yargs from 'yargs';
import {isPluginJson} from 'flipper-common';
const argv = yargs
  .usage('yarn tsc-plugins [args]')
  .version(false)
  .options({
    dir: {
      description: 'Plugins directory name ("plugins" by default)',
      type: 'string',
      default: 'plugins',
      alias: 'd',
    },
  })
  .help()
  .parse(process.argv.slice(1));
const pluginsDir = path.join(rootDir, argv.dir);
const fbPluginsDir = path.join(pluginsDir, 'fb');
const publicPluginsDir = path.join(pluginsDir, 'public');
async function tscPlugins(): Promise<number> {
  const stdout = await new Promise<string | undefined>((resolve) =>
    exec(
      `./node_modules/.bin/tsc -p ./${argv.dir}/tsconfig.json`,
      {
        cwd: rootDir,
      },
      (err, stdout) => {
        if (err) {
          console.error(err);
          resolve(stdout);
        } else {
          resolve(undefined);
        }
      },
    ),
  );
  if (stdout) {
    console.error(stdout);
  }
  const errors = (stdout?.split(EOL) ?? []).filter((l) => l !== '');
  if (errors.length > 0) {
    await findAffectedPlugins(errors);
  }
  return stdout ? 1 : 0;
}
async function findAffectedPlugins(errors: string[]) {
  const [publicPackages, fbPackages] = await Promise.all([
    fs.readdir(publicPluginsDir),
    fs.readdir(fbPluginsDir).catch(() => [] as string[]),
  ]);
  const allPackages = await pmap(
    [
      ...publicPackages.map((p) => path.join(publicPluginsDir, p)),
      ...fbPackages.map((p) => path.join(fbPluginsDir, p)),
    ],
    async (p) => ({
      dir: p,
      json: await fs
        .readJson(path.join(p, 'package.json'))
        .catch(() => undefined),
    }),
  ).then((dirs) => dirs.filter((dir) => !!dir.json));
  const packageByName = new Map(
    allPackages.map((p) => [p.json.name as string, p]),
  );
  const depsByName = new Map<string, Set<string>>();
  function getDependencies(name: string): Set<string> {
    if (!depsByName.has(name)) {
      const set = new Set<string>();
      const pkg = packageByName.get(name)!;
      set.add(name);
      const allDeps = Object.keys({
        ...(pkg.json.dependencies ?? {}),
        ...(pkg.json.peerDependencies ?? {}),
      });
      for (const dep of allDeps) {
        if (packageByName.get(dep)) {
          const subDeps = getDependencies(dep);
          for (const subDep of subDeps) {
            set.add(subDep);
          }
        }
      }
      depsByName.set(name, set);
    }
    return depsByName.get(name)!;
  }
  for (const name of packageByName.keys()) {
    depsByName.set(name, getDependencies(name));
  }
  for (const pkg of allPackages) {
    if (!isPluginJson(pkg.json)) {
      continue;
    }
    const logFile = path.join(pkg.dir, 'tsc-error.log');
    await fs.remove(logFile);
    let logStream: fs.WriteStream | undefined;
    for (const dep of depsByName.get(pkg.json.name)!) {
      const relativeDir = path.relative(rootDir, packageByName.get(dep)!.dir);
      for (const error of errors) {
        if (error.startsWith(relativeDir)) {
          if (!logStream) {
            logStream = fs.createWriteStream(logFile);
            console.error(
              `Plugin ${path.relative(
                rootDir,
                pkg.dir,
              )} has tsc errors. Check ${path.relative(
                rootDir,
                logFile,
              )} for details.`,
            );
          }
          logStream.write(error);
          logStream.write(EOL);
        }
      }
    }
    logStream?.close();
  }
}
tscPlugins()
  .then((code) => {
    process.exit(code);
  })
  .catch((err: any) => {
    console.error(err);
    process.exit(1);
  });