packages/jsii-pacmak/lib/index.ts (243 lines of code) (raw):

import { TypeSystem } from 'jsii-reflect'; import { RosettaTabletReader, UnknownSnippetMode } from 'jsii-rosetta'; import { resolve } from 'path'; import { cwd } from 'process'; import * as logging from './logging'; import { findJsiiModules, updateAllNpmIgnores } from './npm-modules'; import { JsiiModule } from './packaging'; import { ALL_BUILDERS, TargetName } from './targets'; import { Timers } from './timer'; import { Toposorted } from './toposort'; import { flatten } from './util'; //#region Exported APIs export { TargetName }; export { configure as configureLogging } from './logging'; /** * Generates code in the desired targets. */ export async function pacmak({ argv = {}, clean = true, codeOnly = false, fingerprint = true, force = false, forceSubdirectory = true, forceTarget = false, inputDirectories, outputDirectory, parallel = true, recurse = false, rosettaTablet, rosettaUnknownSnippets = undefined, runtimeTypeChecking = true, targets = Object.values(TargetName), timers = new Timers(), updateNpmIgnoreFiles = false, validateAssemblies = false, }: PacmakOptions): Promise<void> { const rosetta = new RosettaTabletReader({ unknownSnippets: rosettaUnknownSnippets, prefixDisclaimer: true, }); if (rosettaTablet) { await rosetta.loadTabletFromFile(rosettaTablet); } const modulesToPackageSorted = await findJsiiModules( inputDirectories, recurse, ); const modulesToPackageFlat = flatten(modulesToPackageSorted); logging.info(`Found ${modulesToPackageFlat.length} modules to package`); if (modulesToPackageFlat.length === 0) { logging.warn('Nothing to do'); return; } if (outputDirectory) { // Ensure this is consistently interpreted as relative to cwd(). This is transparent for absolute // paths, as those would be returned unmodified. const absoluteOutputDirectory = resolve(cwd(), outputDirectory); for (const mod of modulesToPackageFlat) { mod.outputDirectory = absoluteOutputDirectory; } } else if (updateNpmIgnoreFiles) { // if outdir is coming from package.json, verify it is excluded by .npmignore. if it is explicitly // defined via --out, don't perform this verification. await updateAllNpmIgnores(modulesToPackageFlat); } const packCommand = argv['pack-command']; await timers.recordAsync(packCommand, () => { logging.info('Packaging NPM bundles'); return Promise.all(modulesToPackageFlat.map((m) => m.npmPack(packCommand))); }); await timers.recordAsync('load jsii', () => { logging.info('Loading jsii assemblies and translations'); const system = new TypeSystem(); return Promise.all( modulesToPackageFlat.map(async (m) => { await m.load(system, validateAssemblies); return rosetta.addAssembly(m.assembly.spec, m.moduleDirectory); }), ); }); try { const targetSets = sliceTargets( modulesToPackageSorted, targets, forceTarget, ); if (targetSets.every((s) => s.modulesSorted.length === 0)) { throw new Error( `None of the requested packages had any targets to build for '${targets.join( ', ', )}' (use --force-target to force)`, ); } const perLanguageDirectory = targetSets.length > 1 || forceSubdirectory; // We run all target sets in parallel for minimal wall clock time await Promise.all( mapParallelOrSerial( targetSets, async (targetSet) => { logging.info( `Packaging '${targetSet.targetType}' for ${describePackages( targetSet, )}`, ); return timers .recordAsync(targetSet.targetType, () => buildTargetsForLanguage( targetSet.targetType, targetSet.modulesSorted, { argv, clean, codeOnly, fingerprint, force, perLanguageDirectory, rosetta, runtimeTypeChecking, }, ), ) .then( () => logging.info(`${targetSet.targetType} finished`), (err) => { logging.warn(`${targetSet.targetType} failed`); // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors return Promise.reject(err); }, ); }, { parallel }, ), ); } finally { if (clean) { logging.debug('Cleaning up'); await timers.recordAsync('cleanup', () => Promise.all(modulesToPackageFlat.map((m) => m.cleanup())), ); } else { logging.info('Temporary directories retained (--no-clean)'); } } logging.info(`Packaged. ${timers.display()}`); } /** * Options provided to the `pacmak` function. */ export interface PacmakOptions { /** * All command-line arguments that were provided. This includes target-specific parameters, the * handling of which is up to the code generators. * * @default {} */ readonly argv?: { readonly [name: string]: any }; /** * Whether to clean up temporary directories upon completion. * * @default true */ readonly clean?: boolean; /** * Whether to generate source code only (as opposed to built packages). * * @default false */ readonly codeOnly?: boolean; /** * Whether to opportunistically include a fingerprint in generated code, to avoid re-generating * code if the source assembly has not changed. * * @default true */ readonly fingerprint?: boolean; /** * Whether to always re-generate code, even if the fingerprint has not changed. * * @default false */ readonly force?: boolean; /** * Always emit code in a per-language subdirectory, even if there is only one target language. * * @default true */ readonly forceSubdirectory?: boolean; /** * Always try to generate code for the selected targets, even if those are not configured. Use this option at your own * risk, as there are significant chances code generators cannot operate without any configuration. * * @default false */ readonly forceTarget?: boolean; /** * The list of directories to be considered for input assemblies. */ readonly inputDirectories: readonly string[]; /** * The directory in which to output generated packages or code (if `codeOnly` is `true`). * * @default - Configured in `package.json` */ readonly outputDirectory?: string; /** * Whether to parallelize code generation. Turning this to `false` can be beneficial in certain resource-constrained * environments, such as free CI/CD offerings, as it reduces the pressure on IO. * * @default true */ readonly parallel?: boolean; /** * Whether to recursively generate for the selected packages' dependencies. * * @default false */ readonly recurse?: boolean; /** * How rosetta should treat snippets that cannot be loaded from a translation tablet. * * @default UnknownSnippetMode.VERBATIM */ readonly rosettaUnknownSnippets?: UnknownSnippetMode; /** * A Rosetta tablet file where translations for code examples can be found. * * @default undefined */ readonly rosettaTablet?: string; /** * Whether to inject runtime type checks in places where compile-time type checking is not performed. * * @default true */ readonly runtimeTypeChecking?: boolean; /** * The list of targets for which code should be generated. Unless `forceTarget` is `true`, a given target will only * be generated for assemblies that have configured it. * * @default Object.values(TargetName) */ readonly targets?: readonly TargetName[]; /** * A `Timers` object, if you are interested in including the rosetta run in a larger set of timed operations. */ readonly timers?: Timers; /** * Whether to update .npmignore files if `outputDirectory` comes from the `package.json` files. * * @default false */ readonly updateNpmIgnoreFiles?: boolean; /** * Whether assemblies should be validated or not. Validation can be expensive and can be skipped if the assemblies * can be assumed to be valid. * * @default false */ readonly validateAssemblies?: boolean; } //#endregion //#region Building async function buildTargetsForLanguage( targetLanguage: string, modules: Toposorted<JsiiModule>, { argv, clean, codeOnly, fingerprint, force, perLanguageDirectory, rosetta, runtimeTypeChecking, }: { argv: { readonly [name: string]: any }; clean: boolean; codeOnly: boolean; fingerprint: boolean; force: boolean; perLanguageDirectory: boolean; rosetta: RosettaTabletReader; runtimeTypeChecking: boolean; }, ): Promise<void> { // ``argv.target`` is guaranteed valid by ``yargs`` through the ``choices`` directive. const factory = ALL_BUILDERS[targetLanguage as TargetName]; if (!factory) { throw new Error(`Unsupported target: '${targetLanguage}'`); } return factory(modules, { arguments: argv, clean: clean, codeOnly: codeOnly, fingerprint: fingerprint, force: force, languageSubdirectory: perLanguageDirectory, rosetta, runtimeTypeChecking, }).buildModules(); } //#endregion //#region Target Slicing /** * A set of packages (targets) translated into the same language */ interface TargetSet { targetType: string; // Sorted into toposorted tranches modulesSorted: Toposorted<JsiiModule>; } function sliceTargets( modulesSorted: Toposorted<JsiiModule>, requestedTargets: readonly TargetName[], force: boolean, ): readonly TargetSet[] { const ret = new Array<TargetSet>(); for (const target of requestedTargets) { ret.push({ targetType: target, modulesSorted: modulesSorted .map((modules) => modules.filter((m) => force || m.availableTargets.includes(target)), ) .filter((ms) => ms.length > 0), }); } return ret; } //#endregion //#region Parallelization function mapParallelOrSerial<T, R>( collection: readonly T[], mapper: (item: T) => Promise<R>, { parallel }: { parallel: boolean }, ): Array<Promise<R>> { const result = new Array<Promise<R>>(); for (const item of collection) { result.push( result.length === 0 || parallel ? // Running parallel, or first element mapper(item) : // Wait for the previous promise, then make the next one result[result.length - 1].then( () => mapper(item), // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors (error) => Promise.reject(error), ), ); } return result; } //#endregion //#region Misc. Utilities function describePackages(target: TargetSet) { const modules = flatten(target.modulesSorted); if (modules.length > 0 && modules.length < 5) { return modules.map((m) => m.name).join(', '); } return `${modules.length} modules`; } //#endregion