UNPKG

16.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.Generator = void 0;
4const spec = require("@jsii/spec");
5const clone = require("clone");
6const codemaker_1 = require("codemaker");
7const crypto = require("crypto");
8const fs = require("fs-extra");
9const path = require("path");
10const version_1 = require("./version");
11/**
12 * Abstract base class for jsii package generators.
13 * Given a jsii module, it will invoke "events" to emit various elements.
14 */
15class Generator {
16 constructor(options) {
17 this.options = options;
18 this.excludeTypes = new Array();
19 this.code = new codemaker_1.CodeMaker();
20 }
21 get runtimeTypeChecking() {
22 return this.options.runtimeTypeChecking;
23 }
24 get assembly() {
25 if (!this._assembly) {
26 throw new Error('No assembly has been loaded! The #load() method must be called first!');
27 }
28 return this._assembly;
29 }
30 get reflectAssembly() {
31 if (!this._reflectAssembly) {
32 throw new Error('Call load() first');
33 }
34 return this._reflectAssembly;
35 }
36 get metadata() {
37 return { fingerprint: this.fingerprint };
38 }
39 async load(_packageRoot, assembly) {
40 this._reflectAssembly = assembly;
41 this._assembly = assembly.spec;
42 // Including the version of jsii-pacmak in the fingerprint, as a new version may imply different code generation.
43 this.fingerprint = crypto
44 .createHash('sha256')
45 .update(version_1.VERSION_DESC)
46 .update('\0')
47 .update(this.assembly.fingerprint)
48 .digest('base64');
49 return Promise.resolve();
50 }
51 /**
52 * Runs the generator (in-memory).
53 */
54 generate(fingerprint) {
55 this.onBeginAssembly(this.assembly, fingerprint);
56 this.visit(spec.NameTree.of(this.assembly));
57 this.onEndAssembly(this.assembly, fingerprint);
58 }
59 async upToDate(_) {
60 return Promise.resolve(false);
61 }
62 /**
63 * Returns the file name of the assembly resource as it is going to be saved.
64 */
65 getAssemblyFileName() {
66 let name = this.assembly.name;
67 const parts = name.split('/');
68 if (parts.length === 1) {
69 name = parts[0];
70 }
71 else if (parts.length === 2 && parts[0].startsWith('@')) {
72 name = parts[1];
73 }
74 else {
75 throw new Error('Malformed assembly name. Expecting either <name> or @<scope>/<name>');
76 }
77 return `${name}@${this.assembly.version}.jsii.tgz`;
78 }
79 /**
80 * Saves all generated files to an output directory, creating any subdirs if needed.
81 */
82 async save(outdir, tarball, { license, notice }) {
83 const assemblyDir = this.getAssemblyOutputDir(this.assembly);
84 if (assemblyDir) {
85 const fullPath = path.resolve(path.join(outdir, assemblyDir, this.getAssemblyFileName()));
86 await fs.mkdirp(path.dirname(fullPath));
87 await fs.copy(tarball, fullPath, { overwrite: true });
88 if (license) {
89 await fs.writeFile(path.resolve(outdir, 'LICENSE'), license, {
90 encoding: 'utf8',
91 });
92 }
93 if (notice) {
94 await fs.writeFile(path.resolve(outdir, 'NOTICE'), notice, {
95 encoding: 'utf8',
96 });
97 }
98 }
99 return this.code.save(outdir);
100 }
101 //
102 // Bundled assembly
103 // jsii modules should bundle the assembly itself as a resource and use the load() kernel API to load it.
104 //
105 /**
106 * Returns the destination directory for the assembly file.
107 */
108 getAssemblyOutputDir(_mod) {
109 return undefined;
110 }
111 //
112 // Assembly
113 onBeginAssembly(_assm, _fingerprint) {
114 /* noop */
115 }
116 onEndAssembly(_assm, _fingerprint) {
117 /* noop */
118 }
119 //
120 // Namespaces
121 onBeginNamespace(_ns) {
122 /* noop */
123 }
124 onEndNamespace(_ns) {
125 /* noop */
126 }
127 //
128 // Classes
129 onBeginClass(_cls, _abstract) {
130 /* noop */
131 }
132 onEndClass(_cls) {
133 /* noop */
134 }
135 //
136 // Initializers (constructos)
137 onInitializer(_cls, _initializer) {
138 /* noop */
139 }
140 onInitializerOverload(_cls, _overload, _originalInitializer) {
141 /* noop */
142 }
143 //
144 // Properties
145 onBeginProperties(_cls) {
146 /* noop */
147 }
148 onEndProperties(_cls) {
149 /* noop */
150 }
151 onExpandedUnionProperty(_cls, _prop, _primaryName) {
152 return;
153 }
154 //
155 // Methods
156 // onMethodOverload is triggered if the option `generateOverloadsForMethodWithOptionals` is enabled for each overload of the original method.
157 // The original method will be emitted via onMethod.
158 onBeginMethods(_cls) {
159 /* noop */
160 }
161 onEndMethods(_cls) {
162 /* noop */
163 }
164 //
165 // Enums
166 onBeginEnum(_enm) {
167 /* noop */
168 }
169 onEndEnum(_enm) {
170 /* noop */
171 }
172 onEnumMember(_enm, _member) {
173 /* noop */
174 }
175 //
176 // Fields
177 // Can be used to implements properties backed by fields in cases where we want to generate "native" classes.
178 // The default behavior is that properties do not have backing fields.
179 hasField(_cls, _prop) {
180 return false;
181 }
182 onField(_cls, _prop, _union) {
183 /* noop */
184 }
185 visit(node, names = new Array()) {
186 const namespace = !node.fqn && names.length > 0 ? names.join('.') : undefined;
187 if (namespace) {
188 this.onBeginNamespace(namespace);
189 }
190 const visitChildren = () => {
191 Object.keys(node.children)
192 .sort()
193 .forEach((name) => {
194 this.visit(node.children[name], names.concat(name));
195 });
196 };
197 if (node.fqn) {
198 const type = this.assembly.types?.[node.fqn];
199 if (!type) {
200 throw new Error(`Malformed jsii file. Cannot find type: ${node.fqn}`);
201 }
202 if (!this.shouldExcludeType(type.name)) {
203 switch (type.kind) {
204 case spec.TypeKind.Class:
205 const classSpec = type;
206 const abstract = classSpec.abstract;
207 if (abstract && this.options.addBasePostfixToAbstractClassNames) {
208 this.addAbstractPostfixToClassName(classSpec);
209 }
210 this.onBeginClass(classSpec, abstract);
211 this.visitClass(classSpec);
212 visitChildren();
213 this.onEndClass(classSpec);
214 break;
215 case spec.TypeKind.Enum:
216 const enumSpec = type;
217 this.onBeginEnum(enumSpec);
218 this.visitEnum(enumSpec);
219 visitChildren();
220 this.onEndEnum(enumSpec);
221 break;
222 case spec.TypeKind.Interface:
223 const interfaceSpec = type;
224 this.onBeginInterface(interfaceSpec);
225 this.visitInterface(interfaceSpec);
226 visitChildren();
227 this.onEndInterface(interfaceSpec);
228 break;
229 default:
230 throw new Error(`Unsupported type kind: ${type.kind}`);
231 }
232 }
233 }
234 else {
235 visitChildren();
236 }
237 if (namespace) {
238 this.onEndNamespace(namespace);
239 }
240 }
241 /**
242 * Adds a postfix ("XxxBase") to the class name to indicate it is abstract.
243 */
244 addAbstractPostfixToClassName(cls) {
245 cls.name = `${cls.name}Base`;
246 const components = cls.fqn.split('.');
247 cls.fqn = components
248 .map((x, i) => (i < components.length - 1 ? x : `${x}Base`))
249 .join('.');
250 }
251 excludeType(...names) {
252 for (const n of names) {
253 this.excludeTypes.push(n);
254 }
255 }
256 shouldExcludeType(name) {
257 return this.excludeTypes.includes(name);
258 }
259 /**
260 * Returns all the method overloads needed to satisfy optional arguments.
261 * For example, for the method `foo(bar: string, hello?: number, world?: number)`
262 * this method will return:
263 * - foo(bar: string)
264 * - foo(bar: string, hello: number)
265 *
266 * Notice that the method that contains all the arguments will not be returned.
267 */
268 createOverloadsForOptionals(method) {
269 const overloads = new Array();
270 // if option disabled, just return the empty array.
271 if (!this.options.generateOverloadsForMethodWithOptionals ||
272 !method.parameters) {
273 return overloads;
274 }
275 //
276 // pop an argument from the end of the parameter list.
277 // if it is an optional argument, clone the method without that parameter.
278 // continue until we reach a non optional param or no parameters left.
279 //
280 const remaining = clone(method.parameters);
281 let next;
282 next = remaining.pop();
283 // Parameter is optional if it's type is optional, and all subsequent parameters are optional/variadic
284 while (next?.optional) {
285 // clone the method but set the parameter list based on the remaining set of parameters
286 const cloned = clone(method);
287 cloned.parameters = clone(remaining);
288 overloads.push(cloned);
289 // pop the next parameter
290 next = remaining.pop();
291 }
292 return overloads;
293 }
294 visitInterface(ifc) {
295 if (ifc.properties) {
296 ifc.properties.forEach((prop) => {
297 this.onInterfaceProperty(ifc, prop);
298 });
299 }
300 if (ifc.methods) {
301 ifc.methods.forEach((method) => {
302 this.onInterfaceMethod(ifc, method);
303 for (const overload of this.createOverloadsForOptionals(method)) {
304 this.onInterfaceMethodOverload(ifc, overload, method);
305 }
306 });
307 }
308 }
309 visitClass(cls) {
310 const initializer = cls.initializer;
311 if (initializer) {
312 this.onInitializer(cls, initializer);
313 // if method has optional arguments and
314 for (const overload of this.createOverloadsForOptionals(initializer)) {
315 this.onInitializerOverload(cls, overload, initializer);
316 }
317 }
318 // if running in 'pure' mode and the class has methods, emit them as abstract methods.
319 if (cls.methods) {
320 this.onBeginMethods(cls);
321 cls.methods.forEach((method) => {
322 if (!method.static) {
323 this.onMethod(cls, method);
324 for (const overload of this.createOverloadsForOptionals(method)) {
325 this.onMethodOverload(cls, overload, method);
326 }
327 }
328 else {
329 this.onStaticMethod(cls, method);
330 for (const overload of this.createOverloadsForOptionals(method)) {
331 this.onStaticMethodOverload(cls, overload, method);
332 }
333 }
334 });
335 this.onEndMethods(cls);
336 }
337 if (cls.properties) {
338 this.onBeginProperties(cls);
339 cls.properties.forEach((prop) => {
340 if (this.hasField(cls, prop)) {
341 this.onField(cls, prop, spec.isUnionTypeReference(prop.type) ? prop.type : undefined);
342 }
343 });
344 cls.properties.forEach((prop) => {
345 if (!spec.isUnionTypeReference(prop.type)) {
346 if (!prop.static) {
347 this.onProperty(cls, prop);
348 }
349 else {
350 this.onStaticProperty(cls, prop);
351 }
352 }
353 else {
354 // okay, this is a union. some languages support unions (mostly the dynamic ones) and some will need some help
355 // if `expandUnionProperties` is set, we will "expand" each property that has a union type into multiple properties
356 // and postfix their name with the type name (i.e. FooAsToken).
357 // first, emit a property for the union, for languages that support unions.
358 this.onUnionProperty(cls, prop, prop.type);
359 // if require, we also "expand" the union for languages that don't support unions.
360 if (this.options.expandUnionProperties) {
361 for (const [index, type] of prop.type.union.types.entries()) {
362 // create a clone of this property
363 const propClone = clone(prop);
364 const primary = this.isPrimaryExpandedUnionProperty(prop.type, index);
365 const propertyName = primary
366 ? prop.name
367 : `${prop.name}As${this.displayNameForType(type)}`;
368 propClone.type = type;
369 propClone.optional = prop.optional;
370 propClone.name = propertyName;
371 this.onExpandedUnionProperty(cls, propClone, prop.name);
372 }
373 }
374 }
375 });
376 this.onEndProperties(cls);
377 }
378 }
379 /**
380 * Magical heuristic to determine which type in a union is the primary type. The primary type will not have
381 * a postfix with the name of the type attached to the expanded property name.
382 *
383 * The primary type is determined according to the following rules (first match):
384 * 1. The first primitive type
385 * 2. The first primitive collection
386 * 3. No primary
387 */
388 isPrimaryExpandedUnionProperty(ref, index) {
389 if (!ref) {
390 return false;
391 }
392 return (index ===
393 ref.union.types.findIndex((t) => {
394 if (spec.isPrimitiveTypeReference(t)) {
395 return true;
396 }
397 return false;
398 }));
399 }
400 visitEnum(enumSpec) {
401 if (enumSpec.members) {
402 enumSpec.members.forEach((spec) => this.onEnumMember(enumSpec, spec));
403 }
404 }
405 displayNameForType(type) {
406 // last name from FQN
407 if (spec.isNamedTypeReference(type)) {
408 const comps = type.fqn.split('.');
409 const last = comps[comps.length - 1];
410 return this.code.toPascalCase(last);
411 }
412 // primitive name
413 if (spec.isPrimitiveTypeReference(type)) {
414 return this.code.toPascalCase(type.primitive);
415 }
416 // ListOfX or MapOfX
417 const coll = spec.isCollectionTypeReference(type) && type.collection;
418 if (coll) {
419 return `${this.code.toPascalCase(coll.kind)}Of${this.displayNameForType(coll.elementtype)}`;
420 }
421 const union = spec.isUnionTypeReference(type) && type.union;
422 if (union) {
423 return union.types.map((t) => this.displayNameForType(t)).join('Or');
424 }
425 throw new Error(`Cannot determine display name for type: ${JSON.stringify(type)}`);
426 }
427 /**
428 * Looks up a jsii module in the dependency tree.
429 * @param name The name of the jsii module to look up
430 */
431 findModule(name) {
432 // if this is the current module, return it
433 if (this.assembly.name === name) {
434 return this.assembly;
435 }
436 const found = (this.assembly.dependencyClosure ?? {})[name];
437 if (found) {
438 return found;
439 }
440 throw new Error(`Unable to find module ${name} as a dependency of ${this.assembly.name}`);
441 }
442 findType(fqn) {
443 const ret = this.reflectAssembly.system.tryFindFqn(fqn);
444 if (!ret) {
445 throw new Error(`Cannot find type '${fqn}' either as internal or external type`);
446 }
447 return ret.spec;
448 }
449}
450exports.Generator = Generator;
451//# sourceMappingURL=generator.js.map
\No newline at end of file