UNPKG

@ts-for-gir/cli

Version:

TypeScript type definition generator for GObject introspection GIR files

465 lines 19.1 kB
/** * The ModuleLoader is used for reading gir modules from the file system and to solve conflicts (e.g. Gtk-3.0 and Gtk-4.0 would be a conflict) */ import { select } from '@inquirer/prompts'; import { glob } from 'glob'; import { basename, join } from 'path'; import { bold } from 'colorette'; import { DependencyManager, ResolveType, GirModule, Logger, splitModuleName, union, isIterable, WARN_NO_GIR_FILE_FOUND_FOR_PACKAGE, } from '@ts-for-gir/lib'; import { Config } from './config.js'; export class ModuleLoader { config; log; dependencyManager; /** Transitive module dependencies */ modDependencyMap = {}; constructor(config) { this.config = config; this.log = new Logger(config.verbose, 'ModuleLoader'); this.dependencyManager = DependencyManager.getInstance(config); } /** * Groups Gir modules by name id * E.g. Gtk-3.0 and Gtk-4.0 will be grouped * @param girFiles */ groupGirFiles(resolveGirModules) { const girModulesGrouped = {}; for (const resolveGirModule of resolveGirModules) { const { namespace } = splitModuleName(resolveGirModule.packageName); const id = namespace.toLowerCase(); if (!girModulesGrouped[id]) { girModulesGrouped[id] = { namespace: namespace, modules: [resolveGirModule], hasConflict: false, }; } else { girModulesGrouped[id].modules.push(resolveGirModule); girModulesGrouped[id].hasConflict = true; } } return girModulesGrouped; } /** * Sorts out the module the user has not selected via cli prompt * @param girModulesGrouped * @param selected Users selected module packageName */ sortVersionsByAnswer(girModulesGrouped, selected) { const keep = new Set(); let ignore = []; if (!girModulesGrouped.hasConflict) { keep.add(girModulesGrouped.modules[0]); } else { const keepModules = this.findGirModuleByFullNames(girModulesGrouped.modules, selected); const girModulePackageNames = girModulesGrouped.modules.map((resolveGirModule) => resolveGirModule.packageName); if (!keepModules || keepModules.length <= 0) { throw new Error('Module not found!'); } for (const keepModule of keepModules) { keep.add(keepModule); } const toIgnore = girModulePackageNames.filter((packageName) => !selected.includes(packageName)); ignore = ignore.concat(toIgnore); } return { keep, ignore, }; } generateContinueQuestion(message = `do you want to continue?`, choices = ['Yes', 'Go back']) { return { message, choices, }; } generateIgnoreDepsQuestion(message = `Do you want to ignore them too?`, choices = ['Yes', 'No', 'Go back']) { return { message, choices, }; } async askIgnoreDepsPrompt(deps) { const size = deps.length || deps.size || 0; if (size > 0) { // Show dependencies that would be ignored this.log.log(bold('\nThe following modules have the ignored modules as dependencies:')); for (const dep of deps) { this.log.log(`- ${dep.packageName}`); } this.log.log(bold('\n')); // Ask if user wants to ignore these dependencies return select({ message: 'Do you want to ignore them too?', choices: [ { value: 'Yes', name: 'Yes' }, { value: 'No', name: 'No' }, { value: 'Go back', name: 'Go back' }, ], }); } // No dependencies found this.log.log(bold('\nNo dependencies found on the ignored modules')); return select({ message: 'Do you want to continue?', choices: [ { value: 'Yes', name: 'Yes' }, { value: 'Go back', name: 'Go back' }, ], }); } /** * Ask for duplicates / multiple versions of a module * @param girModuleGrouped * @param message */ generateModuleVersionQuestion(girModuleGrouped, message) { message = message || `Multiple versions of '${girModuleGrouped.namespace}' found, which one do you want to use?`; const choices = ['All', ...girModuleGrouped.modules.map((module) => module.packageName)]; return { name: girModuleGrouped.namespace, message, type: 'list', choices, }; } /** * Find modules that depend on the module with the name 'packageName' * @param girModulesGroupedMap * @param packageName */ findGirFilesDependOnPackage(girModulesGroupedMap, packageName) { const girModules = []; for (const girModulesGrouped of Object.values(girModulesGroupedMap)) { for (const girModuleResolvedBy of girModulesGrouped.modules) { if (girModuleResolvedBy.packageName === packageName) { continue; } for (const dep of girModuleResolvedBy.module.dependencies) { if (dep.packageName === packageName && !girModules.includes(girModuleResolvedBy)) { girModules.push(girModuleResolvedBy); } } } } return girModules; } /** * Find modules that depend on the module with the names in `packageNames` * @param girModulesGroupedMap * @param packageName */ findGirFilesDependOnPackages(girModulesGroupedMap, packageNames) { let girModules = []; for (const packageName of packageNames) { girModules = [...girModules, ...this.findGirFilesDependOnPackage(girModulesGroupedMap, packageName)]; } return girModules; } async askForVersionsPrompt(girModulesGrouped) { const choices = ['All', ...girModulesGrouped.modules.map((module) => module.packageName)]; const selected = await select({ message: `Multiple versions of '${girModulesGrouped.namespace}' found, which one do you want to use?`, choices: choices.map((choice) => ({ value: choice, name: choice, })), }); if (selected === 'All') { return { selected: choices.filter((choice) => choice !== 'All'), unselected: [], }; } return { selected: [selected], unselected: choices.filter((choice) => choice !== selected && choice !== 'All'), }; } /** * If multiple versions of the same module are found, this will aks the user with input prompts for the version he wish to use. * Ignores also modules that depend on a module that should be ignored * @param resolveFirModules */ async askForEachConflictVersionsPrompt(girModulesGroupedMap, ignore) { let keep = new Set(); for (const girModulesGrouped of Object.values(girModulesGroupedMap)) { // Remove ignored modules from group girModulesGrouped.modules = girModulesGrouped.modules.filter((girGroup) => !ignore.includes(girGroup.packageName)); girModulesGrouped.hasConflict = girModulesGrouped.modules.length >= 2; if (girModulesGrouped.modules.length <= 0) { continue; } // Ask for version if there is a conflict if (!girModulesGrouped.hasConflict) { keep = union(keep, girModulesGrouped.modules); } else { let goBack = true; let versionAnswer = null; let ignoreDepsAnswer = null; let wouldIgnoreDeps = []; while (goBack) { versionAnswer = await this.askForVersionsPrompt(girModulesGrouped); // Check modules that depend on the unchosen modules wouldIgnoreDeps = this.findGirFilesDependOnPackages(girModulesGroupedMap, versionAnswer.unselected); // Do not check dependencies that have already been ignored wouldIgnoreDeps = wouldIgnoreDeps.filter((dep) => !ignore.includes(dep.packageName)); ignoreDepsAnswer = await this.askIgnoreDepsPrompt(wouldIgnoreDeps); goBack = ignoreDepsAnswer === 'Go back'; } if (!versionAnswer) { throw new Error('Error in processing the prompt versionAnswer'); } if (ignoreDepsAnswer === 'Yes') { // Also ignore the dependencies of the unselected version ignore = ignore.concat(wouldIgnoreDeps.map((dep) => dep.packageName)); } const unionMe = this.sortVersionsByAnswer(girModulesGrouped, versionAnswer.selected); // Do not ignore the selected package version keep = union(keep, unionMe.keep); // Ignore the unchosen package versions ignore = ignore.concat(unionMe.ignore); } } if (ignore && ignore.length > 0) { const ignoreLogList = '- ' + ignore.join('\n- '); this.log.log(bold(`\n The following modules will be ignored:`)); this.log.log(`\n${ignoreLogList}\n`); await this.askAddToIgnoreToConfigPrompt(ignore); } return { keep, ignore, }; } /** * Asks via cli prompt if the user wants to add the ignored modules to his config file * @param ignoredModules */ async askAddToIgnoreToConfigPrompt(ignoredModules) { const shouldAdd = await select({ message: `Do you want to add the ignored modules to your config so that you don't need to select them again next time?\n Config path: '${Config.configFilePath}'`, choices: [ { value: 'No', name: 'No' }, { value: 'Yes', name: 'Yes' }, ], }); if (shouldAdd === 'Yes') { await Config.addToConfig({ ignore: Array.from(ignoredModules), }); this.log.log(`Add ignored modules to '${Config.configFilePath}'`); } } /** * Figure out transitive module dependencies * @param packageName * @param result */ traverseDependencies(packageName, result = {}) { const deps = this.modDependencyMap[packageName]; if (isIterable(deps)) { for (const dep of deps) { if (result[dep.packageName]) continue; result[dep.packageName] = dep; this.traverseDependencies(dep.packageName, result); } } } /** * Extends the modDependencyMap by the current Module, * should be called for each girModule so that the modDependencyMap is complete * @param girModule */ extendDependencyMapByGirModule(girModule) { this.modDependencyMap[girModule.packageName] = girModule.dependencies; } /** * Sets the traverse dependencies for the current girModule, * is required so that all dependencies can be found internally when generating the dependency imports for the module .d.ts file * @param girModules */ async initGirModules(girModules) { for (const girModule of girModules) { const result = {}; this.traverseDependencies(girModule.packageName, result); await girModule.module.initTransitiveDependencies(Object.values(result)); } } /** * Reads a gir xml module file and creates an object of GirModule. * Also sets the setDependencyMap * @param fillName * @param config */ async loadAndCreateGirModule(dependency) { if (!dependency.exists || dependency.path === null) { return null; } this.log.log(`Loading ${dependency.packageName}...`); const girModule = await GirModule.load(dependency, this.config, this.dependencyManager); // Figure out transitive module dependencies this.extendDependencyMapByGirModule(girModule); return girModule; } /** * Returns a girModule found by `packageName` property * @param girModules Array of girModules * @param packageNames Full name like 'Gtk-3.0' you are looking for */ findGirModuleByFullNames(girModules, packageNames) { return girModules.filter((girModule) => packageNames.includes(girModule.packageName)); } /** * Checks if a girModules with the `packageNames` exists * @param girModules * @param packageName */ existsGirModules(girModules, packageName) { const foundModule = this.findGirModuleByFullNames(girModules, [packageName]); return foundModule.length > 0; } /** * Reads the gir xml module files and creates an object of GirModule for each module * @param dependencies * @param girModules * @param resolvedBy * @param failedGirModules * @param ignoreDependencies * @returns */ async loadGirModules(dependencies, ignoreDependencies = [], girModules = [], resolvedBy = ResolveType.BY_HAND, failedGirModules = new Set()) { let newModuleFound = false; // Clone array dependencies = [...dependencies]; while (dependencies.length > 0) { const dependency = dependencies.shift(); if (!dependency?.packageName) continue; // If module has not already been loaded if (!this.existsGirModules(girModules, dependency.packageName)) { const girModule = await this.loadAndCreateGirModule(dependency); if (!girModule) { if (!failedGirModules.has(dependency.packageName)) { this.log.warn(WARN_NO_GIR_FILE_FOUND_FOR_PACKAGE(dependency.packageName)); failedGirModules.add(dependency.packageName); } } else if (girModule && girModule.packageName) { const addModule = { packageName: girModule.packageName, module: girModule, resolvedBy, path: dependency.path, }; girModules.push(addModule); newModuleFound = true; } } } if (!newModuleFound) { return { loaded: girModules, failed: failedGirModules, }; } // Figure out transitive module dependencies await this.initGirModules(girModules); // Load girModules for dependencies for (const girModule of girModules) { // Load dependencies const transitiveDependencies = girModule.module.transitiveDependencies; if (transitiveDependencies.length > 0) { await this.loadGirModules(transitiveDependencies, ignoreDependencies, girModules, ResolveType.DEPENDENCE, failedGirModules); } } return { loaded: girModules, failed: failedGirModules, }; } /** * Find modules with the possibility to use wild cards for module names. E.g. `Gtk*` or `'*'` * @param modules * @param ignore */ async findGirFiles(globPackageNames, ignore = []) { const foundFiles = new Set(); for (let i = 0; i < globPackageNames.length; i++) { if (!globPackageNames[i]) { continue; } const filename = `${globPackageNames[i]}.gir`; const pattern = this.config.girDirectories.map((girDirectory) => join(girDirectory, filename)); const ignoreGirs = ignore.map((girDirectory) => girDirectory + '.gir'); const files = await glob(pattern, { ignore: ignoreGirs }); files.forEach((file) => foundFiles.add(file)); } return foundFiles; } async girFilePathToDependencies(girFiles) { const dependencies = []; for (const girFile of girFiles) { const packageName = basename(girFile, '.gir'); const { namespace, version } = splitModuleName(packageName); const dep = await this.dependencyManager.get(namespace, version); dependencies.push(dep); } return dependencies; } /** * Loads all found `packageNames` * @param girDirectories * @param packageNames * @param doNotAskForVersionOnConflict Set this to false if you want to get a prompt for each version conflict */ async getModulesResolved(packageNames, ignore = [], doNotAskForVersionOnConflict = true) { const girFiles = await this.findGirFiles([...packageNames], ignore); // Always require these because GJS does... const GLib = await this.dependencyManager.get('GLib', '2.0'); const Gio = await this.dependencyManager.get('Gio', '2.0'); const GObject = await this.dependencyManager.get('GObject', '2.0'); const dependencies = await this.girFilePathToDependencies(girFiles); const { loaded, failed } = await this.loadGirModules([ GLib, Gio, GObject, ...dependencies.filter((dep) => dep.namespace !== 'GLib' && dep.namespace !== 'Gio' && dep.namespace !== 'GObject'), ], ignore); let keep = []; if (doNotAskForVersionOnConflict) { keep = loaded; } else { const girModulesGrouped = this.groupGirFiles(loaded); const filtered = await this.askForEachConflictVersionsPrompt(girModulesGrouped, ignore); keep = Array.from(filtered.keep); } const grouped = this.groupGirFiles(keep); return { keep, grouped, ignore, failed }; } /** * Find modules * @param girDirectories * @param modules */ async getModules(modules, ignore = []) { const girFiles = await this.findGirFiles(modules, ignore); const dependencies = await this.girFilePathToDependencies(girFiles); const { loaded, failed } = await this.loadGirModules(dependencies, ignore); const grouped = this.groupGirFiles(loaded); return { grouped, loaded, failed: Array.from(failed) }; } /** Start parsing the gir modules */ parse(girModules) { for (const girModule of girModules) { girModule.module.parse(); } } } //# sourceMappingURL=module-loader.js.map