jsii-pacmak
Version:
A code generation framework for jsii backend languages
451 lines • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Generator = void 0;
const spec = require("@jsii/spec");
const clone = require("clone");
const codemaker_1 = require("codemaker");
const crypto = require("crypto");
const fs = require("fs-extra");
const path = require("path");
const version_1 = require("./version");
/**
* Abstract base class for jsii package generators.
* Given a jsii module, it will invoke "events" to emit various elements.
*/
class Generator {
constructor(options) {
this.options = options;
this.excludeTypes = new Array();
this.code = new codemaker_1.CodeMaker();
}
get runtimeTypeChecking() {
return this.options.runtimeTypeChecking;
}
get assembly() {
if (!this._assembly) {
throw new Error('No assembly has been loaded! The #load() method must be called first!');
}
return this._assembly;
}
get reflectAssembly() {
if (!this._reflectAssembly) {
throw new Error('Call load() first');
}
return this._reflectAssembly;
}
get metadata() {
return { fingerprint: this.fingerprint };
}
async load(_packageRoot, assembly) {
this._reflectAssembly = assembly;
this._assembly = assembly.spec;
// Including the version of jsii-pacmak in the fingerprint, as a new version may imply different code generation.
this.fingerprint = crypto
.createHash('sha256')
.update(version_1.VERSION_DESC)
.update('\0')
.update(this.assembly.fingerprint)
.digest('base64');
return Promise.resolve();
}
/**
* Runs the generator (in-memory).
*/
generate(fingerprint) {
this.onBeginAssembly(this.assembly, fingerprint);
this.visit(spec.NameTree.of(this.assembly));
this.onEndAssembly(this.assembly, fingerprint);
}
async upToDate(_) {
return Promise.resolve(false);
}
/**
* Returns the file name of the assembly resource as it is going to be saved.
*/
getAssemblyFileName() {
let name = this.assembly.name;
const parts = name.split('/');
if (parts.length === 1) {
name = parts[0];
}
else if (parts.length === 2 && parts[0].startsWith('@')) {
name = parts[1];
}
else {
throw new Error('Malformed assembly name. Expecting either <name> or @<scope>/<name>');
}
return `${name}@${this.assembly.version}.jsii.tgz`;
}
/**
* Saves all generated files to an output directory, creating any subdirs if needed.
*/
async save(outdir, tarball, { license, notice }) {
const assemblyDir = this.getAssemblyOutputDir(this.assembly);
if (assemblyDir) {
const fullPath = path.resolve(path.join(outdir, assemblyDir, this.getAssemblyFileName()));
await fs.mkdirp(path.dirname(fullPath));
await fs.copy(tarball, fullPath, { overwrite: true });
if (license) {
await fs.writeFile(path.resolve(outdir, 'LICENSE'), license, {
encoding: 'utf8',
});
}
if (notice) {
await fs.writeFile(path.resolve(outdir, 'NOTICE'), notice, {
encoding: 'utf8',
});
}
}
return this.code.save(outdir);
}
//
// Bundled assembly
// jsii modules should bundle the assembly itself as a resource and use the load() kernel API to load it.
//
/**
* Returns the destination directory for the assembly file.
*/
getAssemblyOutputDir(_mod) {
return undefined;
}
//
// Assembly
onBeginAssembly(_assm, _fingerprint) {
/* noop */
}
onEndAssembly(_assm, _fingerprint) {
/* noop */
}
//
// Namespaces
onBeginNamespace(_ns) {
/* noop */
}
onEndNamespace(_ns) {
/* noop */
}
//
// Classes
onBeginClass(_cls, _abstract) {
/* noop */
}
onEndClass(_cls) {
/* noop */
}
//
// Initializers (constructos)
onInitializer(_cls, _initializer) {
/* noop */
}
onInitializerOverload(_cls, _overload, _originalInitializer) {
/* noop */
}
//
// Properties
onBeginProperties(_cls) {
/* noop */
}
onEndProperties(_cls) {
/* noop */
}
onExpandedUnionProperty(_cls, _prop, _primaryName) {
return;
}
//
// Methods
// onMethodOverload is triggered if the option `generateOverloadsForMethodWithOptionals` is enabled for each overload of the original method.
// The original method will be emitted via onMethod.
onBeginMethods(_cls) {
/* noop */
}
onEndMethods(_cls) {
/* noop */
}
//
// Enums
onBeginEnum(_enm) {
/* noop */
}
onEndEnum(_enm) {
/* noop */
}
onEnumMember(_enm, _member) {
/* noop */
}
//
// Fields
// Can be used to implements properties backed by fields in cases where we want to generate "native" classes.
// The default behavior is that properties do not have backing fields.
hasField(_cls, _prop) {
return false;
}
onField(_cls, _prop, _union) {
/* noop */
}
visit(node, names = new Array()) {
const namespace = !node.fqn && names.length > 0 ? names.join('.') : undefined;
if (namespace) {
this.onBeginNamespace(namespace);
}
const visitChildren = () => {
Object.keys(node.children)
.sort()
.forEach((name) => {
this.visit(node.children[name], names.concat(name));
});
};
if (node.fqn) {
const type = this.assembly.types?.[node.fqn];
if (!type) {
throw new Error(`Malformed jsii file. Cannot find type: ${node.fqn}`);
}
if (!this.shouldExcludeType(type.name)) {
switch (type.kind) {
case spec.TypeKind.Class:
const classSpec = type;
const abstract = classSpec.abstract;
if (abstract && this.options.addBasePostfixToAbstractClassNames) {
this.addAbstractPostfixToClassName(classSpec);
}
this.onBeginClass(classSpec, abstract);
this.visitClass(classSpec);
visitChildren();
this.onEndClass(classSpec);
break;
case spec.TypeKind.Enum:
const enumSpec = type;
this.onBeginEnum(enumSpec);
this.visitEnum(enumSpec);
visitChildren();
this.onEndEnum(enumSpec);
break;
case spec.TypeKind.Interface:
const interfaceSpec = type;
this.onBeginInterface(interfaceSpec);
this.visitInterface(interfaceSpec);
visitChildren();
this.onEndInterface(interfaceSpec);
break;
default:
throw new Error(`Unsupported type kind: ${type.kind}`);
}
}
}
else {
visitChildren();
}
if (namespace) {
this.onEndNamespace(namespace);
}
}
/**
* Adds a postfix ("XxxBase") to the class name to indicate it is abstract.
*/
addAbstractPostfixToClassName(cls) {
cls.name = `${cls.name}Base`;
const components = cls.fqn.split('.');
cls.fqn = components
.map((x, i) => (i < components.length - 1 ? x : `${x}Base`))
.join('.');
}
excludeType(...names) {
for (const n of names) {
this.excludeTypes.push(n);
}
}
shouldExcludeType(name) {
return this.excludeTypes.includes(name);
}
/**
* Returns all the method overloads needed to satisfy optional arguments.
* For example, for the method `foo(bar: string, hello?: number, world?: number)`
* this method will return:
* - foo(bar: string)
* - foo(bar: string, hello: number)
*
* Notice that the method that contains all the arguments will not be returned.
*/
createOverloadsForOptionals(method) {
const overloads = new Array();
// if option disabled, just return the empty array.
if (!this.options.generateOverloadsForMethodWithOptionals ||
!method.parameters) {
return overloads;
}
//
// pop an argument from the end of the parameter list.
// if it is an optional argument, clone the method without that parameter.
// continue until we reach a non optional param or no parameters left.
//
const remaining = clone(method.parameters);
let next;
next = remaining.pop();
// Parameter is optional if it's type is optional, and all subsequent parameters are optional/variadic
while (next?.optional) {
// clone the method but set the parameter list based on the remaining set of parameters
const cloned = clone(method);
cloned.parameters = clone(remaining);
overloads.push(cloned);
// pop the next parameter
next = remaining.pop();
}
return overloads;
}
visitInterface(ifc) {
if (ifc.properties) {
ifc.properties.forEach((prop) => {
this.onInterfaceProperty(ifc, prop);
});
}
if (ifc.methods) {
ifc.methods.forEach((method) => {
this.onInterfaceMethod(ifc, method);
for (const overload of this.createOverloadsForOptionals(method)) {
this.onInterfaceMethodOverload(ifc, overload, method);
}
});
}
}
visitClass(cls) {
const initializer = cls.initializer;
if (initializer) {
this.onInitializer(cls, initializer);
// if method has optional arguments and
for (const overload of this.createOverloadsForOptionals(initializer)) {
this.onInitializerOverload(cls, overload, initializer);
}
}
// if running in 'pure' mode and the class has methods, emit them as abstract methods.
if (cls.methods) {
this.onBeginMethods(cls);
cls.methods.forEach((method) => {
if (!method.static) {
this.onMethod(cls, method);
for (const overload of this.createOverloadsForOptionals(method)) {
this.onMethodOverload(cls, overload, method);
}
}
else {
this.onStaticMethod(cls, method);
for (const overload of this.createOverloadsForOptionals(method)) {
this.onStaticMethodOverload(cls, overload, method);
}
}
});
this.onEndMethods(cls);
}
if (cls.properties) {
this.onBeginProperties(cls);
cls.properties.forEach((prop) => {
if (this.hasField(cls, prop)) {
this.onField(cls, prop, spec.isUnionTypeReference(prop.type) ? prop.type : undefined);
}
});
cls.properties.forEach((prop) => {
if (!spec.isUnionTypeReference(prop.type)) {
if (!prop.static) {
this.onProperty(cls, prop);
}
else {
this.onStaticProperty(cls, prop);
}
}
else {
// okay, this is a union. some languages support unions (mostly the dynamic ones) and some will need some help
// if `expandUnionProperties` is set, we will "expand" each property that has a union type into multiple properties
// and postfix their name with the type name (i.e. FooAsToken).
// first, emit a property for the union, for languages that support unions.
this.onUnionProperty(cls, prop, prop.type);
// if require, we also "expand" the union for languages that don't support unions.
if (this.options.expandUnionProperties) {
for (const [index, type] of prop.type.union.types.entries()) {
// create a clone of this property
const propClone = clone(prop);
const primary = this.isPrimaryExpandedUnionProperty(prop.type, index);
const propertyName = primary
? prop.name
: `${prop.name}As${this.displayNameForType(type)}`;
propClone.type = type;
propClone.optional = prop.optional;
propClone.name = propertyName;
this.onExpandedUnionProperty(cls, propClone, prop.name);
}
}
}
});
this.onEndProperties(cls);
}
}
/**
* Magical heuristic to determine which type in a union is the primary type. The primary type will not have
* a postfix with the name of the type attached to the expanded property name.
*
* The primary type is determined according to the following rules (first match):
* 1. The first primitive type
* 2. The first primitive collection
* 3. No primary
*/
isPrimaryExpandedUnionProperty(ref, index) {
if (!ref) {
return false;
}
return (index ===
ref.union.types.findIndex((t) => {
if (spec.isPrimitiveTypeReference(t)) {
return true;
}
return false;
}));
}
visitEnum(enumSpec) {
if (enumSpec.members) {
enumSpec.members.forEach((spec) => this.onEnumMember(enumSpec, spec));
}
}
displayNameForType(type) {
// last name from FQN
if (spec.isNamedTypeReference(type)) {
const comps = type.fqn.split('.');
const last = comps[comps.length - 1];
return this.code.toPascalCase(last);
}
// primitive name
if (spec.isPrimitiveTypeReference(type)) {
return this.code.toPascalCase(type.primitive);
}
// ListOfX or MapOfX
const coll = spec.isCollectionTypeReference(type) && type.collection;
if (coll) {
return `${this.code.toPascalCase(coll.kind)}Of${this.displayNameForType(coll.elementtype)}`;
}
const union = spec.isUnionTypeReference(type) && type.union;
if (union) {
return union.types.map((t) => this.displayNameForType(t)).join('Or');
}
throw new Error(`Cannot determine display name for type: ${JSON.stringify(type)}`);
}
/**
* Looks up a jsii module in the dependency tree.
* @param name The name of the jsii module to look up
*/
findModule(name) {
// if this is the current module, return it
if (this.assembly.name === name) {
return this.assembly;
}
const found = (this.assembly.dependencyClosure ?? {})[name];
if (found) {
return found;
}
throw new Error(`Unable to find module ${name} as a dependency of ${this.assembly.name}`);
}
findType(fqn) {
const ret = this.reflectAssembly.system.tryFindFqn(fqn);
if (!ret) {
throw new Error(`Cannot find type '${fqn}' either as internal or external type`);
}
return ret.spec;
}
}
exports.Generator = Generator;
//# sourceMappingURL=generator.js.map