UNPKG

@aws-amplify/cli-internal

Version:
245 lines • 12.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CustomResourceGenerator = void 0; const node_path_1 = __importDefault(require("node:path")); const promises_1 = __importDefault(require("node:fs/promises")); const typescript_1 = __importDefault(require("typescript")); const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core"); const amplify_helper_transformer_1 = require("./amplify-helper-transformer"); const CUSTOM_DIR = 'custom'; const TYPES_DIR = 'types'; const AMPLIFY_DIR = 'amplify'; const BACKEND_DIR = 'backend'; const FILTER_FILES = new Set(['package.json', 'yarn.lock']); const BUILD_ARTIFACTS = ['build', 'node_modules', '.npmrc', 'yarn.lock', 'package-lock.json', 'tsconfig.json']; const EXCLUDED_DEPENDENCIES = new Set(['aws-cdk-lib', 'constructs', 'aws-cdk', '@aws-amplify/cli-extensibility-helper']); function isExcludedDependency(name) { return EXCLUDED_DEPENDENCIES.has(name) || name.startsWith('@aws-cdk/'); } function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } class CustomResourceGenerator { constructor(gen1App, backendGenerator, packageJsonGenerator, outputDir, resourceName, logger) { this.backendGenerator = backendGenerator; this.packageJsonGenerator = packageJsonGenerator; this.outputDir = outputDir; this.resourceName = resourceName; this.logger = logger; this.gen1App = gen1App; } async plan() { const rootDir = process.cwd(); const sourceResourcePath = node_path_1.default.join(rootDir, AMPLIFY_DIR, BACKEND_DIR, CUSTOM_DIR, this.resourceName); const destResourcePath = node_path_1.default.join(this.outputDir, AMPLIFY_DIR, CUSTOM_DIR, this.resourceName); return [ { validate: () => undefined, describe: async () => [`Migrate amplify/custom/${this.resourceName}/resource.ts`], execute: async () => { await promises_1.default.mkdir(destResourcePath, { recursive: true }); await promises_1.default.cp(sourceResourcePath, destResourcePath, { recursive: true, filter: (src) => !FILTER_FILES.has(node_path_1.default.basename(src)), }); const sourceTypesPath = node_path_1.default.join(rootDir, AMPLIFY_DIR, BACKEND_DIR, TYPES_DIR); const destTypesPath = node_path_1.default.join(this.outputDir, AMPLIFY_DIR, TYPES_DIR); try { await promises_1.default.mkdir(destTypesPath, { recursive: true }); await promises_1.default.cp(sourceTypesPath, destTypesPath, { recursive: true }); } catch (e) { const isNotFound = e instanceof Error && e.code === 'ENOENT'; if (!isNotFound) { throw e; } } const projectName = await readProjectName(rootDir); const dependencies = await extractDependencies(sourceResourcePath); const constructClassName = capitalize(this.resourceName); await transformResource(destResourcePath, projectName, this.resourceName, constructClassName, dependencies); await removeBuildArtifacts(destResourcePath); await renameCdkStackToConstruct(destResourcePath); await generateResourceWrapper(this.gen1App, destResourcePath, this.resourceName, constructClassName, dependencies); await this.mergeDependencies(sourceResourcePath); this.contributeToBackend(constructClassName); }, }, ]; } async mergeDependencies(sourceResourcePath) { const pkgJsonPath = node_path_1.default.join(sourceResourcePath, 'package.json'); try { const pkg = amplify_cli_core_1.JSONUtilities.readJson(pkgJsonPath); if (pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) { for (const [name, version] of Object.entries(pkg.dependencies)) { if (!isExcludedDependency(name)) { this.packageJsonGenerator.addDependency(name, version); } } } if (pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) { for (const [name, version] of Object.entries(pkg.devDependencies)) { if (!isExcludedDependency(name)) { this.packageJsonGenerator.addDevDependency(name, version); } } } } catch (e) { throw new Error(`Failed to read package.json for custom resource '${this.resourceName}': ${String(e)}`); } } contributeToBackend(constructClassName) { const alias = this.resourceName; const defineFnName = `define${constructClassName}`; this.backendGenerator.addNamespaceImport(alias, `./custom/${this.resourceName}/resource`); this.backendGenerator.addPostDefineBackendStatement(`${alias}.${defineFnName}(backend)`); } } exports.CustomResourceGenerator = CustomResourceGenerator; async function extractDependencies(sourceResourcePath) { const cdkStackFilePath = node_path_1.default.join(sourceResourcePath, 'cdk-stack.ts'); try { const content = await promises_1.default.readFile(cdkStackFilePath, { encoding: 'utf-8' }); const dependencies = []; const dependencyRegex = /AmplifyHelpers\.addResourceDependency\s*\([^,]+,[^,]+,[^,]+,\s*\[([^\]]+)\]/g; let match; while ((match = dependencyRegex.exec(content)) !== null) { const categoryRegex = /category:\s*['"]([^'"]+)['"]/g; let categoryMatch; while ((categoryMatch = categoryRegex.exec(match[1])) !== null) { if (!dependencies.includes(categoryMatch[1])) { dependencies.push(categoryMatch[1]); } } } if (dependencies.length === 0 && content.includes('amplify-dependent-resources-ref')) { const categoryAccessRegex = /\.\s*(auth|api|storage|function|analytics)\s*\./g; let catMatch; while ((catMatch = categoryAccessRegex.exec(content)) !== null) { if (!dependencies.includes(catMatch[1])) { dependencies.push(catMatch[1]); } } if (dependencies.length === 0) { dependencies.push('unknown'); } } return dependencies; } catch (e) { throw new Error(`Failed to read dependencies for custom resource '${sourceResourcePath}': ${String(e)}`); } } async function transformResource(destResourcePath, projectName, resourceName, constructClassName, dependencies) { const cdkStackFilePath = node_path_1.default.join(destResourcePath, 'cdk-stack.ts'); let content = await promises_1.default.readFile(cdkStackFilePath, { encoding: 'utf-8' }); if (!content.includes("from 'constructs'")) { const importRegex = /(import.*from.*['"]; ?\s*\n)/g; let lastImportMatch = null; let regexMatch; while ((regexMatch = importRegex.exec(content)) !== null) { lastImportMatch = regexMatch; } if (lastImportMatch) { const insertIndex = lastImportMatch.index + lastImportMatch[0].length; content = content.slice(0, insertIndex) + "import { Construct } from 'constructs';\n" + content.slice(insertIndex); } else { content = "import { Construct } from 'constructs';\n" + content; } } const sourceFile = typescript_1.default.createSourceFile(cdkStackFilePath, content, typescript_1.default.ScriptTarget.Latest, true); const transformedFile = amplify_helper_transformer_1.AmplifyHelperTransformer.transform(sourceFile, projectName); const transformedWithBranchName = amplify_helper_transformer_1.AmplifyHelperTransformer.addBranchNameVariable(transformedFile, projectName); const printer = typescript_1.default.createPrinter({ newLine: typescript_1.default.NewLineKind.LineFeed }); content = printer.printFile(transformedWithBranchName); content = content.replace(/export class \w+/, `export class ${constructClassName}`); if (dependencies.length > 0) { const importRegex2 = /(import.*from.*['"].*['"];?\s*\n)/g; let lastImportMatch2 = null; let regexMatch2; while ((regexMatch2 = importRegex2.exec(content)) !== null) { lastImportMatch2 = regexMatch2; } if (lastImportMatch2) { const insertIndex = lastImportMatch2.index + lastImportMatch2[0].length; content = content.slice(0, insertIndex) + "import type { Backend } from '../../backend';\n" + content.slice(insertIndex); } } await promises_1.default.writeFile(cdkStackFilePath, content, { encoding: 'utf-8' }); } async function removeBuildArtifacts(destResourcePath) { for (const artifact of BUILD_ARTIFACTS) { try { await promises_1.default.rm(node_path_1.default.join(destResourcePath, artifact), { recursive: true, force: true }); } catch (_a) { } } } async function renameCdkStackToConstruct(destResourcePath) { const cdkStackPath = node_path_1.default.join(destResourcePath, 'cdk-stack.ts'); const constructPath = node_path_1.default.join(destResourcePath, 'construct.ts'); try { await promises_1.default.rename(cdkStackPath, constructPath); } catch (e) { throw new Error(`Failed to rename cdk-stack.ts to construct.ts for custom resource: ${String(e)}`); } } async function generateResourceWrapper(gen1App, destResourcePath, resourceName, constructClassName, dependencies) { const defineFnName = `define${constructClassName}`; const stackName = `custom${resourceName}`; const args = [`backend.createStack('${stackName}')`, `'${resourceName}'`]; if (dependencies.length > 0) { args.push('backend'); } const statefulResourcesArray = gen1App.statefulResourceTypes.map((r) => ` '${r}',`).join('\n'); const content = [ "import { CfnResource } from 'aws-cdk-lib';", "import type { Backend } from '../../backend';", `import { ${constructClassName} } from './construct';`, '', 'export const STATEFUL_RESOURCES = [', statefulResourcesArray, '];', '', `export function ${defineFnName}(backend: Backend) {`, ` const construct = new ${constructClassName}(${args.join(', ')});`, '', ' for (const cfnResource of construct.node', ' .findAll()', ' .filter(', ' (c) =>', ' CfnResource.isCfnResource(c) &&', ' STATEFUL_RESOURCES.includes(', ' c.cfnResourceType', ' )', ' )) {', " (cfnResource as CfnResource).addOverride('UpdateReplacePolicy', 'Retain');", " (cfnResource as CfnResource).addOverride('DeletionPolicy', 'Retain');", ' }', '', ' return construct;', '}', '', ].join('\n'); const resourceTsPath = node_path_1.default.join(destResourcePath, 'resource.ts'); await promises_1.default.writeFile(resourceTsPath, content, 'utf-8'); } async function readProjectName(rootDir) { try { const projectConfigPath = node_path_1.default.join(rootDir, AMPLIFY_DIR, '.config', 'project-config.json'); const projectConfig = amplify_cli_core_1.JSONUtilities.readJson(projectConfigPath); return projectConfig === null || projectConfig === void 0 ? void 0 : projectConfig.projectName; } catch (e) { throw new Error(`Failed to read project config: ${String(e)}`); } } //# sourceMappingURL=custom.generator.js.map