UNPKG

@ts-for-gir/cli

Version:

TypeScript type definition generator for GObject introspection GIR files

330 lines (295 loc) 11.7 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 { AnswerVersion, Dependency, GirModuleResolvedBy, GirModulesGroupedMap, NSRegistry, OptionsGeneration, } from "@ts-for-gir/lib"; import { DependencyManager, GirModule, Logger, ResolveType, union, WARN_NO_GIR_FILE_FOUND_FOR_PACKAGE, } from "@ts-for-gir/lib"; import { DependencyResolver, FileFinder, ModuleGrouper, PromptHandler } from "./module-loader/index.ts"; export class ModuleLoader { private readonly log: Logger; private readonly dependencyManager: DependencyManager; private readonly dependencyResolver: DependencyResolver; private readonly fileFinder: FileFinder; private readonly moduleGrouper: ModuleGrouper; private readonly promptHandler: PromptHandler; constructor( private readonly config: OptionsGeneration, private readonly registry: NSRegistry, ) { this.log = new Logger(config.verbose, "ModuleLoader"); this.dependencyManager = DependencyManager.getInstance(config); this.dependencyResolver = new DependencyResolver(); this.fileFinder = new FileFinder(config.girDirectories, this.dependencyManager); this.moduleGrouper = new ModuleGrouper(); this.promptHandler = new PromptHandler(config.verbose); } /** * 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 */ private async initGirModules(girModules: GirModuleResolvedBy[]): Promise<void> { for (const girModule of girModules) { const dependencies = this.dependencyResolver.getTransitiveDependencies(girModule.packageName); await girModule.module.initTransitiveDependencies(dependencies); } } /** * Reads a gir xml module file and creates an object of GirModule. * Also sets the setDependencyMap */ private 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.registry); // Figure out transitive module dependencies this.dependencyResolver.extendDependencyMapByGirModule(girModule); return girModule; } /** * If multiple versions of the same module are found, this will ask the user with input prompts for the version they wish to use. * Ignores also modules that depend on a module that should be ignored */ private 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.promptHandler.askForVersionsPrompt(girModulesGrouped); // Check modules that depend on the unchosen modules wouldIgnoreDeps = this.dependencyResolver.findModulesDependingOnPackages( girModulesGroupedMap, versionAnswer.unselected, ); // Do not check dependencies that have already been ignored wouldIgnoreDeps = wouldIgnoreDeps.filter((dep) => !ignore.includes(dep.packageName)); ignoreDepsAnswer = await this.promptHandler.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.moduleGrouper.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) { this.promptHandler.showIgnoredModules(ignore); await this.promptHandler.askAddToIgnoreToConfigPrompt(ignore); } return { keep, ignore, }; } /** * Reads the gir xml module files and creates an object of GirModule for each module */ private 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 and filter out ignored dependencies dependencies = [...dependencies].filter((dep) => { const packageName = dep.packageName; return !ignoreDependencies.some((ignored) => { // Remove */ prefix if present (e.g., "*/Gtk-4.0" -> "Gtk-4.0") const cleanIgnored = ignored.startsWith("*/") ? ignored.slice(2) : ignored; return packageName === cleanIgnored; }); }); while (dependencies.length > 0) { const dependency = dependencies.shift(); if (!dependency?.packageName) continue; // If module has not already been loaded if (!this.dependencyResolver.existsGirModules(girModules, dependency.packageName)) { const girModule = await this.loadAndCreateGirModule(dependency); if (!girModule) { if (!failedGirModules.has(dependency.packageName)) { // In external-deps mode the strict check after loading turns missing // transitive deps into a hard error; suppress the per-dep warn here so // the user sees one consolidated error instead of a wall of warnings. // `--allow-missing-deps` opts into the same suppression with a soft fail. if (!this.config.externalDeps) { this.log.warn(WARN_NO_GIR_FILE_FOUND_FOR_PACKAGE(dependency.packageName)); } failedGirModules.add(dependency.packageName); } } else if (girModule?.packageName) { const addModule = { namespace: dependency.namespace, version: dependency.version, 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, }; } /** * Loads all found `packageNames` * @param packageNames Module names to load * @param ignore Modules to ignore * @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.fileFinder.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 Cairo = await this.dependencyManager.get("cairo", "1.0"); // Update library versions for GObject and Gio to use GLib's version const dependencies = await this.fileFinder.girFilePathToDependencies(girFiles); // Load GJS force-loads as DEPENDENCE so they are never treated as user-requested // modules. This ensures --external-deps mode doesn't emit output files for them. const { loaded: forceLoaded, failed: forceFailed } = await this.loadGirModules( [GLib, Gio, GObject, Cairo], ignore, [], ResolveType.DEPENDENCE, ); // Load user-requested modules (and their transitive deps) starting from the // already-loaded force-load registry so they are not double-loaded. const { loaded, failed } = await this.loadGirModules( dependencies.filter( (dep) => dep.namespace !== "GLib" && dep.namespace !== "Gio" && dep.namespace !== "GObject" && dep.namespace !== "cairo", ), ignore, forceLoaded, ResolveType.BY_HAND, forceFailed, ); // External-deps mode is strict by default: any transitive dep GIR that couldn't be // loaded would silently degrade type quality. The hardcoded GLib/Gio/GObject/cairo // force-loads above are exempt (they exist for runtime convenience, not because the // input GIR references them). if (this.config.externalDeps && !this.config.allowMissingDeps && failed.size > 0) { const exempt = new Set(["GLib-2.0", "Gio-2.0", "GObject-2.0", "cairo-1.0"]); const critical = Array.from(failed).filter((pkg) => !exempt.has(pkg)); if (critical.length > 0) { throw new Error( `Missing GIR files for transitive dependencies in --external-deps mode:\n` + critical.map((pkg) => ` - ${pkg}`).join("\n") + `\n\nInstall the corresponding -devel packages, add their directories to ` + `--girDirectories, or pass --allow-missing-deps to generate anyway ` + `(warning: degraded type quality).`, ); } } let keep: GirModuleResolvedBy[] = []; if (doNotAskForVersionOnConflict) { keep = loaded; } else { const girModulesGrouped = this.moduleGrouper.groupGirFiles(loaded); const filtered = await this.askForEachConflictVersionsPrompt(girModulesGrouped, ignore); keep = Array.from(filtered.keep); } const grouped = this.moduleGrouper.groupGirFiles(keep); return { keep, grouped, ignore, failed }; } /** * Find modules * @param modules Module names to find * @param ignore Modules to ignore */ public async getModules( modules: string[], ignore: string[] = [], ): Promise<{ grouped: GirModulesGroupedMap; loaded: GirModuleResolvedBy[]; failed: string[] }> { const girFiles = await this.fileFinder.findGirFiles(modules, ignore); const dependencies = await this.fileFinder.girFilePathToDependencies(girFiles); const { loaded, failed } = await this.loadGirModules(dependencies, ignore); const grouped = this.moduleGrouper.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(); } } }