packages/purplet/src/build/phase1.ts
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;
}