Skip to content

chore: make all L2 Constructs property injectable during release #34328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion tools/@aws-cdk/construct-metadata-updater/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

This tool updates will parse the entire `aws-cdk` repository and does the following things:

1. `ConstructUpdater`: For any non-abstract L2 construct class, add `addConstructMetadata` method call to the constructor to track analytics usage and add necessary import statements if missing
1. `ConstructUpdater`:
- For any non-abstract L2 construct class, add `addConstructMetadata` method call to the constructor to track analytics usage and add necessary import statements if missing.
- Also make all non-abstract L2 Constructs Property Injectable.
It skips over Constructs that are already Property Injectable.
2. `PropertyUpdater`: Generate a JSON Blueprint file in `packages/aws-cdk-lib/core/lib/analytics-data-source/classes.ts` that contains all L2 construct class's props as well as public methods' props.
3. `EnumsUpdater`: Generate a JSON Blueprint file in `packages/aws-cdk-lib/core/lib/analytics-data-source/enums.ts` that gets all ENUMs type in `aws-cdk` repo.
4. `MethodsUpdater`: For any non-abstract L2 construct class, add `@MethodMetadata` decorator to public methods to track analytics usage and add necessary import statements if missing
Expand Down
171 changes: 133 additions & 38 deletions tools/@aws-cdk/construct-metadata-updater/lib/metadata-updater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClassDeclaration, IndentationText, Project, PropertyDeclaration, QuoteKind, SourceFile, Symbol, SyntaxKind } from "ts-morph";
import { ClassDeclaration, IndentationText, Project, PropertyDeclaration, QuoteKind, Scope, SourceFile, Symbol, SyntaxKind } from "ts-morph";
import * as path from "path";
import * as fs from "fs";
// import { exec } from "child_process";
Expand Down Expand Up @@ -152,64 +152,159 @@ export class ConstructsUpdater extends MetadataUpdater {
const classes = this.getCdkResourceClasses(sourceFile.getFilePath());
for (const resource of classes) {
this.addImportAndMetadataStatement(resource.sourceFile, resource.filePath, resource.node);
this.makeConstructsPropInjectable(resource.sourceFile, resource.filePath, resource.node);
}
});
}

/**
* This makes a Construct Property Injectable by doing 3 things:
* - add PROPERTY_INJECTION_ID property
* - import propertyInjectable from core/lib/prop-injectable
* - add class decorator @propertyInjectable
*
* If the Construct already has PROPERTY_INJECTION_ID, then skip it.
*/
private makeConstructsPropInjectable(sourceFile: SourceFile, filePath: string, node: ClassDeclaration) {
console.log(`path: ${filePath}, class: ${node.getName()}`);

if (this.isAlreadyInjectable(node)) {
return; // do nothing
}

// Add PROPERTY_INJECTION_ID
node.addProperty({
scope: Scope.Public,
isStatic: true,
isReadonly: true,
name: 'PROPERTY_INJECTION_ID',
type: "string",
initializer: this.filePathToInjectionId(filePath, node.getName()),
});
console.log(' Added PROPERTY_INJECTION_ID')

// Add Decorator
node.addDecorator({
name: "propertyInjectable",
});
console.log(' Added @propertyInjectable')

// import propertyInjectable
this.importCoreLibFile(sourceFile, filePath, 'prop-injectable', 'propertyInjectable');

// Write the updated file back to disk
sourceFile.saveSync();
}

