UNPKG

@ts-for-gir/cli

Version:

TypeScript type definition generator for GObject introspection GIR files

589 lines (528 loc) 21.9 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 { type Question } from 'inquirer' 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' import type { GirModulesGroupedMap, OptionsGeneration, GirModuleResolvedBy, GirModulesGrouped, DependencyMap, Dependency, AnswerVersion, } from '@ts-for-gir/lib' export class ModuleLoader { log: Logger dependencyManager: DependencyManager /** Transitive module dependencies */ modDependencyMap: DependencyMap = {} constructor(protected readonly config: OptionsGeneration) { 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 */ protected groupGirFiles(resolveGirModules: Set<GirModuleResolvedBy> | GirModuleResolvedBy[]): GirModulesGroupedMap { const girModulesGrouped: GirModulesGroupedMap = {} 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 */ protected sortVersionsByAnswer( girModulesGrouped: GirModulesGrouped, selected: string[], ): { keep: Set<GirModuleResolvedBy>; ignore: string[] } { const keep = new Set<GirModuleResolvedBy>() let ignore: string[] = [] if (!girModulesGrouped.hasConflict) { keep.add(girModulesGrouped.modules[0]) } else { const keepModules = this.findGirModuleByFullNames( girModulesGrouped.modules, selected, ) as GirModuleResolvedBy[] 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, } } protected generateContinueQuestion( message = `do you want to continue?`, choices = ['Yes', 'Go back'], ): { message: string; choices: string[] } { return { message, choices, } } protected generateIgnoreDepsQuestion( message = `Do you want to ignore them too?`, choices = ['Yes', 'No', 'Go back'], ): { message: string; choices: string[] } { return { message, choices, } } protected async askIgnoreDepsPrompt( deps: GirModuleResolvedBy[] | Set<GirModuleResolvedBy>, ): Promise<'Yes' | 'No' | 'Go back'> { const size = (deps as GirModuleResolvedBy[]).length || (deps as Set<GirModuleResolvedBy>).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<'Yes' | 'No' | 'Go back'>({ 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<'Yes' | 'Go back'>({ 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 */ protected generateModuleVersionQuestion(girModuleGrouped: GirModulesGrouped, message?: string): Question { 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 */ protected findGirFilesDependOnPackage( girModulesGroupedMap: GirModulesGroupedMap, packageName: string, ): GirModuleResolvedBy[] { const girModules: GirModuleResolvedBy[] = [] 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 */ protected findGirFilesDependOnPackages( girModulesGroupedMap: GirModulesGroupedMap, packageNames: string[], ): GirModuleResolvedBy[] { let girModules: GirModuleResolvedBy[] = [] for (const packageName of packageNames) { girModules = [...girModules, ...this.findGirFilesDependOnPackage(girModulesGroupedMap, packageName)] } return girModules } protected async askForVersionsPrompt(girModulesGrouped: GirModulesGrouped): Promise<AnswerVersion> { const choices = ['All', ...girModulesGrouped.modules.map((module) => module.packageName)] const selected = await select<string>({ 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 */ protected async askForEachConflictVersionsPrompt( girModulesGroupedMap: GirModulesGroupedMap, ignore: string[], ): Promise<{ keep: Set<GirModuleResolvedBy>; ignore: string[] }> { let keep = new Set<GirModuleResolvedBy>() 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<GirModuleResolvedBy>(keep, girModulesGrouped.modules) } else { let goBack = true let versionAnswer: AnswerVersion | null = null let ignoreDepsAnswer: 'Yes' | 'No' | 'Go back' | null = null let wouldIgnoreDeps: GirModuleResolvedBy[] = [] 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<GirModuleResolvedBy>(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 */ protected async askAddToIgnoreToConfigPrompt(ignoredModules: string[] | Set<string>): Promise<void> { const shouldAdd = await select<'Yes' | 'No'>({ 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 */ protected traverseDependencies(packageName: string, result: { [name: string]: Dependency } = {}): void { 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 */ protected extendDependencyMapByGirModule(girModule: GirModule): void { 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 */ protected async initGirModules(girModules: GirModuleResolvedBy[]): Promise<void> { for (const girModule of girModules) { const result: { [name: string]: Dependency } = {} 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 */ protected async loadAndCreateGirModule(dependency: Dependency): Promise<GirModule | null> { 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 */ protected findGirModuleByFullNames( girModules: (GirModuleResolvedBy | GirModule)[], packageNames: string[], ): Array<GirModuleResolvedBy | GirModule> { return girModules.filter((girModule) => packageNames.includes(girModule.packageName)) } /** * Checks if a girModules with the `packageNames` exists * @param girModules * @param packageName */ protected existsGirModules(girModules: (GirModuleResolvedBy | GirModule)[], packageName: string): boolean { 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 */ protected async loadGirModules( dependencies: Dependency[], ignoreDependencies: string[] = [], girModules: GirModuleResolvedBy[] = [], resolvedBy = ResolveType.BY_HAND, failedGirModules = new Set<string>(), ): Promise<{ loaded: GirModuleResolvedBy[]; failed: Set<string> }> { 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 */ protected async findGirFiles(globPackageNames: string[], ignore: string[] = []): Promise<Set<string>> { const foundFiles = new Set<string>() 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 } protected async girFilePathToDependencies(girFiles: Set<string>): Promise<Dependency[]> { const dependencies: Dependency[] = [] 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 */ public async getModulesResolved( packageNames: string[], ignore: string[] = [], doNotAskForVersionOnConflict = true, ): Promise<{ keep: GirModuleResolvedBy[]; grouped: GirModulesGroupedMap; ignore: string[]; failed: Set<string> }> { 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: GirModuleResolvedBy[] = [] 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 */ public async getModules( modules: string[], ignore: string[] = [], ): Promise<{ grouped: GirModulesGroupedMap; loaded: GirModuleResolvedBy[]; failed: string[] }> { 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 */ public parse(girModules: GirModuleResolvedBy[]): void { for (const girModule of girModules) { girModule.module.parse() } } }