tools/@aws-cdk/spec2cdk/lib/generate.ts (151 lines of code) (raw):
import * as path from 'path';
import { loadAwsServiceSpec } from '@aws-cdk/aws-service-spec';
import { DatabaseBuilder } from '@aws-cdk/service-spec-importers';
import { SpecDatabase } from '@aws-cdk/service-spec-types';
import { TypeScriptRenderer } from '@cdklabs/typewriter';
import * as fs from 'fs-extra';
import { AstBuilder, ServiceModule } from './cdk/ast';
import { ModuleImportLocations } from './cdk/cdk';
import { queryDb, log, PatternedString, TsFileWriter } from './util';
export type PatternKeys = 'moduleName' | 'serviceName' | 'serviceShortName';
export interface GenerateModuleOptions {
/**
* List of services to generate files for.
*
* In CloudFormation notation.
*
* @example ["AWS::Lambda", "AWS::S3"]
*/
readonly services: string[];
/**
* Map of optional suffixes used for classes generated for a service.
*
* @example { "AWS::Lambda": "FooBar"} -> class CfnFunctionFooBar {}
*/
readonly serviceSuffixes?: { [service: string]: string };
/**
* Override the default locations where modules are imported from on the module level
*/
readonly moduleImportLocations?: ModuleImportLocations;
}
export interface GenerateFilePatterns {
/**
* The pattern used to name resource files.
* @default "%module.name%/%service.short%.generated.ts"
*/
readonly resources?: PatternedString<PatternKeys>;
/**
* The pattern used to name augmentations.
* @default "%module.name%/%service.short%-augmentations.generated.ts"
*/
readonly augmentations?: PatternedString<PatternKeys>;
/**
* The pattern used to name canned metrics.
* @default "%module.name%/%service.short%-canned-metrics.generated.ts"
*/
readonly cannedMetrics?: PatternedString<PatternKeys>;
}
export interface GenerateOptions {
/**
* Default location for module imports
*/
readonly importLocations?: ModuleImportLocations;
/**
* Configure where files are created exactly
*/
readonly filePatterns?: GenerateFilePatterns;
/**
* Base path for generated files
*
* @see `options.filePatterns` to configure more complex scenarios.
*
* @default - current working directory
*/
readonly outputPath: string;
/**
* Should the location be deleted before generating new files
* @default false
*/
readonly clearOutput?: boolean;
/**
* Generate L2 stub support files for augmentations (only for testing)
*
* @default false
*/
readonly augmentationsSupport?: boolean;
/**
* Output debug messages
* @default false
*/
readonly debug?: boolean;
}
export interface GenerateModuleMap {
[name: string]: GenerateModuleOptions;
}
export interface GenerateOutput {
outputFiles: string[];
resources: Record<string, string>;
modules: {
[name: string]: Array<{
module: AstBuilder<ServiceModule>;
options: GenerateModuleOptions;
resources: AstBuilder<ServiceModule>['resources'];
outputFiles: string[];
}>;
};
}
/**
* Generates Constructs for modules from the Service Specs
*
* @param modules A map of arbitrary module names to GenerateModuleOptions. This allows for flexible generation of different configurations at a time.
* @param options Configure the code generation
*/
export async function generate(modules: GenerateModuleMap, options: GenerateOptions) {
enableDebug(options);
const db = await loadAwsServiceSpec();
// Load additional schema files
await new DatabaseBuilder(db as any, { validate: false })
.importCloudFormationRegistryResources(path.join(__dirname, '..', 'temporary-schemas'))
.build();
return generator(db, modules, options);
}
/**
* Generates Constructs for all services, with modules name like the service
*
* @param outputPath Base path for generated files. Use `options.filePatterns` to configure more complex scenarios.
* @param options Additional configuration
*/
export async function generateAll(options: GenerateOptions) {
enableDebug(options);
const db = await loadAwsServiceSpec();
const services = await queryDb.getAllServices(db);
const modules: GenerateModuleMap = {};
for (const service of services) {
modules[service.name] = {
services: [service.cloudFormationNamespace],
};
}
return generator(db, modules, options);
}
function enableDebug(options: GenerateOptions) {
if (options.debug) {
process.env.DEBUG = '1';
}
}
async function generator(
db: SpecDatabase,
modules: { [name: string]: GenerateModuleOptions },
options: GenerateOptions,
): Promise<GenerateOutput> {
const timeLabel = '🐢 Completed in';
log.time(timeLabel);
log.debug('Options', options);
const { augmentationsSupport, clearOutput, outputPath = process.cwd() } = options;
const filePatterns = ensureFilePatterns(options.filePatterns);
const renderer = new TypeScriptRenderer();
// store results in a map of modules
const moduleMap: GenerateOutput['modules'] = {};
// Clear output if requested
if (clearOutput) {
fs.removeSync(outputPath);
}
// Go through the module map
log.info('Generating %i modules...', Object.keys(modules).length);
for (const [moduleName, moduleOptions] of Object.entries(modules)) {
const { moduleImportLocations: importLocations = options.importLocations, serviceSuffixes } = moduleOptions;
moduleMap[moduleName] = queryDb.getServicesByCloudFormationNamespace(db, moduleOptions.services).map((s) => {
log.debug(moduleName, s.name, 'ast');
const ast = AstBuilder.forService(s, {
db,
importLocations,
nameSuffix: serviceSuffixes?.[s.cloudFormationNamespace],
});
log.debug(moduleName, s.name, 'render');
const writer = new TsFileWriter(outputPath, renderer, {
['moduleName']: moduleName,
['serviceName']: ast.module.service.toLowerCase(),
['serviceShortName']: ast.module.shortName.toLowerCase(),
});
// Resources
writer.write(ast.module, filePatterns.resources);
if (ast.augmentations?.hasAugmentations) {
const augFile = writer.write(ast.augmentations, filePatterns.augmentations);
if (augmentationsSupport) {
const augDir = path.dirname(augFile);
for (const supportMod of ast.augmentations.supportModules) {
writer.write(supportMod, path.resolve(augDir, `${supportMod.importName}.ts`));
}
}
}
if (ast.cannedMetrics?.hasCannedMetrics) {
writer.write(ast.cannedMetrics, filePatterns.cannedMetrics);
}
return {
module: ast,
options: moduleOptions,
resources: ast.resources,
outputFiles: writer.outputFiles,
};
});
}
const result = {
modules: moduleMap,
resources: Object.values(moduleMap).flat().map(pick('resources')).reduce(mergeObjects, {}),
outputFiles: Object.values(moduleMap).flat().flatMap(pick('outputFiles')),
};
log.info('Summary:');
log.info(' Service files: %i', Object.values(moduleMap).flat().flatMap(pick('module')).length);
log.info(' Resources: %i', Object.keys(result.resources).length);
log.timeEnd(timeLabel);
return result;
}
function ensureFilePatterns(patterns: GenerateFilePatterns = {}): Required<GenerateFilePatterns> {
return {
resources: ({ serviceShortName }) => `${serviceShortName}.generated.ts`,
augmentations: ({ serviceShortName }) => `${serviceShortName}-augmentations.generated.ts`,
cannedMetrics: ({ serviceShortName }) => `${serviceShortName}-canned-metrics.generated.ts`,
...patterns,
};
}
function pick<T>(property: keyof T) {
type x = typeof property;
return (obj: Record<x, any>): any => {
return obj[property];
};
}
function mergeObjects<T>(all: T, res: T) {
return {
...all,
...res,
};
}