Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
126 changes: 125 additions & 1 deletion 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,10 +152,134 @@ 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.importPropertyInjectable(sourceFile, filePath);

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

/**
* If the Construct already has PROPERTY_INJECTION_ID, then it is injectable already.
*/
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;
}

/**
* 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'
*/
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/aws-cdk-lib/';
const startIndex = filePath.indexOf(start);
const subPath = filePath.substring(startIndex + start.length);
const parts: string[] = subPath.split('\/');
if (parts.length < 2) {
throw new Error(`Could not build PROPERTY_INJECTION_ID for ${filePath} ${className}`);
}
return `'aws-cdk-lib.${parts[0]}.${className}'`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about alpha modules?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I am not familiar with alpha modules. Can you give me some examples and I can add them to the unit tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modules in packages/@aws-cdk/ are alpha modules (aka experimental modules). The constructs in these modules should also be property injectable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the logic to handle /packages/@awsk-cdk/...

}

/**
* This returns the relative path of prop-injectable.ts.
*/
private getRelativePathForPropInjectionImport(filePath: string): string {
const absoluteFilePath = path.resolve(filePath);
const absoluteTargetPath = path.resolve(__dirname, '../../../../packages/aws-cdk-lib/core/lib/prop-injectable.ts');
let relativePath = path.relative(path.dirname(absoluteFilePath), absoluteTargetPath).replace(/\\/g, "/").replace(/.ts/, "");
if (absoluteFilePath.includes('@aws-cdk')) {
relativePath = 'aws-cdk-lib/core/lib/prop-injectable'
}
return relativePath;
}

/**
* This adds import of prop-injectable to the file.
*/
private importPropertyInjectable(sourceFile: SourceFile, filePath: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we group this method and the below addImportAndMetadataStatement's import part together into a function and make the function generic to handle different import statement input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. Let me combine them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addImportAndMetadataStatementn and makeConstructsPropInjectable both use importCoreLibFile

const relativePath = this.getRelativePathForPropInjectionImport(filePath);

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

// 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: "propertyInjectable" }],
});
console.log(` Added import for propertyInjectable in file: ${filePath} with relative path: ${relativePath}`);
}

