Skip to content

Commit b99f241

Browse files
authored
chore: make all L2 Constructs property injectable during release (#34328)
### Issue # (if applicable) This is a followup to #33887. DO NOT merge this PR until after 33887 is merged. ### Reason for this change In the original Property Injection PR, we enabled 122 Constructs for Property Injections, but we want to make this feature available for all L2 Constructs. ### Description of changes Add logic to Constructs ConstructsUpdater to if a Construct already have PROPERTY_INJECTION_ID property. If not, it will: - Add PROPERTY_INJECTION_ID with value that is calculated from path and class name - Import aws-cdk-lib/core/lib/prop-injectable - Add class decorator @propertyInjectable ### Describe any new or updated permissions being added No permission change. ### Description of how you validated changes - Added unit tests and ran `npm test` - Ran `./bin/update-construct-metadata` locally and manually verified a few random files. ### Checklist - [ ] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d867878 commit b99f241

File tree

3 files changed

+396
-39
lines changed

3 files changed

+396
-39
lines changed

tools/@aws-cdk/construct-metadata-updater/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

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

5-
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
5+
1. `ConstructUpdater`:
6+
- 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.
7+
- Also make all non-abstract L2 Constructs Property Injectable.
8+
It skips over Constructs that are already Property Injectable.
69
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.
710
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.
811
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

tools/@aws-cdk/construct-metadata-updater/lib/metadata-updater.ts

Lines changed: 133 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ClassDeclaration, IndentationText, Project, PropertyDeclaration, QuoteKind, SourceFile, Symbol, SyntaxKind } from "ts-morph";
1+
import { ClassDeclaration, IndentationText, Project, PropertyDeclaration, QuoteKind, Scope, SourceFile, Symbol, SyntaxKind } from "ts-morph";
22
import * as path from "path";
33
import * as fs from "fs";
44
// import { exec } from "child_process";
@@ -152,64 +152,159 @@ export class ConstructsUpdater extends MetadataUpdater {
152152
const classes = this.getCdkResourceClasses(sourceFile.getFilePath());
153153
for (const resource of classes) {
154154
this.addImportAndMetadataStatement(resource.sourceFile, resource.filePath, resource.node);
155+
this.makeConstructsPropInjectable(resource.sourceFile, resource.filePath, resource.node);
155156
}
156157
});
157158
}
158159

160+
/**
161+
* This makes a Construct Property Injectable by doing 3 things:
162+
* - add PROPERTY_INJECTION_ID property
163+
* - import propertyInjectable from core/lib/prop-injectable
164+
* - add class decorator @propertyInjectable
165+
*
166+
* If the Construct already has PROPERTY_INJECTION_ID, then skip it.
167+
*/
168+
private makeConstructsPropInjectable(sourceFile: SourceFile, filePath: string, node: ClassDeclaration) {
169+
console.log(`path: ${filePath}, class: ${node.getName()}`);
170+
171+
if (this.isAlreadyInjectable(node)) {
172+
return; // do nothing
173+
}
174+
175+
// Add PROPERTY_INJECTION_ID
176+
node.addProperty({
177+
scope: Scope.Public,
178+
isStatic: true,
179+
isReadonly: true,
180+
name: 'PROPERTY_INJECTION_ID',
181+
type: "string",
182+
initializer: this.filePathToInjectionId(filePath, node.getName()),
183+
});
184+
console.log(' Added PROPERTY_INJECTION_ID')
185+
186+
// Add Decorator
187+
node.addDecorator({
188+
name: "propertyInjectable",
189+
});
190+
console.log(' Added @propertyInjectable')
191+
192+
// import propertyInjectable
193+
this.importCoreLibFile(sourceFile, filePath, 'prop-injectable', 'propertyInjectable');
194+
195+
// Write the updated file back to disk
196+
sourceFile.saveSync();
197+
}
159198

