UNPKG

jsii-reflect

Version:

strongly-typed reflection library and tools for jsii

331 lines 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeSystem = exports.JSII_REFLECT_SUPPORTED_ASSEMBLY_FEATURES = void 0; const spec_1 = require("@jsii/spec"); const fs = require("fs-extra"); const path = require("path"); const assembly_1 = require("./assembly"); const class_1 = require("./class"); const enum_1 = require("./enum"); const interface_1 = require("./interface"); const util_1 = require("./util"); /** * The jsii features that supported (in principle) by jsii-reflect * * The features claimed by the user of jsii-reflect must be a subset of these. */ exports.JSII_REFLECT_SUPPORTED_ASSEMBLY_FEATURES = [ 'intersection-types', 'class-covariant-overrides', ]; class TypeSystem { constructor() { /** * The "root" assemblies (ones that loaded explicitly via a "load" call). */ this.roots = new Array(); this._assemblyLookup = new Map(); this._cachedClasses = new Map(); this._locked = false; } get isLocked() { return this._locked; } /** * All assemblies in this type system. */ get assemblies() { return Array.from(this._assemblyLookup.values()); } /** * Locks the TypeSystem from further changes * * Call this once all assemblies have been loaded. * This allows the reflection to optimize and cache certain expensive calls. */ lock() { this._locked = true; } /** * Load all JSII dependencies of the given NPM package directory. * * The NPM package itself does *not* have to be a jsii package, and does * NOT have to declare a JSII dependency on any of the packages. */ async loadNpmDependencies(packageRoot, options = {}) { const pkg = await fs.readJson(path.resolve(packageRoot, 'package.json')); for (const dep of dependenciesOf(pkg)) { if ((0, util_1.isBuiltinModule)(dep)) { continue; } // eslint-disable-next-line no-await-in-loop const depDir = await (0, util_1.findDependencyDirectory)(dep, packageRoot); // eslint-disable-next-line no-await-in-loop const depPkgJson = await fs.readJson(path.join(depDir, 'package.json')); if (!depPkgJson.jsii) { continue; } // eslint-disable-next-line no-await-in-loop await this.loadModule(depDir, options); } } /** * Loads a jsii module or a single .jsii file into the type system. * * If `fileOrDirectory` is a directory, it will be treated as a jsii npm module, * and its dependencies (as determined by its 'package.json' file) will be loaded * as well. * * If `fileOrDirectory` is a file, it will be treated as a single .jsii file. * No dependencies will be loaded. You almost never want this. * * Not validating makes the difference between loading assemblies with lots * of dependencies (such as app-delivery) in 90ms vs 3500ms. * * @param fileOrDirectory A .jsii file path or a module directory * @param validate Whether or not to validate the assembly while loading it. */ async load(fileOrDirectory, options = {}) { if ((await fs.stat(fileOrDirectory)).isDirectory()) { return this.loadModule(fileOrDirectory, options); } return this.loadFile(fileOrDirectory, { ...options, isRoot: true, }); } async loadModule(dir, options = {}) { const out = await _loadModule.call(this, dir, true); if (!out) { throw new Error(`Unable to load module from directory: ${dir}`); } return out; async function _loadModule(moduleDirectory, isRoot = false) { const filePath = path.join(moduleDirectory, 'package.json'); const pkg = JSON.parse(await fs.readFile(filePath, { encoding: 'utf-8' })); if (!pkg.jsii) { throw new Error(`No "jsii" section in ${filePath}`); } // Load the assembly, but don't recurse if we already have an assembly with the same name. // Validation is not an insignificant time sink, and loading IS insignificant, so do a // load without validation first. This saves about 2/3rds of processing time. const asm = this.loadAssembly((0, spec_1.findAssemblyFile)(moduleDirectory), false, options.supportedFeatures); if (this.includesAssembly(asm.name)) { const existing = this.findAssembly(asm.name); if (existing.version !== asm.version) { throw new Error(`Conflicting versions of ${asm.name} in type system: previously loaded ${existing.version}, trying to load ${asm.version}`); } // Make sure that we mark this thing as root after all if it wasn't yet. if (isRoot) { this.addRoot(asm); } return existing; } if (options.validate !== false) { asm.validate(); } const root = this.addAssembly(asm, { isRoot }); // Using || instead of ?? because npmjs.com will alter the package.json file and possibly put `false` in pkg.bundleDependencies. // This is actually non compliant to the package.json specification, but that's how it is... const bundled = pkg.bundledDependencies ?? pkg.bundleDependencies ?? []; for (const name of dependenciesOf(pkg)) { if (bundled.includes(name)) { continue; } // eslint-disable-next-line no-await-in-loop const depDir = await (0, util_1.findDependencyDirectory)(name, moduleDirectory); // eslint-disable-next-line no-await-in-loop await _loadModule.call(this, depDir); } return root; } } loadFile(file, options = {}) { const assembly = this.loadAssembly(file, options.validate !== false, options.supportedFeatures); return this.addAssembly(assembly, options); } addAssembly(asm, options = {}) { if (this.isLocked) { throw new Error('The typesystem has been locked from further changes'); } if (asm.system !== this) { throw new Error('Assembly has been created for different typesystem'); } if (!this._assemblyLookup.has(asm.name)) { this._assemblyLookup.set(asm.name, asm); } if (options.isRoot !== false) { this.addRoot(asm); } return asm; } /** * Determines whether this TypeSystem includes a given assembly. * * @param name the name of the assembly being looked for. */ includesAssembly(name) { return this._assemblyLookup.has(name); } isRoot(name) { return this.roots.map((r) => r.name).includes(name); } findAssembly(name) { const ret = this.tryFindAssembly(name); if (!ret) { throw new Error(`Assembly "${name}" not found`); } return ret; } tryFindAssembly(name) { return this._assemblyLookup.get(name); } findFqn(fqn) { const [assembly] = fqn.split('.'); const asm = this.findAssembly(assembly); return asm.findType(fqn); } tryFindFqn(fqn) { const [assembly] = fqn.split('.'); const asm = this.tryFindAssembly(assembly); return asm?.tryFindType(fqn); } findClass(fqn) { const type = this.findFqn(fqn); if (!(type instanceof class_1.ClassType)) { throw new Error(`FQN ${fqn} is not a class`); } return type; } findInterface(fqn) { const type = this.findFqn(fqn); if (!(type instanceof interface_1.InterfaceType)) { throw new Error(`FQN ${fqn} is not an interface`); } return type; } findEnum(fqn) { const type = this.findFqn(fqn); if (!(type instanceof enum_1.EnumType)) { throw new Error(`FQN ${fqn} is not an enum`); } return type; } /** * All methods in the type system. */ get methods() { const getMethods = (mod) => { return [ ...flatMap(mod.submodules, getMethods), ...flatMap(mod.interfaces, (iface) => iface.ownMethods), ...flatMap(mod.classes, (clazz) => clazz.ownMethods), ]; }; return flatMap(this.assemblies, getMethods); } /** * All properties in the type system. */ get properties() { const getProperties = (mod) => { return [ ...flatMap(mod.submodules, getProperties), ...flatMap(mod.interfaces, (iface) => iface.ownProperties), ...flatMap(mod.classes, (clazz) => clazz.ownProperties), ]; }; return flatMap(this.assemblies, getProperties); } /** * All classes in the type system. */ get classes() { const out = new Array(); this.assemblies.forEach((a) => { // Cache the class list for each assembly. We can't use @memoized for this method since new // assemblies can be added between calls, via loadModule(). if (!this._cachedClasses.has(a)) { this._cachedClasses.set(a, collectTypes(a, (item) => item.classes)); } out.push(...this._cachedClasses.get(a)); }); return out; } /** * All interfaces in the type system. */ get interfaces() { const out = new Array(); this.assemblies.forEach((a) => { out.push(...collectTypes(a, (item) => item.interfaces)); }); return out; } /** * All enums in the type system. */ get enums() { const out = new Array(); this.assemblies.forEach((a) => { out.push(...collectTypes(a, (item) => item.enums)); }); return out; } /** * Load an assembly without adding it to the typesystem * @param file Assembly file to load * @param validate Whether to validate the assembly or just assume it matches the schema */ loadAssembly(file, validate = true, supportedFeatures) { validateFeatureSubset(supportedFeatures); const contents = (0, spec_1.loadAssemblyFromFile)(file, validate, supportedFeatures); const pjFile = path.join(path.dirname(file), 'package.json'); let pjData = {}; try { pjData = JSON.parse(fs.readFileSync(pjFile, 'utf-8')); } catch (e) { // Opportunistically it's not a failure if the file doesn't exist if (e.code !== 'ENOENT') { throw new Error(`Error loading ${pjFile}: ${e}`); } } return new assembly_1.Assembly(this, contents, path.dirname(file), pjData); } addRoot(asm) { if (!this.roots.some((r) => r.name === asm.name)) { this.roots.push(asm); } } } exports.TypeSystem = TypeSystem; function dependenciesOf(packageJson) { const deps = new Set(); Object.keys(packageJson.dependencies ?? {}).forEach(deps.add.bind(deps)); Object.keys(packageJson.peerDependencies ?? {}).forEach(deps.add.bind(deps)); return Array.from(deps); } function collectTypes(module, getter) { const result = new Array(); for (const submodule of module.submodules) { result.push(...collectTypes(submodule, getter)); } result.push(...getter(module)); return result; } function flatMap(collection, mapper) { return collection .map(mapper) .reduce((acc, elt) => acc.concat(elt), new Array()); } /** * Check that all requested features are a subset of the features that jsii-reflect itself supports */ function validateFeatureSubset(fs) { const unsupported = (fs ?? []).filter((f) => !exports.JSII_REFLECT_SUPPORTED_ASSEMBLY_FEATURES.includes(f)); if (unsupported.length > 0) { throw new Error(`This version of jsii-reflect does not support the requested features: ${unsupported.join(',')}`); } } //# sourceMappingURL=type-system.js.map