Skip to content

Commit

Permalink
chore(aws-cdk-lib): extract experimental L2s from aws-cdk-lib (#14135)
Browse files Browse the repository at this point in the history
This PR adds logic to extract the L2 of experimental modules from `aws-cdk-lib`.  The reason for keeping the L1s of experimental modules is twofold: 

1. Removing the L1 means that modules with an experimental L2 are in a disadvantage in comparison to modules that does not have an L2. 
2. `CfnInclude` requires the L1.

The main logic was added in `ubergen`. If the monolithic package includes the key `ubergen.stripExperimental` in its `package.json` file, experimental modules source files will not be copied, instead we  execute `cfn2ts` to regenerate the L1.

#### Testing

1. Added a post-package script -`verify-stripped-exp.js`, to `aws-cdk-lib`. The script will install the tarball and verify that only L1 files are includes in experimental modules. 

2. verified that the generated tarball can be used with a consumer application, and that experimental modules are working properly.

adding `do-not-merege` as some stable modules still depends on experimental modules, the work to remove these dependencies is in progress.

#### Unrelated callout

We found a bug in a util function copied from `ubergen`, this PR adds a fix to both code path, but clearly this copy-pasta is not a great mechanism. This kind of copied util scripts is prevalent across our repo and we don't yet have a mechnisam to share sure common runtime functionally between modules. Not sure what is the right solution here but wanted to call it out, in case someone has a fix/methodology I have missed.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
NetaNir authored Apr 19, 2021
1 parent 21a52c6 commit f8b6445
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 12 deletions.
6 changes: 5 additions & 1 deletion packages/aws-cdk-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
},
"stripDeprecated": true
},
"cdk-package": {
"post": "node ./scripts/verify-stripped-exp.js"
},
"pkglint": {
"exclude": [
"package-info/maturity",
Expand Down Expand Up @@ -313,6 +316,7 @@
"announce": false
},
"ubergen": {
"exclude": true
"exclude": true,
"excludeExperimentalModules": true
}
}
158 changes: 158 additions & 0 deletions packages/aws-cdk-lib/scripts/verify-stripped-exp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// +------------------------------------------------------------------------------------------------
// | this script is executed post packaging to verify that experimental modules in aws-cdk-lib includes **only** L1 autogenerated files.
// | The purpose is to avoid publishing L2 of experimental modules with aws-cdk-lib
// |
import { spawnSync } from 'child_process';
import * as console from 'console';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs-extra';

async function main(tempDir: string) {
console.log('🧐 Verifying all experimental modules includes only L1s files...');
const cwd = process.cwd();
const awsCdkModulesRepoPath = path.join(findWorkspacePath(), 'packages', '@aws-cdk');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const version = require('./../package.json').version;
const tarFullPath = path.join(cwd, 'dist', 'js', `aws-cdk-lib@${version}.jsii.tgz`);

const invalidCfnModules = new Map<string, Array<String>>();
const invalidModules = new Array<string>();

// install the tarball in a temp directory
console.log(`installing aws-cdk-lib from dist/js into ${tempDir}`);
exec('npm', ['install', '--prefix', tempDir, tarFullPath]);
const installedAwsCdkLibPath = path.join(tempDir, 'node_modules', 'aws-cdk-lib', 'lib');

for (const module of fs.readdirSync(awsCdkModulesRepoPath)) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pkgJson = require(path.join(awsCdkModulesRepoPath, module, 'package.json'));
if (pkgJson.stability !== 'experimental') {
continue;
}
if (pkgJson['cdk-build'].cloudformation) {
// if a cfn module, verify only the allowed files exists
const files = await listAllFiles(path.join(installedAwsCdkLibPath, module));
const invalidFiles = new Array();
files.forEach(file => {
if (!isAllowedFile(file)) {
invalidFiles.push(file);
}
});
if (invalidFiles.length > 0) {
invalidCfnModules.set(module, invalidFiles);
}
} else {
// not a cfn module, verify it was entirely removed
if (fs.existsSync(path.join(installedAwsCdkLibPath, module))) {
invalidModules.push(module);
}
}
}

if (invalidCfnModules.size > 0 || invalidModules.length > 0) {
if (invalidCfnModules.size > 0 ) {
console.log('cfn module with invalid files:');
for (let [module, files] of invalidCfnModules.entries()) {
console.log(`${module}:`);
files.forEach(file => console.log(`\t ${file}`));
}
}
console.log('---------------------------------------------');
if (invalidModules.length > 0) {
console.log('non-cfn experimental modules:');
invalidModules.forEach(m => console.log(`\t ${m}`));
}
throw new Error('Verification Error');
}
}

