@ts-for-gir/cli
Version:
TypeScript type definition generator for GObject introspection GIR files
465 lines • 19.1 kB
JavaScript
/**
* 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