160199
/**
161-
* Add the import statement for MetadataType to the file.
200+
* If the Construct already has PROPERTY_INJECTION_ID, then it is injectable already.
162201
*/
163-
private addImportAndMetadataStatement(sourceFile: any, filePath: string, node: any) {
164-
const ret = this.addLineInConstructor(sourceFile, node);
165-
if (!ret) {
166-
return;
202+
private isAlreadyInjectable(classDeclaration: ClassDeclaration): boolean {
203+
const properties: PropertyDeclaration[] = classDeclaration.getProperties();
204+
for (const prop of properties) {
205+
if (prop.getName() === 'PROPERTY_INJECTION_ID') {
206+
console.log(`Skipping ${classDeclaration.getName()}. It is already injectable`);
207+
return true;
208+
}
167209
}
210+
return false;
211+
}
168212

169-
const absoluteFilePath = path.resolve(filePath);
170-
const absoluteTargetPath = path.resolve(__dirname, '../../../../packages/aws-cdk-lib/core/lib/metadata-resource.ts');
213+
/**
214+
* This converts the filePath
215+
* '<HOME_DIR>/<CDK_HOME>/aws-cdk/packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts'
216+
* and className 'ApiKey'
217+
* to 'aws-cdk-lib.aws-apigateway.ApiKey'.
218+
*
219+
* '<HOME_DIR>/<CDK_HOME>/aws-cdk/packages/@aws-cdk/aws-amplify-alpha/lib/app.ts'
220+
* and className 'App'
221+
* to '@aws-cdk.aws-amplify-alpha.App
222+
*/
223+
private filePathToInjectionId(filePath: string, className: string | undefined): string {
224+
if (!className) {
225+
throw new Error('Could not build PROPERTY_INJECTION_ID if className is undefined');
226+
}
227+
228+
const start = '/packages/';
229+
const startIndex = filePath.indexOf(start);
230+
const subPath = filePath.substring(startIndex + start.length);
231+
const parts: string[] = subPath.split('\/');
232+
if (parts.length < 3) {
233+
throw new Error(`Could not build PROPERTY_INJECTION_ID for ${filePath} ${className}`);
234+
}
235+
236+
// we only care about /packages/aws-cdk-lib/ and /packages/@aws-cdk/,
237+
// but in case there are L2 constructs in other sub dir, it will handle it too.
238+
return `'${parts[0]}.${parts[1]}.${className}'`;
239+
}
171240

241+
/**
242+
* This returns the relative path of the import file in core/lib.
243+
* For example, importFile is prop-injectable or metadata-resource
244+
*/
245+
private getRelativePathForPropInjectionImport(filePath: string, importFile: string): string {
246+
const absoluteFilePath = path.resolve(filePath);
247+
const absoluteTargetPath = path.resolve(__dirname, `../../../../packages/aws-cdk-lib/core/lib/${importFile}.ts`);
172248
let relativePath = path.relative(path.dirname(absoluteFilePath), absoluteTargetPath).replace(/\\/g, "/").replace(/.ts/, "");
173249
if (absoluteFilePath.includes('@aws-cdk')) {
174-
relativePath = 'aws-cdk-lib/core/lib/metadata-resource'
250+
relativePath = `aws-cdk-lib/core/lib/${importFile}`
175251
}
252+
return relativePath;
253+
}
176254

177-
// Check if an import from 'metadata-resource' already exists
255+
/**
256+
* This adds import of a class in aws-cdk-lib/core/lib to the file.
257+
*/
258+
private importCoreLibFile(
259+
sourceFile: SourceFile,
260+
filePath: string,
261+
importfileName: string,
262+
importClassName: string
263+
) {
264+
const relativePath = this.getRelativePathForPropInjectionImport(filePath, importfileName);
265+
266+
// Check if an import from the import file already exists
178267
const existingImport = sourceFile.getImportDeclarations().find((stmt: any) => {
179-
return stmt.getModuleSpecifier().getText().includes('/metadata-resource');
268+
return stmt.getModuleSpecifier().getText().includes(importfileName);
180269
});
181-
182270
if (existingImport) {
183-
// Check if 'MethodMetadata' is already imported
184-
const namedImports = existingImport.getNamedImports().map((imp: any) => imp.getName());
185-
if (!namedImports.includes("addConstructMetadata")) {
186-
existingImport.addNamedImport({ name: "addConstructMetadata" });
187-
console.log(`Merged import for addConstructMetadata in file: ${filePath}`);
188-
}
189-
} else {
190-
// Find the correct insertion point (after the last import before the new one)
191-
const importDeclarations = sourceFile.getImportDeclarations();
192-
let insertIndex = importDeclarations.length; // Default to appending
193-
194-
for (let i = importDeclarations.length - 1; i >= 0; i--) {
195-
const existingImport = importDeclarations[i].getModuleSpecifier().getLiteralText();
271+
return;
272+
}
196273

197-
// Insert the new import before the first one that is lexicographically greater
198-
if (existingImport.localeCompare(relativePath) > 0) {
199-
insertIndex = i;
200-
} else {
201-
break;
202-
}
274+
// Find the correct insertion point (after the last import before the new one)
275+
const importDeclarations = sourceFile.getImportDeclarations();
276+
let insertIndex = importDeclarations.length; // Default to appending
277+
278+
for (let i = importDeclarations.length - 1; i >= 0; i--) {
279+
const existingImport = importDeclarations[i].getModuleSpecifier().getLiteralText();
280+
281+
// Insert the new import before the first one that is lexicographically greater
282+
if (existingImport.localeCompare(relativePath) > 0) {
283+
insertIndex = i;
284+
} else {
285+
break;
203286
}
204-
205-
// Insert the new import at the correct index
206-
sourceFile.insertImportDeclaration(insertIndex, {
207-
moduleSpecifier: relativePath,
208-
namedImports: [{ name: "addConstructMetadata" }],
209-
});
210-
console.log(`Added import for addConstructMetadata in file: ${filePath} with relative path: ${relativePath}`);
211287
}
212288

289+
// Insert the new import at the correct index
290+
sourceFile.insertImportDeclaration(insertIndex, {
291+
moduleSpecifier: relativePath,
292+
namedImports: [{ name: importClassName }],
293+
});
294+
console.log(` Added import for ${importClassName} in file: ${filePath} with relative path: ${relativePath}`);
295+
}
296+
297+
/**
298+
* Add the import statement for MetadataType to the file.
299+
*/
300+
private addImportAndMetadataStatement(sourceFile: any, filePath: string, node: any) {
301+
const ret = this.addLineInConstructor(sourceFile, node);
302+
if (!ret) {
303+
return;
304+
}
305+
306+
this.importCoreLibFile(sourceFile, filePath, 'metadata-resource', 'addConstructMetadata');
307+
213308
// Write the updated file back to disk
214309
sourceFile.saveSync();
215310
}

0 commit comments

Comments
 (0)