const tempDir = fs.mkdtempSync(os.tmpdir());

main(tempDir).then(
() => {
fs.removeSync(tempDir);
console.log('✅ All experimental modules includes only L1s files!');
process.exit(0);
},
(err) => {
process.stderr.write(`${err}\n`);
process.stderr.write(`❌ Verification failed, Some experimental modules includes non L1 files, see details above. Inspect working directory: '${tempDir}'`);
process.exit(1);
},
);


/**
* Spawn sync with error handling
*/
function exec(cmd: string, args: string[]) {
const proc = spawnSync(cmd, args);

if (proc.error) {
throw proc.error;
}

if (proc.status !== 0) {
if (proc.stdout || proc.stderr) {
throw new Error(`${cmd} exited with status ${proc.status}; stdout: ${proc.stdout?.toString().trim()}\n\n\nstderr: ${proc.stderr?.toString().trim()}`);
}
throw new Error(`${cmd} exited with status ${proc.status}`);
}

return proc;
}

const GENERATED_SUFFIX_REGEX = new RegExp(/generated\.(js|d\.ts)$/);
const ALLOWED_FILES = ['.jsiirc.json', 'index.ts', 'index.js', 'index.d.ts'];

/**
* Recursively collect all files in dir
*/
async function listAllFiles(dir: string) {
const ret = new Array();

async function recurse(part: string) {
const files = await fs.readdir(part);
for (const file of files) {
const fullPath = path.join(part, file);
if ((await fs.stat(fullPath)).isDirectory()) {
await recurse(fullPath);
} else {
ret.push(file);
}
}
}
await recurse(dir);
return ret;
}

/**
* Find the workspace root path. Walk up the directory tree until you find lerna.json
*/
function findWorkspacePath() {

return _findRootPath(process.cwd());

function _findRootPath(part: string): string {
if (part === path.resolve(part, '..')) {
throw new Error('couldn\'t find a \'lerna.json\' file when walking up the directory tree, are you in a aws-cdk project?');
}

if (fs.existsSync(path.resolve(part, 'lerna.json'))) {
return part;
}
return _findRootPath(path.resolve(part, '..'));
}
}

/**
* @param file
* @returns true if the file allowed in an L1 only modules, otherwise false
*/
function isAllowedFile(file: string) {
if (GENERATED_SUFFIX_REGEX.test(file)) {
return true;
}
return ALLOWED_FILES.includes(file);
}
18 changes: 12 additions & 6 deletions tools/cdk-build-tools/bin/cdk-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from 'fs-extra';
import * as yargs from 'yargs';
import * as yarnCling from 'yarn-cling';
import { shell } from '../lib/os';
import { cdkPackageOptions, isJsii, isPrivate } from '../lib/package-info';
import { Timers } from '../lib/timer';

const timers = new Timers();
Expand All @@ -22,26 +23,26 @@ async function main() {
})
.argv;

// if this is a jsii package, use jsii-packmak
const options = cdkPackageOptions();

const outdir = 'dist';
const pkg = await fs.readJson('package.json');

