projenrc/abstract/pdk-project.ts (237 lines of code) (raw):

/*! Copyright [Amazon.com](http://amazon.com/), Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ import { PublishConfig as _PublishConfig } from "@pnpm/types"; import { Project, SampleDir, TextFile, TextFileOptions } from "projen"; import { JsiiProject, JsiiProjectOptions, Stability } from "projen/lib/cdk"; import { NodePackageManager } from "projen/lib/javascript"; import { NodePackageUtils } from "../../packages/monorepo/src"; import { NxProject } from "../../packages/monorepo/src/components/nx-project"; import type { Nx } from "../../packages/monorepo/src/nx-types"; import { PDKInternalProjectUtils } from "../projects/internal/internal-project"; export const PDK_NAMESPACE = "@aws/"; const AWS_PDK = "pdk"; const README = "README.md"; export type PublishConfig = _PublishConfig & { access?: undefined | "restricted" | "public"; }; /** * Configuration options for the PDK Project. */ export interface PDKProjectOptions extends JsiiProjectOptions { /** * Nx project configuration. * * @see https://nx.dev/reference/project-configuration */ readonly nx?: Nx.ProjectConfig; /** * PublishConfig. * * @see https://pnpm.io/package_json#publishconfig */ readonly publishConfig?: PublishConfig; } /** * Project type to be used when creating new Constructs. * * This project handles correct naming for the PDK, along with validation and auto publishing of artifacts to the various package managers. */ export abstract class PDKProject extends JsiiProject { public readonly options: PDKProjectOptions; public readonly nx: NxProject; constructor(options: PDKProjectOptions) { const name = `${PDK_NAMESPACE}${options.name}`; super({ ...options, packageManager: NodePackageManager.PNPM, projenCommand: NodePackageUtils.command.projen(NodePackageManager.PNPM), stability: options.stability || Stability.EXPERIMENTAL, github: false, depsUpgrade: false, sampleCode: false, docgen: false, prettier: true, releaseToNpm: options.releaseToNpm ?? false, projenDevDependency: false, jsiiVersion: "*", srcdir: "src", testdir: "test", name, packageName: name, outdir: `packages/${options.name}`, gitignore: [...(options.gitignore || []), "LICENSE_THIRD_PARTY"], disableTsconfigDev: false, disableTsconfig: true, publishToPypi: { distName: "aws_pdk", module: "aws_pdk", }, publishToMaven: { mavenEndpoint: "https://aws.oss.sonatype.org", mavenGroupId: "software.aws", mavenArtifactId: "pdk", javaPackage: "software.aws.pdk", }, }); this.preCompileTask.prependExec("rm -f tsconfig.json"); this.postCompileTask.prependExec("rm -f tsconfig.json"); this.packageTask.reset(); this.options = options; if ( options.stability && !Object.values(Stability).find((f) => f === options.stability) ) { throw new Error(`stability must be one of: ${Object.values(Stability)}`); } if (this.deps.all.find((dep) => AWS_PDK === dep.name)) { throw new Error("PDK Projects cannot have a dependency on the @aws/pdk!"); } if (!this.parent) { throw new Error("parent must be provided!"); } if (options.sampleCode !== false) { new SampleDir(this, this.srcdir, { files: { "index.ts": "// export * from 'my-construct';", }, }); new SampleDir(this, this.testdir, { files: { ".gitkeep": "// Delete me once tests are added", }, }); } if (options.eslint !== false) { this.eslint?.addIgnorePattern("scripts/**/*.ts"); const eslintTask = this.tasks.tryFind("eslint"); eslintTask?.reset( `eslint --ext .ts,.tsx \${CI:-'--fix'} --no-error-on-unmatched-pattern ${this.srcdir} ${this.testdir}`, { receiveArgs: true } ); eslintTask && this.testTask.spawn(eslintTask); this.addTask("eslint-staged", { description: "Run eslint against the staged files only", steps: [ { exec: "eslint --fix --no-error-on-unmatched-pattern $(git diff --name-only --relative --staged HEAD . | grep -E '.(ts|tsx)$' | grep -v 'samples/*' | xargs)", }, ], }); this.packageTask.spawn(eslintTask!); } if (options.jest !== false) { const jestTask = this.jest && this.addTask("jest", { exec: [ "jest", "--passWithNoTests", // Only update snapshot locally "${CI:-'--updateSnapshot'}", // Always run in band for nx runner (nx run-many) "${NX_WORKSPACE_ROOT:+'--runInBand'}", ].join(" "), receiveArgs: true, }); this.testTask.reset(); jestTask && this.testTask.spawn(jestTask); // Most PDK tests rely on projen's test behaviour of synthing to a temporary directory // See: https://github.com/projen/projen/issues/2947 this.testTask.env("PROJEN_SELF_TEST", "true"); } if (options.releaseToNpm !== true) { this.tasks.tryFind("package-all")?.reset(); } if (!!options.publishConfig) { this.package.addField("publishConfig", { access: "public", ...options.publishConfig, }); } this.nx = NxProject.ensure(this); options.nx && this.nx.merge(options.nx); options.docgen !== false && new PDKDocgen(this); // Suppress JSII upgrade warnings this.tasks.addEnvironment("JSII_SUPPRESS_UPGRADE_PROMPT", "true"); this.generateReadme(options.name); } private generateReadme(name: string) { new Readme(this, { lines: this.generateReadmeLines(name), readonly: true, }); } private generateReadmeLines(name: string) { if (name === "pdk") { return [ "# AWS PDK", "", "All documentation is located at: https://aws.github.io/aws-pdk", ].concat( this.parent?.subprojects .filter((s) => !PDKInternalProjectUtils.isInternal(s)) .filter((s) => s.name !== `${PDK_NAMESPACE}${name}`) .sort((s1, s2) => s1.name.localeCompare(s2.name)) .map((s) => [""].concat((s.tryFindFile(README) as Readme)._lines!)) .flat() as string[] ); } else { return [ `# ${name}`, "", `Please refer to [Developer Guide](./docs/developer_guides/${name}/index.md).`, ]; } } } class Readme extends TextFile { _lines: string[]; constructor(project: Project, options: TextFileOptions) { super(project, README, options); this._lines = options.lines!; } addLine(line: string): void { this._lines?.push(line); super.addLine(line); } } /** * Utility to override nested object path value - performed in-place on the object. * @param obj Object to override path value * @param path Path of value to override * @param value Value to override * @param {boolean} [append=false] Indicates if array values are appended to, rather than overwritten. */ export function overrideField( obj: any, path: string, value: any, append?: boolean ): void { const parts = path.split("."); let curr = obj; while (parts.length > 1) { const key = parts.shift() as string; // if we can't recurse further or the previous value is not an // object overwrite it with an object. const isObject = curr[key] != null && typeof curr[key] === "object" && !Array.isArray(curr[key]); if (!isObject) { curr[key] = {}; } curr = curr[key]; } const lastKey = parts.shift() as string; if ( append && curr[lastKey] != null && Array.isArray(value) && Array.isArray(curr[lastKey]) ) { curr[lastKey] = [...curr[lastKey], ...value]; } else { curr[lastKey] = value; } } export class PDKDocgen { constructor(project: PDKProject) { project.addDevDeps("jsii-docgen"); const docsBasePath = "docs/api"; const docgen = project.addTask("docgen", { description: "Generate API docs from .jsii manifest", exec: `mkdir -p ${docsBasePath}/typescript && jsii-docgen -r=false -o ${docsBasePath}/typescript/index.md && sed -i'' -e 's/@aws\\//@aws\\/pdk\\//g' ${docsBasePath}/typescript/index.md`, }); docgen.exec( `mkdir -p ${docsBasePath}/python && jsii-docgen -l python -r=false -o ${docsBasePath}/python/index.md && sed -i'' -e 's/aws.pdk/aws.pdk.${this.toSnakeCase( project )}/g' ${docsBasePath}/python/index.md` ); docgen.exec( `mkdir -p ${docsBasePath}/java && jsii-docgen -l java -r=false -o ${docsBasePath}/java/index.md && sed -i'' -e 's/software.aws.pdk/software.aws.pdk.${this.toSnakeCase( project )}/g' ${docsBasePath}/java/index.md` ); NxProject.of(project)?.addBuildTargetFiles( [`!{projectRoot}/${docsBasePath}/**/*`], [`{projectRoot}/${docsBasePath}`] ); // spawn docgen after compilation (requires the .jsii manifest). project.postCompileTask.spawn(docgen); project.gitignore.exclude(`/${docsBasePath}`); project.annotateGenerated(`/${docsBasePath}`); } private toSnakeCase(project: PDKProject) { return project.name .split("/") .reverse()[0] .toLowerCase() .replace(/-/g, "_"); } }