packages/jsii-pacmak/lib/target.ts (106 lines of code) (raw):

import * as spec from '@jsii/spec'; import * as fs from 'fs-extra'; import * as reflect from 'jsii-reflect'; import { RosettaTabletReader } from 'jsii-rosetta'; import * as path from 'path'; import * as spdx from 'spdx-license-list/full'; import { traverseDependencyGraph } from './dependency-graph'; import { IGenerator } from './generator'; import * as logging from './logging'; export abstract class Target { protected readonly packageDir: string; protected readonly fingerprint: boolean; protected readonly force: boolean; protected readonly arguments: { [name: string]: any }; protected readonly targetName: string; protected readonly assembly: reflect.Assembly; protected readonly rosetta: RosettaTabletReader; protected readonly runtimeTypeChecking: boolean; protected abstract readonly generator: IGenerator; public constructor(options: TargetOptions) { this.arguments = options.arguments; this.assembly = options.assembly; this.fingerprint = options.fingerprint ?? true; this.force = options.force ?? false; this.packageDir = options.packageDir; this.rosetta = options.rosetta; this.runtimeTypeChecking = options.runtimeTypeChecking; this.targetName = options.targetName; } /** * Emits code artifacts. * * @param outDir the directory where the generated source will be placed. */ public async generateCode(outDir: string, tarball: string): Promise<void> { await this.generator.load(this.packageDir, this.assembly); if (this.force || !(await this.generator.upToDate(outDir))) { this.generator.generate(this.fingerprint); const licenseFile = path.join(this.packageDir, 'LICENSE'); const license = (await fs.pathExists(licenseFile)) ? await fs.readFile(licenseFile, 'utf8') : spdx[this.assembly.license]?.licenseText; const noticeFile = path.join(this.packageDir, 'NOTICE'); const notice = (await fs.pathExists(noticeFile)) ? await fs.readFile(noticeFile, 'utf8') : undefined; await this.generator.save(outDir, tarball, { license, notice }); } else { logging.info( `Generated code for ${this.targetName} was already up-to-date in ${outDir} (use --force to re-generate)`, ); } } /** * Builds the generated code. * * @param sourceDir the directory where the generated source was put. * @param outDir the directory where the build artifacts will be placed. */ public abstract build(sourceDir: string, outDir: string): Promise<void>; /** * A utility to copy files from one directory to another. * * @param sourceDir the directory to copy from. * @param targetDir the directory to copy into. */ protected async copyFiles(sourceDir: string, targetDir: string) { // Preemptively create target directory, to avoid unsafely racing on it's creation. await fs.mkdirp(targetDir); await fs.copy(sourceDir, targetDir, { recursive: true }); } /** * Traverses the dep graph and returns a list of pacmak output directories * available locally for this specific target. This allows target builds to * take local dependencies in case a dependency is checked-out. * * @param packageDir The directory of the package to resolve from. */ protected async findLocalDepsOutput(rootPackageDir: string) { return findLocalBuildDirs(rootPackageDir, this.targetName); } } /** * Traverses the dep graph and returns a list of pacmak output directories * available locally for this specific target. This allows target builds to * take local dependencies in case a dependency is checked-out. * * @param packageDir The directory of the package to resolve from. */ export async function findLocalBuildDirs( rootPackageDir: string, targetName: string, ) { const results = new Set<string>(); await traverseDependencyGraph(rootPackageDir, processPackage); return Array.from(results); async function processPackage( packageDir: string, pkg: any, isRoot: boolean, ): Promise<boolean> { // no jsii or jsii.outdir - either a misconfigured jsii package or a non-jsii dependency. either way, we are done here. if (!pkg.jsii || !pkg.jsii.outdir) { return false; } if (isRoot) { // This is the root package - no need to register it's outdir return true; } // if an output directory exists for this module, then we add it to our // list of results (unless it's the root package, which we are currently building) const outdir = path.join(packageDir, pkg.jsii.outdir, targetName); if (await fs.pathExists(outdir)) { logging.debug(`Found ${outdir} as a local dependency output`); results.add(outdir); } return true; } } export interface TargetConstructor { /** * Provides information about an assembly in the usual package repositories for the target. This includes information * necessary to locate the package in the repositories (a URL to the repository's public endpoint), as well as usage * instructions for the various configruation files (e.g: Maven POM, Gemfile, ...) and/or installation instructions * using the standard command line tools (npm, yarn, ...). * * @param assm the assembly for which coodinates are requested. * * @return Information about the assembly in the various package managers supported for a given language. The return * value is a hash, as some packages can be used across different languages (typescript & javascript, java & * scala & clojure & kotlin...). */ toPackageInfos?: (assm: spec.Assembly) => { [language: string]: PackageInfo }; /** * Provides the native way to reference a Type, for example a Java import statement, or a Javscript require directive. * Particularly useful when generating documentation. * * @param type the JSII type for which a native reference is requested. * @param options the target-specific options provided. * * @return the native reference for the target for each supported language (there can be multiple languages * supported by a given target: typescript & javascript, java & scala & clojure & kotlin, ...) */ toNativeReference?: ( type: spec.Type, options: any, ) => { [language: string]: string | undefined }; new (options: TargetOptions): Target; } /** * Information about a package */ export interface PackageInfo { /** The name by which the package repository is known */ repository: string; /** The URL to the package within it's repository */ url: string; /** * Configuration fragments or installation instructions, by client scenario (e.g: maven + gradle). Values can be a * plain string (documentation should render as a pre-formatted block of text using monospace font), or an object * describing a language-tagged block of code. * * @example { * maven: { * language: 'xml', * code: '<dependency><groupId>grp</groupId><artifactId>art</artifactId><version>version</version></dependency>' * }, * gradle: "compile 'grp:art:version'", * } * * @example { * npm: { language: 'console', code: '$ npm install pkg' }, * yarn: { language: 'console', code: '$ yarn add pkg' }, * 'package.json': { language: json, code: '{"pkg": "^version" }' } * } */ usage: { [label: string]: string | { language: string; code: string } }; } export interface TargetOptions { /** The name of the target language we are generating */ targetName: string; /** The directory where the JSII package is located */ packageDir: string; /** The JSII-reflect assembly for this JSII assembly */ assembly: reflect.Assembly; /** The Rosetta instance */ rosetta: RosettaTabletReader; /** Whether to generate runtime type-checking code */ runtimeTypeChecking: boolean; /** * Whether to fingerprint the produced artifacts. * @default true */ fingerprint?: boolean; /** * Whether artifacts should be re-build even if their fingerprints look up-to-date. * @default false */ force?: boolean; /** * Arguments provided by the user (how they are used is target-dependent) */ arguments: { [name: string]: any }; }