@aws-amplify/cli-internal
Version:
Amplify CLI
245 lines • 12.2 kB
JavaScript
;
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