/**
* Add the import statement for MetadataType to the file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,217 @@ describe('ResourceMetadataUpdater', () => {
expect(mockConstructor.insertStatements).toHaveBeenCalled();
});
});

describe('filePathToInjectionId', () => {
it('should return successfully', () => {
// GIVEN
const filePath = '/local/home/user/cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts';
const className = 'ApiKey';

// WHEN
const injectionId = (updater as any).filePathToInjectionId(filePath, className);

// THEN
expect(injectionId).toEqual("'aws-cdk-lib.aws-apigateway.ApiKey'");
});

it('should throw error for bad filepath', () => {
// GIVEN
const filePath = 'cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway';
const className = 'ApiKey';

// WHEN THEN
expect(() => (updater as any).filePathToInjectionId(filePath, className))
.toThrow('Could not build PROPERTY_INJECTION_ID for cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway ApiKey');
});

it('should throw error for undefined className', () => {
// GIVEN
const filePath = 'cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway';
const className = undefined;

// WHEN THEN
expect(() => (updater as any).filePathToInjectionId(filePath, className))
.toThrow('Could not build PROPERTY_INJECTION_ID if className is undefined');
});
});

describe('isAlreadyInjectable', () => {
it('should return true', () => {
// GIVEN
const mockProperty1 = {
getName: jest.fn().mockReturnValue('Property1'),
} as any;

const mockProperty2 = {
getName: jest.fn().mockReturnValue('PROPERTY_INJECTION_ID'),
} as any;

mockClassDeclaration = {
getName: jest.fn().mockReturnValue('TestClass'),
getProperties: jest.fn().mockReturnValue([mockProperty1, mockProperty2])
} as any;

// WHEN
const result = (updater as any).isAlreadyInjectable(mockClassDeclaration);

//THEN
expect(result).toBe(true);
});

it('should return false', () => {
// GIVEN
const mockProperty1 = {
getName: jest.fn().mockReturnValue('Property1'),
} as any;

const mockProperty2 = {
getName: jest.fn().mockReturnValue('INJECTION_ID'),
} as any;

mockClassDeclaration = {
getName: jest.fn().mockReturnValue('TestClass'),
getProperties: jest.fn().mockReturnValue([mockProperty1, mockProperty2])
} as any;

// WHEN
const result = (updater as any).isAlreadyInjectable(mockClassDeclaration);

// THEN
expect(result).toBe(false);
});
});

describe('importPropertyInjectable', () => {
it('should be inserted in the correct place', () => {
// GIVEN
const filePath = '/local/home/user/cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts';
const relativePath = '../../core/lib/prop-injectable';

const module1 = {
getText: jest.fn().mockReturnValue('../../core/lib/errors'),
getLiteralText: jest.fn().mockReturnValue('../../core/lib/errors'),
} as any;
const import1 = {
getModuleSpecifier: jest.fn().mockReturnValue(module1),
} as any;

const module2 = {
getText: jest.fn().mockReturnValue('../../core/lib/removal-policies'),
getLiteralText: jest.fn().mockReturnValue('../../core/lib/removal-policies'),
} as any;
const import2 = {
getModuleSpecifier: jest.fn().mockReturnValue(module2),
} as any;

mockSourceFile = {
forEachChild: jest.fn((callback) => callback(mockClassDeclaration)),
getImportDeclarations: jest.fn().mockReturnValue([import1, import2]),
addImportDeclaration: jest.fn(),
saveSync: jest.fn(),
insertImportDeclaration: jest.fn()
} as any;

// Setup spies to return the relative path
const updaterSpy = jest.spyOn(updater as any, 'getRelativePathForPropInjectionImport');
updaterSpy.mockReturnValueOnce(relativePath);

// WHEN
(updater as any).importPropertyInjectable(mockSourceFile, filePath);

// THEN
expect(mockSourceFile.insertImportDeclaration).toHaveBeenCalledWith(
1,
{
moduleSpecifier: relativePath,
namedImports: [{ name: "propertyInjectable" }],
},
);
});
});

describe('makeConstructsPropInjectable', () => {
it('should skip because class already has PROPERTY_INJECTION_ID', () => {
// GIVEN
const filePath = '/local/home/user/cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts';

const mockProperty1 = {
getName: jest.fn().mockReturnValue('Property1'),
} as any;

const mockProperty2 = {
getName: jest.fn().mockReturnValue('PROPERTY_INJECTION_ID'),
} as any;

mockClassDeclaration = {
getName: jest.fn().mockReturnValue('TestClass'),
getProperties: jest.fn().mockReturnValue([mockProperty1, mockProperty2]),
addProperty: jest.fn(),
addDecorator: jest.fn(),
} as any;

mockSourceFile = {
forEachChild: jest.fn((callback) => callback(mockClassDeclaration)),
getImportDeclarations: jest.fn().mockReturnValue([]),
addImportDeclaration: jest.fn(),
saveSync: jest.fn(),
insertImportDeclaration: jest.fn()
} as any;

// WHEN
(updater as any).makeConstructsPropInjectable(mockSourceFile, filePath, mockClassDeclaration);

// THEN
expect(mockClassDeclaration.addProperty).not.toHaveBeenCalled();
expect(mockClassDeclaration.addDecorator).not.toHaveBeenCalled();
expect(mockSourceFile.insertImportDeclaration).not.toHaveBeenCalled();
});

it('should add PROPERTY_INJECTION_ID, import and class decorator', () => {
// GIVEN
const filePath = '/local/home/user/cdk/aws-cdk/packages/aws-cdk-lib/aws-apigateway/lib/api-key.ts';
const relativePath = '../../core/lib/prop-injectable';

const mockProperty1 = {
getName: jest.fn().mockReturnValue('Property1'),
} as any;

mockClassDeclaration = {
getName: jest.fn().mockReturnValue('TestClass'),
getProperties: jest.fn().mockReturnValue([mockProperty1]),
addProperty: jest.fn(),
addDecorator: jest.fn(),
} as any;

const module1 = {
getText: jest.fn().mockReturnValue('../../core/lib/errors'),
getLiteralText: jest.fn().mockReturnValue('../../core/lib/errors'),
} as any;
const import1 = {
getModuleSpecifier: jest.fn().mockReturnValue(module1),
} as any;

mockSourceFile = {
forEachChild: jest.fn((callback) => callback(mockClassDeclaration)),
getImportDeclarations: jest.fn().mockReturnValue([import1]),
addImportDeclaration: jest.fn(),
saveSync: jest.fn(),
insertImportDeclaration: jest.fn()
} as any;

// Setup spies to return the relative path
const updaterSpy = jest.spyOn(updater as any, 'getRelativePathForPropInjectionImport');
updaterSpy.mockReturnValueOnce(relativePath);

// WHEN
(updater as any).makeConstructsPropInjectable(mockSourceFile, filePath, mockClassDeclaration);

// THEN
expect(mockClassDeclaration.addProperty).toHaveBeenCalled();
expect(mockClassDeclaration.addDecorator).toHaveBeenCalled();
expect(mockSourceFile.insertImportDeclaration).toHaveBeenCalled();
});
});
});

describe('PropertyUpdater', () => {
Expand Down
Loading