nihilqui
Version:
Typescript .d.ts generator from GIR for gjs and node-gtk
594 lines (564 loc) • 22.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/**
* Default values, parse the config file and handle CLI flags
*/
import inquirer from 'inquirer'
import { Options } from 'yargs'
import { cosmiconfig, Options as ConfigSearchOptions } from 'cosmiconfig'
import { join, extname, dirname, resolve } from 'path'
import { writeFile } from 'fs/promises'
import {
merge,
isEqual,
Logger,
APP_NAME,
APP_USAGE,
ERROR_CONFIG_EXTENSION_UNSUPPORTED,
WARN_USE_ESM_FOR_ALIAS,
WARN_USE_GJS_FOR_ALIAS,
} from '@ts-for-gir/lib'
import { readTsJsConfig } from './utils.js'
import type { Environment, UserConfig, ConfigFlags, UserConfigLoadResult, GenerateConfig } from '@ts-for-gir/lib'
export class Config {
static appName = APP_NAME
static usage = APP_USAGE
/**
* Default cli flag and argument values
*/
static defaults = {
environments: ['gjs'],
print: false,
configName: '.ts-for-girrc.js',
root: process.cwd(),
outdir: './@types',
girDirectories: getDefaultGirDirectories(),
modules: ['*'],
ignore: [],
verbose: false,
ignoreVersionConflicts: false,
noNamespace: false,
buildType: 'lib',
moduleType: 'esm',
noComments: false,
noDebugComments: false,
fixConflicts: true,
noDOMLib: false,
generateAlias: false,
promisify: true,
npmScope: '@girs',
package: false,
packageYarn: false,
}
static configFilePath = join(process.cwd(), Config.defaults.configName)
/**
* CLI options used in commands/generate.ts and commands/list.ts
*/
static options: { [name: string]: Options } = {
modules: {
description: "GIR modules to load, e.g. 'Gio-2.0'. Accepts multiple modules",
array: true,
default: Config.defaults.modules,
normalize: true,
},
girDirectories: {
type: 'string',
alias: 'g',
description: 'GIR directories',
array: true,
default: Config.defaults.girDirectories,
normalize: true,
},
root: {
type: 'string',
description: 'Root directory of your project',
default: Config.defaults.root,
normalize: true,
},
outdir: {
type: 'string',
alias: 'o',
description: 'Directory to output to',
default: Config.defaults.outdir,
normalize: true,
},
environments: {
type: 'string',
alias: 'e',
description: 'Javascript environment',
array: true,
choices: ['gjs', 'node'],
default: Config.defaults.environments,
normalize: true,
},
ignore: {
type: 'string',
alias: 'i',
description: 'Modules that should be ignored',
array: true,
default: Config.defaults.ignore,
normalize: true,
},
buildType: {
type: 'string',
alias: 'b',
description: 'Definitions generation type',
array: false,
choices: ['lib', 'types'],
default: Config.defaults.buildType,
normalize: true,
},
moduleType: {
type: 'string',
alias: 't',
description: 'Specify what module code is generated.',
choices: ['esm', 'commonjs', 'cjs'],
default: Config.defaults.moduleType,
normalize: true,
},
verbose: {
type: 'boolean',
alias: 'v',
description: 'Switch on/off the verbose mode',
default: Config.defaults.verbose,
normalize: true,
},
ignoreVersionConflicts: {
type: 'boolean',
description: 'Do not ask for package versions if multiple versions are found',
default: Config.defaults.ignoreVersionConflicts,
normalize: true,
},
print: {
type: 'boolean',
alias: 'p',
description: 'Print the output to console and create no files',
default: Config.defaults.print,
normalize: true,
},
configName: {
type: 'string',
description: 'Name of the config if you want to use a different name',
default: Config.defaults.configName,
normalize: true,
},
noNamespace: {
type: 'boolean',
alias: 'd',
description: 'Do not export all symbols for each module as a namespace',
default: Config.defaults.noNamespace,
normalize: true,
},
noComments: {
type: 'boolean',
alias: 'n',
description: 'Do not generate documentation comments',
default: Config.defaults.noComments,
normalize: true,
},
noDebugComments: {
type: 'boolean',
description: 'Do not generate debugging inline comments',
default: Config.defaults.noDebugComments,
normalize: true,
},
fixConflicts: {
type: 'boolean',
description: 'Fix Inheritance and implementation type conflicts',
default: Config.defaults.fixConflicts,
normalize: true,
},
noDOMLib: {
type: 'boolean',
description: 'Disables the generation of types that are in conflict with the DOM types',
default: Config.defaults.noDOMLib,
normalize: true,
},
generateAlias: {
type: 'boolean',
alias: 'a',
description: 'Generate an alias tsconfig file to support GJS ESM module imports',
default: Config.defaults.generateAlias,
normalize: true,
},
promisify: {
type: 'boolean',
description: 'Generate promisified functions for async/finish calls',
default: Config.defaults.promisify,
normalize: true,
},
npmScope: {
type: 'string',
description: 'Scope of the generated NPM packages',
default: Config.defaults.npmScope,
normalize: true,
},
package: {
type: 'boolean',
description: 'Generates an NPM compatible packages for each GIR module',
default: Config.defaults.package,
normalize: true,
},
packageYarn: {
type: 'boolean',
description: 'Adds Yarn workspace support to the NPM packages',
default: Config.defaults.packageYarn,
normalize: true,
},
}
/**
* CLI flags used in commands/generate.ts
*/
static generateOptions = {
modules: this.options.modules,
girDirectories: this.options.girDirectories,
root: this.options.root,
outdir: this.options.outdir,
environments: this.options.environments,
ignore: this.options.ignore,
buildType: this.options.buildType,
moduleType: this.options.moduleType,
verbose: this.options.verbose,
ignoreVersionConflicts: this.options.ignoreVersionConflicts,
print: this.options.print,
configName: this.options.configName,
noNamespace: this.options.noNamespace,
noComments: this.options.noComments,
noDebugComments: this.options.noDebugComments,
noDOMLib: this.options.noDOMLib,
fixConflicts: this.options.fixConflicts,
generateAlias: this.options.generateAlias,
promisify: this.options.promisify,
npmScope: this.options.npmScope,
package: this.options.package,
packageYarn: this.options.packageYarn,
}
static listOptions = {
modules: this.options.modules,
girDirectories: Config.options.girDirectories,
ignore: Config.options.ignore,
configName: Config.options.configName,
verbose: Config.options.verbose,
}
static docOptions = {
modules: this.options.modules,
girDirectories: Config.options.girDirectories,
outdir: Config.options.outdir,
environments: Config.options.environments,
ignore: Config.options.ignore,
verbose: Config.options.verbose,
ignoreVersionConflicts: Config.options.ignoreVersionConflicts,
configName: Config.options.configName,
}
/**
* Overwrites values in the user config file
* @param configsToAdd
*/
public static async addToConfig(configsToAdd: Partial<UserConfig>, configName?: string): Promise<void> {
const userConfig = await this.loadConfigFile(configName)
const path = userConfig?.filepath || this.configFilePath
const configToStore = {}
merge(configToStore, userConfig?.config || {}, configsToAdd)
const fileExtension = extname(path)
let writeConfigString = ''
switch (fileExtension) {
case '.js':
writeConfigString = `export default ${JSON.stringify(configToStore, null, 4)}`
break
case '.json':
writeConfigString = `${JSON.stringify(configToStore, null, 4)}`
break
default:
Logger.error(ERROR_CONFIG_EXTENSION_UNSUPPORTED)
break
}
if (writeConfigString && path) {
return writeFile(path, writeConfigString)
}
}
/**
* The user can create a `.ts-for-girrc` file for his default configs,
* this method load this config file an returns the user configuration
* @param configName If the user uses a custom config file name
*/
private static async loadConfigFile(configName?: string): Promise<UserConfigLoadResult | null> {
const configSearchOptions: ConfigSearchOptions = {
loaders: {
// ESM loader
'.js': async (filepath) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const file = await import(filepath)
// Files with `exports.default = { ... }`
if (file?.default?.default) {
return file.default.default as Partial<UserConfig>
}
// Files with `export default { ... }`
if (file?.default) {
return file.default as Partial<UserConfig>
}
// Files with `export { ... }`
return file as Partial<UserConfig>
},
},
}
if (configName) {
configSearchOptions.searchPlaces = [configName]
}
const configFile: UserConfigLoadResult | null = await cosmiconfig(Config.appName, configSearchOptions).search()
if (configFile?.filepath) {
Config.configFilePath = configFile.filepath
}
return configFile
}
public static getGenerateConfig(config: UserConfig, environment: Environment = 'gjs'): GenerateConfig {
const generateConfig: GenerateConfig = {
environment: environment,
girDirectories: config.girDirectories,
root: config.root,
outdir: config.outdir,
verbose: config.verbose,
buildType: config.buildType,
moduleType: config.moduleType,
noNamespace: config.noNamespace,
noComments: config.noComments,
noDebugComments: config.noDebugComments,
fixConflicts: config.fixConflicts,
noDOMLib: config.noDOMLib,
generateAlias: config.generateAlias,
promisify: config.promisify,
npmScope: config.npmScope,
package: config.package,
packageYarn: config.packageYarn,
}
return generateConfig
}
protected static async validateTsConfig(config: UserConfig): Promise<UserConfig> {
const tsCompilerOptions = (config.outdir && readTsJsConfig(config.outdir)?.compilerOptions) || {}
const tsConfigHasDOMLib = tsCompilerOptions.noLib
? false // NoLib makes typescript to ignore the lib property
: Array.isArray(tsCompilerOptions.lib)
? tsCompilerOptions.lib.some((lib) => lib.toLowerCase().startsWith('dom'))
: true // Typescript includes DOM lib by default
if (config.environments.includes('gjs') && tsConfigHasDOMLib && !config.noDOMLib) {
const answer = (
await inquirer.prompt([
{
type: 'list',
name: 'include',
choices: ['Yes', 'No'],
message:
'Your typescript compilerOptions includes the DOM lib, this conflicts with some Gjs global types, do you want to skip generating those types?',
},
])
).include as 'Yes' | 'No'
if (answer == 'No') config.noDOMLib = true
}
return config
}
public static async validate(config: UserConfig): Promise<UserConfig> {
if (config.generateAlias) {
if (!config.environments.includes('gjs')) {
Logger.warn(WARN_USE_GJS_FOR_ALIAS)
config.environments.push('gjs')
}
if (config.moduleType !== 'esm') {
Logger.warn(WARN_USE_ESM_FOR_ALIAS)
config.moduleType = 'esm'
}
}
config = await this.validateTsConfig(config)
return config
}
/**
* Loads the values of the config file and concatenate them with passed cli flags / arguments.
* The values from config file are preferred if the cli flag value is the default (and so not set / overwritten)
* @param options
*/
public static async load(options: ConfigFlags): Promise<UserConfig> {
const configFile = await this.loadConfigFile(options.configName)
const configFileData = configFile?.config || {}
const config: UserConfig = {
environments: options.environments,
buildType: options.buildType,
moduleType: options.moduleType,
verbose: options.verbose,
ignoreVersionConflicts: options.ignoreVersionConflicts,
print: options.print,
root: options.root,
outdir: options.outdir,
girDirectories: options.girDirectories,
ignore: options.ignore,
modules: options.modules,
noNamespace: options.noNamespace,
noComments: options.noComments,
noDebugComments: options.noDebugComments,
fixConflicts: options.fixConflicts,
noDOMLib: options.noDOMLib,
generateAlias: options.generateAlias,
promisify: options.promisify,
npmScope: options.npmScope,
package: options.package,
packageYarn: options.packageYarn,
}
if (configFileData) {
// environments
if (isEqual(config.environments, Config.defaults.environments) && configFileData.environments) {
config.environments = configFileData.environments
}
// buildType
if (config.buildType === Config.options.buildType.default && configFileData.buildType) {
config.buildType = configFileData.buildType
}
// moduleType
if (config.moduleType === Config.options.moduleType.default && configFileData.moduleType) {
config.moduleType = configFileData.moduleType
}
// verbose
if (config.verbose === Config.options.verbose.default && typeof configFileData.verbose === 'boolean') {
config.verbose = configFileData.verbose
}
// ignoreVersionConflicts
if (
config.ignoreVersionConflicts === Config.options.ignoreVersionConflicts.default &&
typeof configFileData.ignoreVersionConflicts === 'boolean'
) {
config.ignoreVersionConflicts = configFileData.ignoreVersionConflicts
}
// print
if (config.print === Config.options.print.default && typeof configFileData.print === 'boolean') {
config.print = configFileData.print
}
// root
if (config.root === Config.options.root.default && (configFileData.root || configFile?.filepath)) {
// Use the config file path as the root path if no root path is set
config.root =
configFileData.root ||
(configFile?.filepath ? dirname(configFile.filepath) : (Config.options.root.default as string))
}
// outdir
if (config.outdir === Config.options.outdir.default && configFileData.outdir) {
config.outdir = config.print ? null : configFileData.outdir
}
// girDirectories
if (config.girDirectories === Config.options.girDirectories.default && configFileData.girDirectories) {
config.girDirectories = configFileData.girDirectories
}
// ignore
if (
(!config.ignore || config.ignore.length <= 0 || isEqual(config.ignore, Config.defaults.ignore)) &&
configFileData.ignore
) {
config.ignore = configFileData.ignore
}
// modules
if (
(config.modules.length <= 0 || isEqual(config.modules, Config.defaults.modules)) &&
configFileData.modules
) {
config.modules = configFileData.modules
}
// noNamespace
if (
config.noNamespace === Config.options.noNamespace.default &&
typeof configFileData.noNamespace === 'boolean'
) {
config.noNamespace = configFileData.noNamespace
}
// noComments
if (
config.noComments === Config.options.noComments.default &&
typeof configFileData.noComments === 'boolean'
) {
config.noComments = configFileData.noComments
}
// noDebugComments
if (
config.noDebugComments === Config.options.noDebugComments.default &&
typeof configFileData.noDebugComments === 'boolean'
) {
config.noDebugComments = configFileData.noDebugComments
}
// fixConflicts
if (
config.fixConflicts === Config.options.fixConflicts.default &&
typeof configFileData.fixConflicts === 'boolean'
) {
config.fixConflicts = configFileData.fixConflicts
}
// noDOMLib
if (config.noDOMLib === Config.options.noDOMLib.default && typeof configFileData.noDOMLib === 'boolean') {
config.noDOMLib = configFileData.noDOMLib
}
// generateAlias
if (
config.generateAlias === Config.options.generateAlias.default &&
typeof configFileData.generateAlias === 'boolean'
) {
config.generateAlias = configFileData.generateAlias
}
// promisify
if (
config.promisify === Config.options.promisify.default &&
typeof configFileData.promisify === 'boolean'
) {
config.promisify = configFileData.promisify
}
// npmScope
if (config.npmScope === Config.options.npmScope.default && configFileData.npmScope) {
config.npmScope = configFileData.npmScope
}
// package
if (config.package === Config.options.package.default && typeof configFileData.package === 'boolean') {
config.package = configFileData.package
}
// packageYarn
if (
config.packageYarn === Config.options.packageYarn.default &&
typeof configFileData.packageYarn === 'boolean'
) {
config.packageYarn = configFileData.packageYarn
}
}
if ((config.moduleType as string) === 'commonjs') {
config.moduleType = 'cjs'
}
// If outdir is not absolute, make it absolute to the root path
if (config.outdir && !config.outdir?.startsWith('/')) {
config.outdir = resolve(config.root, config.outdir)
}
// Same for girDirectories
if (config.girDirectories) {
config.girDirectories = config.girDirectories.map((dir) => {
if (!dir.startsWith('/')) {
return resolve(config.root, dir)
}
return dir
})
}
return await this.validate(config)
}
}
function getDefaultGirDirectories(): string[] {
const girDirectories = [
'/usr/local/share/gir-1.0',
'/usr/share/gir-1.0',
'/usr/share/gnome-shell',
'/usr/share/gnome-shell/gir-1.0',
'/usr/lib64/mutter-10',
'/usr/lib64/mutter-11',
'/usr/lib64/mutter-12',
'/usr/lib/x86_64-linux-gnu/mutter-10',
'/usr/lib/x86_64-linux-gnu/mutter-11',
'/usr/lib/x86_64-linux-gnu/mutter-12',
]
// NixOS and other distributions does not have a /usr/local/share directory.
// Instead, the nix store paths with Gir files are set as XDG_DATA_DIRS.
// See https://github.com/NixOS/nixpkgs/blob/96e18717904dfedcd884541e5a92bf9ff632cf39/pkgs/development/libraries/gobject-introspection/setup-hook.sh#L7-L10
const dataDirs = process.env['XDG_DATA_DIRS']?.split(':') || []
for (let dataDir of dataDirs) {
dataDir = join(dataDir, 'gir-1.0')
if (!girDirectories.includes(dataDir)) {
girDirectories.push(dataDir)
}
}
return girDirectories
}