UNPKG

@ts-for-gir/lib

Version:

Typescript .d.ts generator from GIR for gjs

464 lines (404 loc) 14.5 kB
import { readFile } from "node:fs/promises"; import { type GirInclude, type GirNamespace, type GirRepository, type GirXML, parser } from "@gi.ts/parser"; import { APP_VERSION } from "./constants.ts"; import type { GirModule } from "./gir-module.ts"; import { LibraryVersion } from "./library-version.ts"; import { Logger } from "./logger.ts"; import type { Dependency, FileInfo, OptionsGeneration } from "./types/index.ts"; import { findFilesInDirs } from "./utils/files.ts"; import { splitModuleName } from "./utils/girs.ts"; import { sanitizeNamespace, transformImportName, transformModuleNamespaceName } from "./utils/naming.ts"; import { pascalCase } from "./utils/strings.ts"; export class DependencyManager { protected log: Logger; protected readonly config: OptionsGeneration; protected _cache: { [packageName: string]: Dependency } = {}; static instances: { [key: string]: DependencyManager } = {}; protected constructor(config: OptionsGeneration) { this.config = config; this.log = new Logger(config.verbose, "DependencyManager"); } /** * Get the DependencyManager singleton instance */ static getInstance(config?: OptionsGeneration): DependencyManager { const configKey = config ? JSON.stringify(config) : Object.keys(DependencyManager.instances)[0]; if (DependencyManager.instances[configKey]) { return DependencyManager.instances[configKey]; } if (!config) { throw new Error("config parameter is required to initialize DependencyManager"); } const instance = new DependencyManager(config); DependencyManager.instances[configKey] = instance; return instance; } protected parsePackageName(namespaceOrPackageName: string, version?: string) { let packageName: string; let namespace: string; if (version) { namespace = namespaceOrPackageName; packageName = `${namespace}-${version}`; } else { packageName = namespaceOrPackageName; const { namespace: _namespace, version: _version } = splitModuleName(packageName); namespace = _namespace; version = _version; } return { packageName, namespace, version }; } protected parseArgs(namespaceOrPackageNameOrRepo: string | GirRepository, version?: string, noOverride?: boolean) { let packageName: string; let namespace: string; let repo: GirRepository | null = null; if (typeof namespaceOrPackageNameOrRepo === "string") { // Special case for Gjs if (!noOverride && namespaceOrPackageNameOrRepo === "Gjs") { return { ...this.getGjs(), repo: null }; } const args = this.parsePackageName(namespaceOrPackageNameOrRepo, version); version = args.version; packageName = args.packageName; namespace = args.namespace; } else { repo = namespaceOrPackageNameOrRepo; const ns = repo.namespace?.[0]; if (!ns) { throw new Error("Invalid GirRepository"); } version = ns.$.version; namespace = ns.$.name; packageName = `${namespace}-${version}`; } return { packageName, namespace, version, repo }; } /** * Get all dependencies in the cache * @returns All dependencies in the cache */ all(): Dependency[] { return Object.values(this._cache); } getAllPackageNames(): string[] { return Object.keys(this._cache); } /** * Get the core dependencies * @returns */ async core(): Promise<Dependency[]> { return [ await this.get("GObject", "2.0"), await this.get("GLib", "2.0"), await this.get("Gio", "2.0"), await this.get("cairo", "1.0"), ]; } createImportProperties(namespace: string, packageName: string, version: string, libraryVersion?: LibraryVersion) { const importPath = this.createImportPath(packageName, namespace, version); const importDef = this.createImportDef(namespace, importPath); // For GObject and Gio, use GLib's library version if available let effectiveLibraryVersion = libraryVersion; if ((namespace === "GObject" || namespace === "Gio") && this._cache["GLib-2.0"]) { const glibDep = this._cache["GLib-2.0"]; if (glibDep.libraryVersion.toString() !== "0.0.0") { effectiveLibraryVersion = glibDep.libraryVersion; } } const packageJsonImport = this.createPackageJsonImport(importPath, effectiveLibraryVersion); return { importPath, importDef, packageJsonImport, }; } createImportPath(packageName: string, namespace: string, version: string): string { // In external-deps mode every dep import is resolved against an installed npm package // (e.g. `@girs/glib-2.0`), regardless of `package` mode. User-supplied overrides win // for namespaces with non-default scopes/versions (e.g. `Soup → @girs/soup-3.0`). if (this.config.externalDeps) { const override = this.config.externalPackages?.[namespace]; if (override) return override; const importName = transformImportName(packageName); return `${this.config.npmScope}/${importName}`; } if (!this.config.package) { return `gi://${namespace}?version=${version}`; } const importName = transformImportName(packageName); const importPath = `${this.config.npmScope}/${importName}`; return importPath; } createImportDef(namespace: string, importPath: string): string { return this.config.noNamespace ? `import type * as ${namespace} from '${importPath}'` : `import type ${namespace} from '${importPath}';`; } createPackageJsonImport(importPath: string, libraryVersion?: LibraryVersion): string { const pinnedVersion = libraryVersion ? `${libraryVersion.toString()}-${APP_VERSION}` : APP_VERSION; const format = this.config.depVersionFormat ?? (this.config.workspace ? "workspace" : "exact"); let depVersion: string; switch (format) { case "workspace": depVersion = "workspace:^"; break; case "caret": depVersion = `^${pinnedVersion}`; break; case "any": depVersion = "*"; break; default: depVersion = pinnedVersion; break; } return `"${importPath}": "${depVersion}"`; } protected async parseGir(path: string) { const girXML = parser.parseGir(await readFile(path, "utf8")); const repo = girXML.repository[0]; const ns = repo?.namespace?.[0]; const version = ns?.$.version; return { girXML, repo, ns, version }; } protected async parseGirAndReturnLatestVersion(filesInfo: FileInfo[]) { const libraryVersions: { libraryVersion: LibraryVersion; girXML: GirXML; fileInfo: FileInfo }[] = []; if (filesInfo.length > 1) { this.log.warn(`Multiple paths found for ${filesInfo[0].filename}`); } for (const fileInfo of filesInfo) { if (!fileInfo.exists || !fileInfo.path) { continue; } const { girXML, ns, version } = await this.parseGir(fileInfo.path); if (!version || !ns) { continue; } const libraryVersion = new LibraryVersion(ns?.constant, version); if (filesInfo.length > 1) { this.log.muted(` - ${fileInfo.path} (${libraryVersion.toString()})`); } libraryVersions.push({ libraryVersion, girXML, fileInfo, }); } // Compare all library versions and return the latest version const latestLibraryVersion = libraryVersions.sort((a, b) => a.libraryVersion.compare(b.libraryVersion))[0]; if (!latestLibraryVersion) { this.log.warn("No latest library version found", { libraryVersions, filesInfo, }); return { libraryVersion: new LibraryVersion(), girXML: null, fileInfo: filesInfo[0], }; } if (filesInfo.length > 1) { this.log.muted( `Use latest version ${latestLibraryVersion.libraryVersion.toString()} from ${latestLibraryVersion.fileInfo.path}`, ); } return latestLibraryVersion; } /** * Get the dependency object by packageName * @param packageName The package name (with version affix) of the dependency * @returns The dependency object */ async get(packageName: string): Promise<Dependency>; /** * Get the dependency object by namespace and version * @param namespace The namespace of the dependency * @param version The version of the dependency * @returns The dependency object */ async get(namespace: string, version: string, noOverride?: boolean): Promise<Dependency>; /** * Get the dependency object by {@link GirRepository} * @param namespace The namespace of the dependency * @param version The version of the dependency * @returns The dependency object */ async get(repo: GirRepository, version?: string, noOverride?: boolean): Promise<Dependency>; async get( namespaceOrPackageNameOrRepo: string | GirRepository, _version?: string, noOverride?: boolean, ): Promise<Dependency> { const parsedArgs = this.parseArgs(namespaceOrPackageNameOrRepo, _version, noOverride); const { packageName, repo } = parsedArgs; let { namespace, version } = parsedArgs; namespace = sanitizeNamespace(namespace); if (this._cache[packageName]) { const dep = this._cache[packageName]; return dep; } const filename = `${packageName}.gir`; const filesInfo = await findFilesInDirs(this.config.girDirectories, filename); const { libraryVersion, girXML, fileInfo } = await this.parseGirAndReturnLatestVersion(filesInfo); const ns = (girXML?.repository[0]?.namespace?.[0] || repo?.namespace?.[0] || null) as GirNamespace | null; // Use the version from the gir file if it exists if (ns?.$.version) { version = ns?.$.version; } if (ns?.$.name) { namespace = ns?.$.name; } const dependency: Dependency = { ...fileInfo, namespace, packageName, importName: transformImportName(packageName), importNamespace: transformModuleNamespaceName(packageName), version, libraryVersion, girXML, ...this.createImportProperties(namespace, packageName, version, libraryVersion), }; // Special case for Cairo // This is a special case for Cairo because Cairo in GJS is provided as a built-in module that doesn't // follow the standard GI repository pattern. // So we need to special case it and redirect to the 'cairo' package. // This changes the typescript import definition to use the internal 'cairo' package instead of the 'cairo-1.0' Gir package. if (!noOverride && namespace === "cairo" && version === "1.0") { dependency.importDef = this.createImportDef("cairo", "cairo"); } this._cache[packageName] = dependency; return dependency; } /** * Get all dependencies with the given namespace * @param namespace The namespace of the dependency * @returns All dependencies with the given namespace */ list(namespace: string): Dependency[] { const packageNames = this.all(); const candidates = packageNames.filter((dep) => { return dep.namespace === namespace && dep.exists; }); return candidates; } /** * Get girModule for dependency * @param girModules * @param packageName */ getModule(girModules: GirModule[], dep: Dependency): GirModule | undefined { return girModules.find( (m) => m.packageName === dep.packageName && m.namespace === dep.namespace && m.version === dep.version, ); } /** * Add all dependencies from an array of gir modules * @param girModules */ async addAll(girModules: GirModule[]): Promise<Dependency[]> { for (const girModule of girModules) { await this.get(girModule.namespace, girModule.version || "0.0"); } return this.all(); } /** * Transforms a gir include object array to a dependency object array * @param girIncludes - Array of gir includes * @returns Array of dependencies */ async fromGirIncludes(girIncludes: GirInclude[]): Promise<Dependency[]> { const dependencies: Dependency[] = []; for (const i of girIncludes) { dependencies.unshift(await this.get(i.$.name, i.$.version || "0.0")); } return dependencies; } /** * Check if multiple dependencies with the given namespace exist in the cache * @param namespace The namespace of the dependency * @returns */ hasConflict(namespace: string): boolean { const packageNames = this.getAllPackageNames(); const candidates = packageNames.filter((packageName) => { return packageName.startsWith(`${namespace}-`) && this._cache[packageName].namespace === namespace; }); return candidates.length > 1; } /** * get the latest version of the dependency with the given namespace * @param namespace The namespace of the dependency * @returns The latest version of the dependency */ getLatestVersion(namespace: string): Dependency | undefined { const candidates = this.list(namespace); const latestVersion = candidates .sort((a, b) => { return a.version.localeCompare(b.version); }) .pop(); return latestVersion; } /** * Check if the given version is the latest version of the dependency * @param namespace The namespace of the dependency * @param version The version of the dependency * @returns */ isLatestVersion(namespace: string, version: string): boolean { const latestVersion = this.getLatestVersion(namespace); return latestVersion?.version === version; } /** * Find a dependency by it's namespace from the cache, if multiple versions are found, the latest version is returned * @param namespace The namespace of the dependency * @returns The dependency object or null if not found */ find(namespace: string): Dependency | null { // Special case for Gjs if (namespace === "Gjs") { return this.getGjs(); } const packageNames = this.getAllPackageNames(); const candidates = packageNames.filter((packageName) => { return packageName.startsWith(`${namespace}-`) && this._cache[packageName].namespace === namespace; }); if (candidates.length > 1) { this.log.warn(`Found multiple versions of ${namespace}: ${candidates.join(", ")}`); } const latestVersion = candidates.sort().pop(); if (latestVersion && this._cache[latestVersion]) { const dep = this._cache[latestVersion]; return dep; } return null; } protected getPseudoPackage( packageName: string, namespace: string = pascalCase(packageName), version = "2.0", ): Dependency { if (this._cache[`${packageName}_pseudo`]) { return this._cache[`${packageName}_pseudo`]; } const dep: Dependency = { namespace, exists: true, filename: "", path: "", packageName: packageName, importName: transformImportName(packageName), importNamespace: transformModuleNamespaceName(packageName), version, libraryVersion: new LibraryVersion(), girXML: null, ...this.createImportProperties(packageName, packageName, version), }; this._cache[`${packageName}_pseudo`] = dep; return dep; } getGjs(): Dependency { return this.getPseudoPackage("Gjs"); } }