// if this is a private module, don't package
if (pkg.private) {
if (isPrivate()) {
process.stdout.write('No packaging for private modules.\n');
return;
}

// If we need to shrinkwrap this, do so now.
const packageOptions = pkg['cdk-package'] ?? {};
if (packageOptions.shrinkWrap) {
if (options.shrinkWrap) {
await yarnCling.generateShrinkwrap({
packageJsonFile: 'package.json',
outputFile: 'npm-shrinkwrap.json',
});
}

if (pkg.jsii) {
// if this is a jsii package, use jsii-packmak
if (isJsii()) {
const command = [args['jsii-pacmak'],
args.verbose ? '-vvv' : '-v',
...args.targets ? flatMap(args.targets, (target: string) => ['-t', target]) : [],
Expand All @@ -55,8 +56,13 @@ async function main() {
await fs.mkdirp(target);
await fs.move(tarball, path.join(target, path.basename(tarball)));
}

if (options.post) {
await shell(options.post, { timers });
}
}


main().then(() => {
buildTimer.end();
process.stdout.write(`Package complete. ${timers.display()}\n`);
Expand Down
26 changes: 26 additions & 0 deletions tools/cdk-build-tools/lib/package-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,27 @@ export function cdkBuildOptions(): CDKBuildOptions {
return currentPackageJson()['cdk-build'] || {};
}

/**
* Return the cdk-package options
*/
export function cdkPackageOptions(): CDKPackageOptions {
return currentPackageJson()['cdk-package'] || {};
}

/**
* Whether this is a jsii package
*/
export function isJsii(): boolean {
return currentPackageJson().jsii !== undefined;
}

/**
* Whether this is a private package
*/
export function isPrivate(): boolean {
return currentPackageJson().private !== undefined;
}

export interface File {
filename: string;
path: string;
Expand Down Expand Up @@ -160,6 +174,18 @@ export interface CDKBuildOptions {
stripDeprecated?: boolean;
}

export interface CDKPackageOptions {
/**
* Should this package be shrinkwrap
*/
shrinkWrap?: boolean;

/*
* An optional command (formatted as a list of strings) to run after packaging
*/
post?: string[];
}

/**
* Return a full path to the config file in this package
*
Expand Down
42 changes: 38 additions & 4 deletions tools/ubergen/bin/ubergen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as console from 'console';
import * as os from 'os';
import * as path from 'path';
import * as process from 'process';
import cfn2ts from 'cfn2ts';
import * as fs from 'fs-extra';
import * as ts from 'typescript';

Expand Down Expand Up @@ -59,9 +59,14 @@ interface PackageJson {
readonly name: string;
readonly types: string;
readonly version: string;
readonly stability: string;
readonly [key: string]: unknown;
readonly 'cdk-build': {
readonly cloudformation: string[] | string;
};
readonly ubergen?: {
readonly deprecatedPackages?: readonly string[];
readonly excludeExperimentalModules?: boolean;
};
}

Expand All @@ -73,7 +78,7 @@ function findWorkspacePath(): string {
return _findRootPath(process.cwd());

function _findRootPath(part: string): string {
if (process.cwd() === os.homedir()) {
if (part === path.resolve(part, '..')) {
throw new Error('couldn\'t find a \'lerna.json\' file when walking up the directory tree, are you in a aws-cdk project?');
}

Expand Down Expand Up @@ -214,13 +219,20 @@ async function verifyDependencies(packageJson: any, libraries: readonly LibraryR
async function prepareSourceFiles(libraries: readonly LibraryReference[], packageJson: PackageJson) {
console.log('📝 Preparing source files...');

if (packageJson.ubergen?.excludeExperimentalModules) {
console.log('\t 👩🏻‍🔬 \'excludeExperimentalModules\' enabled. Regenerating all experimental modules as L1s using cfn2ts...');
}

await fs.remove(LIB_ROOT);

const indexStatements = new Array<string>();
for (const library of libraries) {
const libDir = path.join(LIB_ROOT, library.shortName);
await transformPackage(library, packageJson, libDir, libraries);
const copied = await transformPackage(library, packageJson, libDir, libraries);

if (!copied) {
continue;
}
if (library.shortName === 'core') {
indexStatements.push(`export * from './${library.shortName}';`);
} else {
Expand Down Expand Up @@ -260,7 +272,28 @@ async function transformPackage(
) {
await fs.mkdirp(destination);

await copyOrTransformFiles(library.root, destination, allLibraries, uberPackageJson);
if (uberPackageJson.ubergen?.excludeExperimentalModules && library.packageJson.stability === 'experimental') {
// when stripExperimental is enabled, we only want to add the L1s of experimental modules.
let cfnScopes = library.packageJson['cdk-build'].cloudformation;

if (cfnScopes === undefined) {
return false;
}
cfnScopes = Array.isArray(cfnScopes) ? cfnScopes : [cfnScopes];

const destinationLib = path.join(destination, 'lib');
await fs.mkdirp(destinationLib);
await cfn2ts(cfnScopes, destinationLib);
// create a lib/index.ts which only exports the generated files
fs.writeFileSync(path.join(destinationLib, 'index.ts'),
/// logic copied from `create-missing-libraries.ts`
cfnScopes.map(s => (s === 'AWS::Serverless' ? 'AWS::SAM' : s).split('::')[1].toLocaleLowerCase())
.map(s => `export * from './${s}.generated';`)
.join('\n'));
await copyOrTransformFiles(destination, destination, allLibraries, uberPackageJson);
} else {
await copyOrTransformFiles(library.root, destination, allLibraries, uberPackageJson);
}

await fs.writeFile(
path.join(destination, 'index.ts'),
Expand All @@ -284,6 +317,7 @@ async function transformPackage(
{ encoding: 'utf8' },
);
}
return true;
}

function transformTargets(monoConfig: PackageJson['jsii']['targets'], targets: PackageJson['jsii']['targets']): PackageJson['jsii']['targets'] {
Expand Down
3 changes: 2 additions & 1 deletion tools/ubergen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
},
"dependencies": {
"fs-extra": "^9.1.0",
"typescript": "~3.9.9"
"typescript": "~3.9.9",
"cfn2ts": "0.0.0"
},
"keywords": [
"aws",
Expand Down

0 comments on commit f8b6445

Please sign in to comment.