cloudformation-declarations
Version:
TypeScript declarations and helpers for writing CloudFormation templates in TS or JS.
285 lines (284 loc) • 13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const glob_1 = require("glob");
const lodash_1 = require("lodash");
const assert = require("assert");
const request = require("request-promise");
const utils_1 = require("./utils");
const bluebird_1 = require("bluebird");
const jsdom_1 = require("jsdom");
const paths = {
generatedDeclaration: 'src/declarations/generated/cloudformation-types.ts',
docsCache: 'cache/html-docs.json',
specificationsDirectory: 'specifications'
};
function main() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const rootNamespace = new Namespace();
const specs = Object.create(null);
/** Cache of parsed HTML documentation with JQuery instances, stored by URL. */
const parsedDocs = new (class extends utils_1.Cache {
constructor() {
super(...arguments);
this.maxItems = 30;
}
dispose(v) {
v.window.close();
}
createItem(k, ck) {
const window = new jsdom_1.JSDOM(docs[ck]).window;
const $ = utils_1.JQueryFactory(window);
return { $, window };
}
});
/** Get a JQuery object for the given documentation URL */
function get$(url) { return parsedDocs.getOrCreate(url).$; }
const docs = Object.create(null);
// Pull docs from filesystem cache to avoid re-downloading all of them again.
Object.assign(docs, utils_1.readJsonFile(paths.docsCache, {}));
// Load all AWS specs into memory
glob_1.sync('**.json', { cwd: paths.specificationsDirectory }).map(path => {
specs[path] = utils_1.readJsonFile(`specifications/${path}`);
});
try {
yield bluebird_1.each(lodash_1.toPairs(specs), ([path, spec]) => tslib_1.__awaiter(this, void 0, void 0, function* () {
yield bluebird_1.each([
...lodash_1.toPairs(spec.PropertyTypes),
...lodash_1.toPairs(spec.ResourceTypes)
], cb);
function cb([name, type]) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
createType(name);
yield fetchDocs(type.Documentation);
bluebird_1.each(lodash_1.toPairs(type.Properties), ([propName, prop]) => tslib_1.__awaiter(this, void 0, void 0, function* () {
yield fetchDocs(prop.Documentation);
}));
});
}
function createType(name) {
rootNamespace.getOrCreateChildType(name);
}
function fetchDocs(url) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const actualUrl = utils_1.normalizeUrl(url);
if (typeof docs[actualUrl] === 'string') {
return;
}
console.log(`Downloading ${url}`);
docs[actualUrl] = yield request(actualUrl);
});
}
}));
}
finally {
// Save documentation cache back to disc
utils_1.writeJsonFile(paths.docsCache, docs);
}
const allResourceTypes = [];
const renderedIdentifierPaths = new Set();
/** Generated type declaration .ts */
const declaration = utils_1.template `
import * as C from '../core';
${lodash_1.map(specs, (spec, path) => {
const specJson = utils_1.readJsonFile(`specifications/${path}`);
const allDeclarations = lodash_1.sortBy([
...lodash_1.toPairs(specJson.ResourceTypes).map(v => [...v, 'resource']),
...lodash_1.toPairs(specJson.PropertyTypes).map(v => [...v, 'property'])
], v => v[0]);
return utils_1.template `
${lodash_1.map(allDeclarations, ([k, v, type]) => renderType(v, k, type))}
`;
function renderType(v, k, specType) {
const isResource = specType === 'resource';
const nameParts = k.split(/::|\./);
const namespace = nameParts.slice(0, -1).join('.');
const identifier = nameParts[nameParts.length - 1];
const identifierPath = namespace ? `${namespace}.${identifier}` : `${identifier}`;
if (renderedIdentifierPaths.has(identifierPath)) {
return `/* already emitted ${identifierPath} */\n`;
}
renderedIdentifierPaths.add(identifierPath);
if (isResource)
allResourceTypes.push(identifierPath);
const propertiesIdentifierPath = `${identifierPath}.Properties`;
const $ = get$(v.Documentation);
// Emit `declaration` and wrap it in a namespace declaration if necessary
function declarationInNamespace(identifierPath, declaration) {
const namespace = identifierPath.split('.').slice(0, -1).join('.');
return utils_1.template `
${namespace && `export namespace ${namespace} {`}
${declaration}
${namespace && `}`}
`;
}
utils_1.log(`Generating declaration for ${identifierPath}...`);
const description = $('.topictitle+p').text();
return utils_1.template `
${isResource && declarationInNamespace(identifierPath, utils_1.template `
/**
* ${description}
*
* Documentation: ${v.Documentation}
*/
export interface ${identifier} extends C.CommonResourceProps {
Type: '${k}' = '${k}';
Properties: ${propertiesIdentifierPath}
}
/**
* ${description}
*
* Documentation: ${v.Documentation}
*/
export function ${identifier}(props: C.Omit<${identifier}, 'Type'>): ${identifier} {
return {Type: '${k}', ...props};
}
`)}
${declarationInNamespace(isResource ? `${identifierPath}.Properties` : identifierPath, utils_1.template `
export interface ${isResource ? 'Properties' : identifier} {
${lodash_1.map(v.Properties, (prop, propName) => {
const $dt = $('.variablelist>dl>dt').filter((i, e) => $(e).text() === propName);
const $dd = $dt.find('+*');
const description = $dd.find('>p').eq(0).text();
const type = $dd.find('>p>em').filter((i, e) => $(e).text() === 'Type').parent().text().slice(6);
return utils_1.template `
/**
* ${type}
*
* ${description}
*
* UpdateType: ${prop.UpdateType}
* Documentation: ${prop.Documentation}
*/
${propName}${!prop.Required && '?'}: ${renderTypeString(prop, rootNamespace.getOrCreateChildNamespace(identifierPath))};
`;
})}
}
`)}
`;
}
})}
export type Resource =\n${allResourceTypes.join('\n| ')};
`;
function parseTypeName(fullName) {
const nameParts = fullName.split(/::|\./);
let namespace = nameParts.slice(0, -1).join('.');
if (namespace === '')
namespace = undefined;
const name = nameParts[nameParts.length - 1];
return { namespace, name };
}
function renderTypeString(prop, relativeTo) {
if (prop.PrimitiveType) {
return renderPrimitiveType(prop.PrimitiveType);
}
if (prop.Type) {
if (prop.PrimitiveItemType) {
return `${renderPropertyType(prop.Type, relativeTo, [renderPrimitiveType(prop.PrimitiveItemType)])}`;
}
else if (prop.ItemType) {
return `${renderPropertyType(prop.Type, relativeTo, [renderPropertyType(prop.ItemType, relativeTo)])}`;
}
else {
return `${renderPropertyType(prop.Type, relativeTo)}`;
}
}
throw new Error('Unexpected property');
}
function renderPrimitiveType(prim) {
return `C.CF${prim}`;
}
function renderPropertyType(findName, relativeTo, generics) {
const genericsStr = generics && generics.length ? `<${generics.join(', ')}>` : '';
if (lodash_1.includes(['Map', 'List'], findName))
return `C.CF${findName}${genericsStr}`;
const ret = relativeTo.resolveType(findName).fullName();
assert(typeof ret === 'string');
return `${ret}${genericsStr}`;
}
utils_1.writeFile(paths.generatedDeclaration, utils_1.prettyPrintTs(declaration));
});
}
class Shared {
constructor(name, parent) {
this._parent = null;
this.name = null;
name != null && (this.name = name);
parent != null && (this._parent = parent);
}
fullName() {
const parentName = this._parent ? this._parent.fullName() : null;
return parentName ? `${parentName}.${this.name}` : this.name;
}
_splitPath(s) {
return s.split(/::|\./);
}
toJSON() {
return Object.assign({}, this, { _parent: undefined });
}
}
/** Represents an AWS "Type" in the CloudFormation schema (e.g. AWS::ApiGateway::Account) */
class Type extends Shared {
}
/** Represents an AWS "Namespace" in the Cloudformation schema (e.g. AWS::ApiGateway) */
class Namespace extends Shared {
constructor() {
super(...arguments);
this._namespaces = new Map();
this._types = new Map();
}
getOrCreateChildType(awsFullName) {
if (typeof awsFullName === 'string')
awsFullName = this._splitPath(awsFullName);
if (awsFullName.length === 1) {
let type = this._types.get(awsFullName[0]);
if (!type) {
type = new Type(awsFullName[0], this);
this._types.set(awsFullName[0], type);
}
return type;
}
else {
return this.getOrCreateChildNamespace(awsFullName.slice(0, -1)).getOrCreateChildType(awsFullName.slice(-1));
}
}
getType(awsFullName) {
if (typeof awsFullName === 'string')
awsFullName = this._splitPath(awsFullName);
if (awsFullName.length === 1)
return this._types.get(awsFullName[0]) || null;
const ns = this._namespaces.get(awsFullName[0]);
return ns ? ns.getType(awsFullName.slice(1)) : null;
}
getOrCreateChildNamespace(awsFullName) {
if (typeof awsFullName === 'string')
awsFullName = this._splitPath(awsFullName);
let ns = this._namespaces.get(awsFullName[0]);
if (!ns)
this._namespaces.set(awsFullName[0], ns = new Namespace(awsFullName[0], this));
if (awsFullName.length === 1)
return ns;
return ns.getOrCreateChildNamespace(awsFullName.slice(1));
}
resolveType(awsFullName) {
if (typeof awsFullName === 'string')
awsFullName = this._splitPath(awsFullName);
const type = this.getType(awsFullName);
if (type)
return type;
if (this._parent)
return this._parent.resolveType(awsFullName);
return null;
}
toJSON() {
return Object.assign({}, super.toJSON(), {
_namespaces: lodash_1.fromPairs(Array.from(this._namespaces)),
_types: lodash_1.fromPairs(Array.from(this._types))
});
}
}
main().catch(e => {
console.error(e);
process.exit(1);
});
//# sourceMappingURL=index.js.map