|
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"; |
2 | 2 | import * as path from "path";
|
3 | 3 | import * as fs from "fs";
|
4 | 4 | // import { exec } from "child_process";
|
@@ -152,64 +152,159 @@ export class ConstructsUpdater extends MetadataUpdater {
|
152 | 152 | const classes = this.getCdkResourceClasses(sourceFile.getFilePath());
|
153 | 153 | for (const resource of classes) {
|
154 | 154 | this.addImportAndMetadataStatement(resource.sourceFile, resource.filePath, resource.node);
|
| 155 | + this.makeConstructsPropInjectable(resource.sourceFile, resource.filePath, resource.node); |
155 | 156 | }
|
156 | 157 | });
|
157 | 158 | }
|
158 | 159 |
|
| 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 | + } |
159 | 198 |
|
160 | 199 | /**
|
161 |
| - * Add the import statement for MetadataType to the file. |
| 200 | + * If the Construct already has PROPERTY_INJECTION_ID, then it is injectable already. |
162 | 201 | */
|
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 | + } |
167 | 209 | }
|
| 210 | + return false; |
| 211 | + } |
168 | 212 |
|
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 | + } |
171 | 240 |
|
| 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`); |
172 | 248 | let relativePath = path.relative(path.dirname(absoluteFilePath), absoluteTargetPath).replace(/\\/g, "/").replace(/.ts/, "");
|
173 | 249 | if (absoluteFilePath.includes('@aws-cdk')) {
|
174 |
| - relativePath = 'aws-cdk-lib/core/lib/metadata-resource' |
| 250 | + relativePath = `aws-cdk-lib/core/lib/${importFile}` |
175 | 251 | }
|
| 252 | + return relativePath; |
| 253 | + } |
176 | 254 |
|
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 |
178 | 267 | const existingImport = sourceFile.getImportDeclarations().find((stmt: any) => {
|
179 |
| - return stmt.getModuleSpecifier().getText().includes('/metadata-resource'); |
| 268 | + return stmt.getModuleSpecifier().getText().includes(importfileName); |
180 | 269 | });
|
181 |
| - |
182 | 270 | 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 | + } |
196 | 273 |
|
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; |
203 | 286 | }
|
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}`); |
211 | 287 | }
|
212 | 288 |
|
| 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 | + |
213 | 308 | // Write the updated file back to disk
|
214 | 309 | sourceFile.saveSync();
|
215 | 310 | }
|
|
0 commit comments