/**
* Add the import statement for MetadataType to the file.
* If the Construct already has PROPERTY_INJECTION_ID, then it is injectable already.
*/
private addImportAndMetadataStatement(sourceFile: any, filePath: string, node: any) {
const ret = this.addLineInConstructor(sourceFile, node);
if (!ret) {
return;
private isAlreadyInjectable(classDeclaration: ClassDeclaration): boolean {
const properties: PropertyDeclaration[] = classDeclaration.getProperties();
for (const prop of properties) {
if (prop.getName() === 'PROPERTY_INJECTION_ID') {
console.log(`Skipping ${classDeclaration.getName()}. It is already injectable`);
return true;
}
}
return false;
}

const absoluteFilePath = path.resolve(filePath);
const absoluteTargetPath = path.resolve(__dirname, '../../../../packages/aws-cdk-lib/core/lib/metadata-resource.ts');
/**
* This converts the filePath
* '<HOME_DIR>/<CDK_HOME>/aws-cdk/packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts'
* and className 'ApiKey'
* to 'aws-cdk-lib.aws-apigateway.ApiKey'.
*
* '<HOME_DIR>/<CDK_HOME>/aws-cdk/packages/@aws-cdk/aws-amplify-alpha/lib/app.ts'
* and className 'App'
* to '@aws-cdk.aws-amplify-alpha.App
*/
private filePathToInjectionId(filePath: string, className: string | undefined): string {
if (!className) {
throw new Error('Could not build PROPERTY_INJECTION_ID if className is undefined');
}

const start = '/packages/';
const startIndex = filePath.indexOf(start);
const subPath = filePath.substring(startIndex + start.length);
const parts: string[] = subPath.split('\/');
if (parts.length < 3) {
throw new Error(`Could not build PROPERTY_INJECTION_ID for ${filePath} ${className}`);
}

// we only care about /packages/aws-cdk-lib/ and /packages/@aws-cdk/,
// but in case there are L2 constructs in other sub dir, it will handle it too.
return `'${parts[0]}.${parts[1]}.${className}'`;
}

/**
* This returns the relative path of the import file in core/lib.
* For example, importFile is prop-injectable or metadata-resource
*/
private getRelativePathForPropInjectionImport(filePath: string, importFile: string): string {
const absoluteFilePath = path.resolve(filePath);
const absoluteTargetPath = path.resolve(__dirname, `../../../../packages/aws-cdk-lib/core/lib/${importFile}.ts`);
let relativePath = path.relative(path.dirname(absoluteFilePath), absoluteTargetPath).replace(/\\/g, "/").replace(/.ts/, "");
if (absoluteFilePath.includes('@aws-cdk')) {
relativePath = 'aws-cdk-lib/core/lib/metadata-resource'
relativePath = `aws-cdk-lib/core/lib/${importFile}`
}
return relativePath;
}

// Check if an import from 'metadata-resource' already exists
/**
* This adds import of a class in aws-cdk-lib/core/lib to the file.
*/
private importCoreLibFile(
sourceFile: SourceFile,
filePath: string,
importfileName: string,
importClassName: string
) {
const relativePath = this.getRelativePathForPropInjectionImport(filePath, importfileName);

// Check if an import from the import file already exists
const existingImport = sourceFile.getImportDeclarations().find((stmt: any) => {
return stmt.getModuleSpecifier().getText().includes('/metadata-resource');
return stmt.getModuleSpecifier().getText().includes(importfileName);
});

if (existingImport) {
// Check if 'MethodMetadata' is already imported
const namedImports = existingImport.getNamedImports().map((imp: any) => imp.getName());
if (!namedImports.includes("addConstructMetadata")) {
existingImport.addNamedImport({ name: "addConstructMetadata" });
console.log(`Merged import for addConstructMetadata in file: ${filePath}`);
}
} else {
// Find the correct insertion point (after the last import before the new one)
const importDeclarations = sourceFile.getImportDeclarations();
let insertIndex = importDeclarations.length; // Default to appending

for (let i = importDeclarations.length - 1; i >= 0; i--) {
const existingImport = importDeclarations[i].getModuleSpecifier().getLiteralText();
return;
}

// Insert the new import before the first one that is lexicographically greater
if (existingImport.localeCompare(relativePath) > 0) {
insertIndex = i;
} else {
break;
}
// Find the correct insertion point (after the last import before the new one)
const importDeclarations = sourceFile.getImportDeclarations();
let insertIndex = importDeclarations.length; // Default to appending

for (let i = importDeclarations.length - 1; i >= 0; i--) {
const existingImport = importDeclarations[i].getModuleSpecifier().getLiteralText();

// Insert the new import before the first one that is lexicographically greater
if (existingImport.localeCompare(relativePath) > 0) {
insertIndex = i;
} else {
break;
}

// Insert the new import at the correct index
sourceFile.insertImportDeclaration(insertIndex, {
moduleSpecifier: relativePath,
namedImports: [{ name: "addConstructMetadata" }],
});
console.log(`Added import for addConstructMetadata in file: ${filePath} with relative path: ${relativePath}`);
}

// Insert the new import at the correct index
sourceFile.insertImportDeclaration(insertIndex, {
moduleSpecifier: relativePath,
namedImports: [{ name: importClassName }],
});
console.log(` Added import for ${importClassName} in file: ${filePath} with relative path: ${relativePath}`);
}

/**
* Add the import statement for MetadataType to the file.
*/
private addImportAndMetadataStatement(sourceFile: any, filePath: string, node: any) {
const ret = this.addLineInConstructor(sourceFile, node);
if (!ret) {
return;
}

this.importCoreLibFile(sourceFile, filePath, 'metadata-resource', 'addConstructMetadata');

// Write the updated file back to disk
sourceFile.saveSync();
}
Expand Down
Loading
Loading