CRBT-Team/Purplet

View on GitHub
packages/purplet/src/build/phase1.ts

Summary

Maintainability
A
0 mins
Test Coverage
import path from 'path';
import externalPlugin from 'rollup-plugin-all-external';
import { Logger } from '@paperdave/logger';
import { asyncIterToArray, walk } from '@paperdave/utils';
import { spawnSync } from 'child_process';
import { writeFile } from 'fs/promises';
import { Plugin, rollup, VERSION as ROLLUP_VERSION } from 'rollup';
import type { ResolvedConfig } from '../config/types';
import type { FeatureScan } from '../utils/build-phase-1';
import { isSourceFile } from '../utils/filetypes';
import { purpletSourceCode } from '../utils/fs';

export interface Phase1Options {
  config: ResolvedConfig;
  sharedRollupPlugins: Plugin[];
}

/** Phase 1 is building a map of the bot features. */
export async function buildPhase1({ config, sharedRollupPlugins }: Phase1Options) {
  const log = new Logger('build:phase1', { debug: true });

  const modulePaths = (await asyncIterToArray(walk(config.paths.features))).filter(isSourceFile);

  const modules = modulePaths.map((filename, index) => ({
    id: `${path
      .relative(config.paths.features, filename)
      .replace(/\.[jt]sx?$/, '')
      .replace(/[^a-z0-9]/g, '_')
      .replace(/^_+|(_)_+|_+$/g, '$1')}_${index}`,
    relative: path.relative(config.paths.features, filename),
    filename,
  }));

  log('found %d modules:', modules.length);
  for (const { relative, id } of modules) {
    log(`  ${relative} -> ${id}`);
  }

  const phase1source = [
    '// Phase 1 file generated by Purplet v__VERSION__',
    `import { printPhase1Data, moduleToFeatureArray } from 'purplet/internal';`,
    ``,
    ...modules.map(
      ({ id, filename }) => `import * as ${id} from '${filename.replace(/[\\']/g, '\\$&')}';`
    ),
    ``,
    `printPhase1Data([`,
    ...modules.map(
      ({ id, relative }) =>
        `  moduleToFeatureArray('${relative.replace(/[\\']/g, '\\$&')}', ${id}),`
    ),
    `].flat());`,
    ``,
  ].join('\n');

  const phase1File = path.join(config.temp, 'phase1.ts');
  const phase1OutFile = path.join(config.temp, 'phase1.mjs');
  await writeFile(phase1File, phase1source);

  log('running rollup build');
  const phase1Rollup = await rollup({
    input: phase1File,
    external: id => id.startsWith(purpletSourceCode),
    plugins: [...sharedRollupPlugins, externalPlugin()],
  });
  await phase1Rollup.write({
    file: phase1OutFile,
    format: 'esm',
    banner: `// Phase 1 file generated by Purplet v__VERSION__ and Rollup ${ROLLUP_VERSION}`,
  });

  log('executing ' + phase1OutFile);

  // We use a separate process so we can ensure if a database connection or something is attempted,
  // it is killed off as soon as possible.
  const {
    status: phase1Status,
    stdout: phase1OutputText,
    error: phase1Error,
  } = spawnSync(process.argv[0], [phase1OutFile], {
    stdio: 'pipe',
  });

  if (phase1Status !== 0 || phase1Error) {
    Logger.error(phase1OutputText.toString());
    throw new Error('Scan of bot features failed');
  }

  return JSON.parse(phase1OutputText.toString()) as FeatureScan;
}