@ts-for-gir/cli 4.0.0-beta.9 → 4.0.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +335 -86
  2. package/bin/ts-for-gir +28832 -0
  3. package/bin/ts-for-gir-dev +43 -0
  4. package/bin/ts-for-gir-gjs +348955 -0
  5. package/dist-templates/types-locally/.ts-for-girrc.js +6 -0
  6. package/dist-templates/types-locally/README.md +15 -0
  7. package/dist-templates/types-locally/esbuild.ts +10 -0
  8. package/dist-templates/types-locally/main.ts +21 -0
  9. package/dist-templates/types-locally/package.json +18 -0
  10. package/dist-templates/types-locally/tsconfig.json +17 -0
  11. package/dist-templates/types-npm/README.md +14 -0
  12. package/dist-templates/types-npm/esbuild.ts +10 -0
  13. package/dist-templates/types-npm/main.ts +19 -0
  14. package/dist-templates/types-npm/package.json +23 -0
  15. package/dist-templates/types-npm/tsconfig.json +15 -0
  16. package/dist-templates/types-workspace/.ts-for-girrc.js +12 -0
  17. package/dist-templates/types-workspace/README.md +26 -0
  18. package/dist-templates/types-workspace/package.json +22 -0
  19. package/dist-templates/types-workspace/packages/app/esbuild.ts +10 -0
  20. package/dist-templates/types-workspace/packages/app/main.ts +19 -0
  21. package/dist-templates/types-workspace/packages/app/package.json +23 -0
  22. package/dist-templates/types-workspace/packages/app/tsconfig.json +15 -0
  23. package/dist-templates/types-workspace/tsconfig.json +11 -0
  24. package/package.json +60 -37
  25. package/src/commands/analyze.ts +344 -0
  26. package/src/commands/command-builder.ts +30 -0
  27. package/src/commands/copy.ts +71 -76
  28. package/src/commands/create.ts +223 -0
  29. package/src/commands/doc.ts +44 -47
  30. package/src/commands/generate.ts +35 -79
  31. package/src/commands/index.ts +8 -4
  32. package/src/commands/json.ts +43 -0
  33. package/src/commands/list.ts +71 -90
  34. package/src/commands/run-generation-command.ts +75 -0
  35. package/src/commands/self-update.ts +142 -0
  36. package/src/config/config-loader.ts +238 -0
  37. package/src/config/config-writer.ts +52 -0
  38. package/src/config/defaults.ts +68 -0
  39. package/src/config/index.ts +16 -0
  40. package/src/config/options.ts +365 -0
  41. package/src/config.ts +3 -456
  42. package/src/formatters/typescript-formatter.ts +17 -0
  43. package/src/generation-handler.ts +122 -67
  44. package/src/index.ts +4 -4
  45. package/src/module-loader/dependency-resolver.ts +100 -0
  46. package/src/module-loader/file-finder.ts +65 -0
  47. package/src/module-loader/index.ts +8 -0
  48. package/src/module-loader/module-grouper.ts +77 -0
  49. package/src/module-loader/prompt-handler.ts +111 -0
  50. package/src/module-loader.ts +321 -578
  51. package/src/start.ts +36 -15
  52. package/src/types/command-args.ts +154 -0
  53. package/src/types/command-definition.ts +17 -0
  54. package/src/types/commands.ts +30 -0
  55. package/src/types/index.ts +15 -0
  56. package/src/types/report-types.ts +34 -0
  57. package/lib/commands/copy.d.ts +0 -12
  58. package/lib/commands/copy.js +0 -80
  59. package/lib/commands/copy.js.map +0 -1
  60. package/lib/commands/doc.d.ts +0 -12
  61. package/lib/commands/doc.js +0 -40
  62. package/lib/commands/doc.js.map +0 -1
  63. package/lib/commands/generate.d.ts +0 -12
  64. package/lib/commands/generate.js +0 -71
  65. package/lib/commands/generate.js.map +0 -1
  66. package/lib/commands/index.d.ts +0 -4
  67. package/lib/commands/index.js +0 -5
  68. package/lib/commands/index.js.map +0 -1
  69. package/lib/commands/list.d.ts +0 -12
  70. package/lib/commands/list.js +0 -81
  71. package/lib/commands/list.js.map +0 -1
  72. package/lib/config.d.ts +0 -108
  73. package/lib/config.js +0 -410
  74. package/lib/config.js.map +0 -1
  75. package/lib/generation-handler.d.ts +0 -10
  76. package/lib/generation-handler.js +0 -48
  77. package/lib/generation-handler.js.map +0 -1
  78. package/lib/index.d.ts +0 -4
  79. package/lib/index.js +0 -5
  80. package/lib/index.js.map +0 -1
  81. package/lib/module-loader.d.ts +0 -148
  82. package/lib/module-loader.js +0 -468
  83. package/lib/module-loader.js.map +0 -1
  84. package/lib/start.d.ts +0 -2
  85. package/lib/start.js +0 -16
  86. package/lib/start.js.map +0 -1
@@ -2,101 +2,82 @@
2
2
  * Everything you need for the `ts-for-gir list` command is located here
3
3
  */
4
4
 
5
- import { Argv, BuilderCallback } from 'yargs'
6
- import { ModuleLoader } from '../module-loader.js'
7
- import { Config } from '../config.js'
8
- import { Logger, ERROR_NO_MODULES_FOUND, ResolveType } from '@ts-for-gir/lib'
5
+ import type { ConfigFlags } from "@ts-for-gir/lib";
6
+ import { APP_NAME, ERROR_NO_MODULES_FOUND, Logger, NSRegistry } from "@ts-for-gir/lib";
7
+ import { getOptionsGeneration, listOptions, load } from "../config.ts";
8
+ import { ModuleLoader } from "../module-loader.ts";
9
+ import type { ListCommandArgs } from "../types/index.ts";
10
+ import { createBuilder } from "./command-builder.ts";
9
11
 
10
- import type { ConfigFlags } from '@ts-for-gir/lib'
12
+ const command = "list [modules..]";
11
13
 
12
- const command = 'list [modules..]'
14
+ const description = "Lists all available GIR modules";
13
15
 
14
- const description = 'Lists all available GIR modules'
16
+ const logger = new Logger(true, "ListCommand");
15
17
 
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- const builder: BuilderCallback<any, ConfigFlags> = (yargs: Argv<any>) => {
18
- const optionNames = Object.keys(Config.listOptions)
19
- for (const optionName of optionNames) {
20
- yargs = yargs.option(optionName, Config.listOptions[optionName])
21
- }
22
- return yargs.example(examples) as Argv<ConfigFlags>
23
- }
24
-
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- const handler = async (args: ConfigFlags) => {
27
- const config = await Config.load(args)
28
- const generateConfig = Config.getOptionsGeneration(config)
29
- const moduleLoader = new ModuleLoader(generateConfig)
30
- const { grouped, failed } = await moduleLoader.getModules(config.modules, config.ignore)
31
- const moduleGroups = Object.values(grouped)
32
- if (Object.keys(grouped).length === 0) {
33
- return Logger.error(ERROR_NO_MODULES_FOUND(config.girDirectories))
34
- }
35
-
36
- const conflictModules = moduleGroups.filter((moduleGroup) => moduleGroup.hasConflict)
37
-
38
- const byHandModules = moduleGroups.filter(
39
- (moduleGroup) => moduleGroup.modules[0].resolvedBy === ResolveType.BY_HAND,
40
- )
41
-
42
- const depModules = moduleGroups.filter(
43
- (moduleGroup) => moduleGroup.modules[0].resolvedBy === ResolveType.DEPENDENCE,
44
- )
45
-
46
- Logger.info('\nSearch for gir files in:')
47
- for (const dir of config.girDirectories) {
48
- Logger.white(`- ${dir}`)
49
- }
50
-
51
- Logger.info('\nSelected Modules:')
52
- for (const moduleGroup of byHandModules) {
53
- for (const depModule of moduleGroup.modules) {
54
- Logger.white(`- ${depModule.packageName}`)
55
- Logger.gray(` - ${depModule.path}`)
56
- }
57
- }
58
-
59
- if (depModules.length > 0) {
60
- Logger.yellow('\nDependencies:')
61
- for (const moduleGroup of depModules) {
62
- for (const depModule of moduleGroup.modules) {
63
- Logger.white(`- ${depModule.packageName}`)
64
- Logger.gray(`- ${depModule.path}`)
65
- }
66
- }
67
- }
68
-
69
- if (conflictModules.length > 0) {
70
- Logger.danger('\nConflicts:')
71
- for (const moduleGroup of conflictModules) {
72
- Logger.white(`- ${moduleGroup.namespace}`)
73
- for (const conflictModule of moduleGroup.modules) {
74
- Logger.white(` - ${conflictModule.packageName}`)
75
- Logger.gray(` - ${conflictModule.path}`)
76
- }
77
- }
78
- }
18
+ const examples: ReadonlyArray<[string, string?]> = [
19
+ [`${APP_NAME} list -g ./vala-girs/gir-1.0`, "Lists all available GIR modules in ./vala-girs/gir-1.0"],
20
+ [
21
+ `${APP_NAME} list --ignore=Gtk-3.0 xrandr-1.3`,
22
+ "Lists all available GIR modules in /usr/share/gir-1.0 but not Gtk-3.0 and xrandr-1.3",
23
+ ],
24
+ ];
79
25
 
80
- if (failed.length > 0) {
81
- Logger.danger('\nDependencies not found:')
82
- for (const fail of failed) {
83
- Logger.white(`- ${fail}`)
84
- }
85
- }
86
- }
26
+ const builder = createBuilder<ListCommandArgs>(listOptions, examples);
87
27
 
88
- const examples: ReadonlyArray<[string, string?]> = [
89
- [`${Config.appName} list -g ./vala-girs/gir-1.0`, `Lists all available GIR modules in ./vala-girs/gir-1.0`],
90
- [
91
- `${Config.appName} list --ignore=Gtk-3.0 xrandr-1.3`,
92
- 'Lists all available GIR modules in /usr/share/gir-1.0 but not Gtk-3.0 and xrandr-1.3',
93
- ],
94
- ]
28
+ const handler = async (args: ConfigFlags) => {
29
+ const config = await load(args);
30
+ const generateConfig = getOptionsGeneration(config);
31
+ const registry = new NSRegistry();
32
+ const moduleLoader = new ModuleLoader(generateConfig, registry);
33
+ const { grouped, failed } = await moduleLoader.getModules(config.modules, config.ignore);
34
+ const moduleGroups = Object.values(grouped);
35
+
36
+ if (Object.keys(grouped).length === 0) {
37
+ return logger.error(ERROR_NO_MODULES_FOUND(config.girDirectories));
38
+ }
39
+
40
+ const conflictModules = moduleGroups.filter((moduleGroup) => moduleGroup.hasConflict);
41
+ const allModules = moduleGroups.filter((moduleGroup) => !moduleGroup.hasConflict);
42
+
43
+ logger.info("\nSearch for gir files in:");
44
+ for (const dir of config.girDirectories) {
45
+ logger.white(`- ${dir}`);
46
+ }
47
+
48
+ // Show all available modules
49
+ logger.info("\nAvailable Modules:");
50
+ for (const moduleGroup of allModules) {
51
+ for (const module of moduleGroup.modules) {
52
+ logger.white(`- ${module.packageName}`);
53
+ logger.gray(` - ${module.path}`);
54
+ }
55
+ }
56
+
57
+ // Only show sections if there is actual content
58
+ if (conflictModules.length > 0) {
59
+ logger.danger("\nConflicts:");
60
+ for (const moduleGroup of conflictModules) {
61
+ logger.white(`- ${moduleGroup.namespace}`);
62
+ for (const conflictModule of moduleGroup.modules) {
63
+ logger.white(` - ${conflictModule.packageName}`);
64
+ logger.gray(` - ${conflictModule.path}`);
65
+ }
66
+ }
67
+ }
68
+
69
+ if (failed.length > 0) {
70
+ logger.danger("\nDependencies not found:");
71
+ for (const fail of failed) {
72
+ logger.white(`- ${fail}`);
73
+ }
74
+ }
75
+ };
95
76
 
96
77
  export const list = {
97
- command,
98
- description,
99
- builder,
100
- handler,
101
- examples,
102
- }
78
+ command,
79
+ description,
80
+ builder,
81
+ handler,
82
+ examples,
83
+ };
@@ -0,0 +1,75 @@
1
+ import type { GeneratorType } from "@ts-for-gir/generator-base";
2
+ import {
3
+ type ConfigFlags,
4
+ ERROR_NO_MODULES_FOUND,
5
+ type GirModule,
6
+ Logger,
7
+ NSRegistry,
8
+ ReporterService,
9
+ ResolveType,
10
+ } from "@ts-for-gir/lib";
11
+ import { getOptionsGeneration, load } from "../config.ts";
12
+ import { GenerationHandler } from "../generation-handler.ts";
13
+ import { ModuleLoader } from "../module-loader.ts";
14
+
15
+ interface GenerationCommandOptions {
16
+ generatorType: GeneratorType;
17
+ loggerName: string;
18
+ configureRegistry?: (registry: NSRegistry) => void;
19
+ }
20
+
21
+ export async function runGenerationCommand(args: ConfigFlags, options: GenerationCommandOptions): Promise<void> {
22
+ const config = await load(args);
23
+ const generateConfig = getOptionsGeneration(config);
24
+ const logger = new Logger(true, options.loggerName);
25
+ const registry = new NSRegistry();
26
+
27
+ options.configureRegistry?.(registry);
28
+
29
+ const moduleLoader = new ModuleLoader(generateConfig, registry);
30
+
31
+ let tsForGir: GenerationHandler | null = null;
32
+
33
+ try {
34
+ const { keep } = await moduleLoader.getModulesResolved(
35
+ config.modules,
36
+ config.ignore || [],
37
+ config.ignoreVersionConflicts,
38
+ );
39
+
40
+ if (keep.length === 0) {
41
+ logger.error(ERROR_NO_MODULES_FOUND(config.girDirectories));
42
+ return;
43
+ }
44
+
45
+ tsForGir = new GenerationHandler(generateConfig, options.generatorType, registry);
46
+
47
+ moduleLoader.parse(keep);
48
+
49
+ // In external-deps mode, only generate the user-requested module(s). Transitively-loaded
50
+ // deps stay in the registry for type resolution but must not produce their own output.
51
+ const toGenerate = generateConfig.externalDeps ? keep.filter((m) => m.resolvedBy === ResolveType.BY_HAND) : keep;
52
+
53
+ const girModules = Array.from(toGenerate).map((girModuleResolvedBy) => girModuleResolvedBy.module as GirModule);
54
+
55
+ await tsForGir.start(girModules);
56
+ } catch (error) {
57
+ if (generateConfig.reporter && tsForGir) {
58
+ const service = ReporterService.getInstance();
59
+
60
+ if (tsForGir.log) {
61
+ tsForGir.log.reportGenerationFailure(
62
+ "Main",
63
+ error instanceof Error ? error : new Error(String(error)),
64
+ "Generation failed",
65
+ );
66
+ }
67
+
68
+ const report = service.generateComprehensiveReport();
69
+ service.printComprehensiveSummary(report);
70
+ await service.saveComprehensiveReport(undefined, report);
71
+ }
72
+
73
+ throw error;
74
+ }
75
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Everything you need for the `ts-for-gir self-update` command is located here
3
+ */
4
+
5
+ import { chmodSync, existsSync, renameSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ import { APP_NAME, APP_VERSION } from "@ts-for-gir/lib";
10
+
11
+ import type { SelfUpdateCommandArgs } from "../types/index.ts";
12
+
13
+ const REPO = "gjsify/ts-for-gir";
14
+ const GITHUB_API = "https://api.github.com";
15
+ const GJS_ASSET_NAME = "ts-for-gir-gjs";
16
+
17
+ function getCurrentBinaryPath(): string | null {
18
+ const p = process.argv[1] ?? null;
19
+ if (!p) return null;
20
+ // Refuse to update in dev mode (source file or node_modules path)
21
+ if (p.endsWith(".ts") || p.includes("node_modules")) return null;
22
+ return p;
23
+ }
24
+
25
+ async function fetchJson(url: string): Promise<unknown> {
26
+ const headers: Record<string, string> = {
27
+ Accept: "application/vnd.github.v3+json",
28
+ "User-Agent": `ts-for-gir/${APP_VERSION}`,
29
+ };
30
+ const token = process.env.GITHUB_TOKEN;
31
+ if (token) headers.Authorization = `token ${token}`;
32
+
33
+ const response = await fetch(url, { headers });
34
+ if (!response.ok) {
35
+ throw new Error(`HTTP ${response.status} from ${url}`);
36
+ }
37
+ return response.json();
38
+ }
39
+
40
+ async function downloadBinary(url: string, destPath: string): Promise<void> {
41
+ const response = await fetch(url, {
42
+ headers: { "User-Agent": `ts-for-gir/${APP_VERSION}` },
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`HTTP ${response.status} downloading binary`);
46
+ }
47
+ const buffer = await response.arrayBuffer();
48
+ const tmpPath = join(tmpdir(), `ts-for-gir-update-${Date.now()}`);
49
+ writeFileSync(tmpPath, Buffer.from(buffer));
50
+ chmodSync(tmpPath, 0o755);
51
+ renameSync(tmpPath, destPath); // atomic on POSIX
52
+ }
53
+
54
+ const command = "self-update";
55
+ const description = "Update ts-for-gir to the latest version from GitHub releases";
56
+ const examples: ReadonlyArray<[string, string?]> = [
57
+ [`${APP_NAME} self-update`, "Check for updates and install the latest version"],
58
+ [`${APP_NAME} self-update --check`, "Only check for a newer version, do not install"],
59
+ [`${APP_NAME} self-update --force`, "Force reinstall even if already on the latest version"],
60
+ ];
61
+
62
+ const builder = (yargs: import("yargs").Argv) =>
63
+ yargs
64
+ .option("check", {
65
+ description: "Only check for a newer version, do not install",
66
+ type: "boolean",
67
+ default: false,
68
+ })
69
+ .option("force", {
70
+ description: "Force reinstall even if already on the latest version",
71
+ type: "boolean",
72
+ default: false,
73
+ })
74
+ .example(examples as [string, string?][]);
75
+
76
+ const handler = async (args: SelfUpdateCommandArgs): Promise<void> => {
77
+ console.log(`Checking for updates... (current: v${APP_VERSION})`);
78
+
79
+ let release: Record<string, unknown>;
80
+ try {
81
+ release = (await fetchJson(`${GITHUB_API}/repos/${REPO}/releases/latest`)) as Record<string, unknown>;
82
+ } catch (err) {
83
+ const msg = err instanceof Error ? err.message : String(err);
84
+ process.stderr.write(`Failed to fetch release information: ${msg}\n`);
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+
89
+ const latestVersion = (release.tag_name as string).replace(/^v/, "");
90
+
91
+ if (latestVersion === APP_VERSION && !args.force) {
92
+ console.log(`Already up to date (v${APP_VERSION})`);
93
+ return;
94
+ }
95
+
96
+ if (args.check) {
97
+ console.log(`New version available: v${latestVersion} (current: v${APP_VERSION})`);
98
+ console.log(`Run \`${APP_NAME} self-update\` to install it.`);
99
+ return;
100
+ }
101
+
102
+ console.log(`Updating to v${latestVersion}...`);
103
+
104
+ const assets = release.assets as Array<Record<string, string>>;
105
+ const asset = assets.find((a) => a.name === GJS_ASSET_NAME);
106
+ if (!asset) {
107
+ process.stderr.write(
108
+ `No GJS binary found in release ${release.tag_name}.\n` +
109
+ "self-update requires the GJS bundle to be installed via install.js.\n" +
110
+ "For npm installations use: npm update -g @ts-for-gir/cli\n",
111
+ );
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+
116
+ const currentPath = getCurrentBinaryPath();
117
+ if (!currentPath || !existsSync(currentPath)) {
118
+ process.stderr.write(
119
+ "Cannot determine current binary path for update.\n" +
120
+ "self-update only works when running the installed GJS binary.\n",
121
+ );
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+
126
+ try {
127
+ await downloadBinary(asset.browser_download_url, currentPath);
128
+ console.log(`Successfully updated to v${latestVersion}`);
129
+ } catch (err) {
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ process.stderr.write(`Update failed: ${msg}\n`);
132
+ process.exitCode = 1;
133
+ }
134
+ };
135
+
136
+ export const selfUpdate = {
137
+ command,
138
+ description,
139
+ builder,
140
+ handler,
141
+ examples,
142
+ };
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Config loader functionality for ts-for-gir
3
+ */
4
+
5
+ import { dirname, resolve } from "node:path";
6
+ import type { ConfigFlags, OptionsGeneration, UserConfig, UserConfigLoadResult } from "@ts-for-gir/lib";
7
+ import { APP_NAME, isEqual } from "@ts-for-gir/lib";
8
+ import { type Options as ConfigSearchOptions, cosmiconfig } from "cosmiconfig";
9
+ import { setConfigFilePath } from "./config-writer.ts";
10
+ import { defaults } from "./defaults.ts";
11
+ import { docOptions, options } from "./options.ts";
12
+
13
+ /**
14
+ * The user can create a `.ts-for-girrc` file for his default configs,
15
+ * this method load this config file an returns the user configuration
16
+ * @param configName If the user uses a custom config file name
17
+ */
18
+ export async function loadConfigFile(configName?: string): Promise<UserConfigLoadResult | null> {
19
+ const configSearchOptions: Partial<ConfigSearchOptions> = {
20
+ loaders: {
21
+ // ESM loader
22
+ ".js": async (filepath) => {
23
+ const file = await import(filepath);
24
+
25
+ // Files with `exports.default = { ... }`
26
+ if (file?.default?.default) {
27
+ return file.default.default as Partial<UserConfig>;
28
+ }
29
+ // Files with `export default { ... }`
30
+ if (file?.default) {
31
+ return file.default as Partial<UserConfig>;
32
+ }
33
+ // Files with `export { ... }`
34
+ return file as Partial<UserConfig>;
35
+ },
36
+ },
37
+ };
38
+
39
+ if (configName) {
40
+ configSearchOptions.searchPlaces = [configName];
41
+ }
42
+
43
+ const configFile: UserConfigLoadResult | null = await cosmiconfig(APP_NAME, configSearchOptions).search();
44
+
45
+ if (configFile?.filepath) {
46
+ setConfigFilePath(configFile.filepath);
47
+ }
48
+
49
+ return configFile;
50
+ }
51
+
52
+ /**
53
+ * Convert UserConfig to OptionsGeneration
54
+ */
55
+ export function getOptionsGeneration(config: UserConfig): OptionsGeneration {
56
+ const generateConfig: OptionsGeneration = {
57
+ ...config,
58
+ };
59
+ return generateConfig;
60
+ }
61
+
62
+ /**
63
+ * Parse `Namespace=npm-package` strings (from repeatable `--external-package` flag) into a
64
+ * map. Silently drops entries that don't contain `=`. Empty input returns undefined so the
65
+ * field stays absent in the merged config (rather than `{}`, which would shadow rc values).
66
+ */
67
+ function parseExternalPackagePairs(pairs: string[] | undefined): Record<string, string> | undefined {
68
+ if (!pairs || pairs.length === 0) return undefined;
69
+ const map: Record<string, string> = {};
70
+ for (const pair of pairs) {
71
+ const eq = pair.indexOf("=");
72
+ if (eq < 1) continue;
73
+ const ns = pair.slice(0, eq).trim();
74
+ const pkg = pair.slice(eq + 1).trim();
75
+ if (ns && pkg) map[ns] = pkg;
76
+ }
77
+ return Object.keys(map).length > 0 ? map : undefined;
78
+ }
79
+
80
+ /**
81
+ * Validate the configuration
82
+ */
83
+ export function validate(config: UserConfig): UserConfig {
84
+ return config;
85
+ }
86
+
87
+ /**
88
+ * Merge a single config value from file config to user config
89
+ * @param userConfig The user config object to update
90
+ * @param configFileData The config file data to merge from
91
+ * @param key The config key to merge
92
+ * @param optionDefault The default value from options
93
+ * @param validator Optional validation function
94
+ */
95
+ const isBoolean = (v: unknown) => typeof v === "boolean";
96
+
97
+ function mergeConfigValue<K extends keyof UserConfig>(
98
+ userConfig: UserConfig,
99
+ configFileData: Partial<UserConfig>,
100
+ key: K,
101
+ optionDefault: unknown,
102
+ validator?: (value: unknown) => boolean,
103
+ ): void {
104
+ const fileValue = configFileData[key];
105
+ const userValue = userConfig[key];
106
+
107
+ // Skip if no file value
108
+ if (fileValue === undefined) return;
109
+
110
+ // Apply validator if provided
111
+ if (validator && !validator(fileValue)) return;
112
+
113
+ // Check if user value is default
114
+ const isDefault =
115
+ userValue === optionDefault ||
116
+ (Array.isArray(userValue) && Array.isArray(optionDefault) && isEqual(userValue, optionDefault));
117
+
118
+ if (isDefault) {
119
+ (userConfig[key] as UserConfig[K]) = fileValue as UserConfig[K];
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Loads the values of the config file and concatenate them with passed cli flags / arguments.
125
+ * The values from config file are preferred if the cli flag value is the default (and so not set / overwritten)
126
+ * @param cliOptions CLI options passed by the user
127
+ */
128
+ export async function load(cliOptions: ConfigFlags): Promise<UserConfig> {
129
+ const configFile = await loadConfigFile(cliOptions.configName);
130
+ const configFileData = configFile?.config || {};
131
+
132
+ // `--external-package GLib=@girs/glib-2.0` arrives as a string[]; collapse to Record.
133
+ // Drop the raw array so it doesn't pollute the merged UserConfig surface.
134
+ const externalPackagesFromCli = parseExternalPackagePairs(
135
+ (cliOptions as { externalPackage?: string[] }).externalPackage,
136
+ );
137
+ const { externalPackage: _externalPackage, ...cliOptionsClean } = cliOptions as ConfigFlags & {
138
+ externalPackage?: string[];
139
+ };
140
+
141
+ const userConfig: UserConfig = {
142
+ ...cliOptionsClean,
143
+ };
144
+ if (externalPackagesFromCli) {
145
+ userConfig.externalPackages = externalPackagesFromCli;
146
+ }
147
+
148
+ if (configFileData) {
149
+ // Boolean options — config file overrides CLI defaults
150
+ const booleanKeys: Array<[keyof UserConfig, unknown]> = [
151
+ ["verbose", options.verbose.default],
152
+ ["ignoreVersionConflicts", options.ignoreVersionConflicts.default],
153
+ ["print", options.print.default],
154
+ ["noNamespace", options.noNamespace.default],
155
+ ["noComments", options.noComments.default],
156
+ ["promisify", options.promisify.default],
157
+ ["workspace", options.workspace.default],
158
+ ["onlyVersionPrefix", options.onlyVersionPrefix.default],
159
+ ["noPrettyPrint", options.noPrettyPrint.default],
160
+ ["noAdvancedVariants", options.noAdvancedVariants.default],
161
+ ["package", options.package.default],
162
+ ["reporter", options.reporter.default],
163
+ ["externalDeps", options.externalDeps.default],
164
+ ["allowMissingDeps", options.allowMissingDeps.default],
165
+ ["combined", docOptions.combined.default],
166
+ ["merge", docOptions.merge.default],
167
+ ];
168
+ for (const [key, defaultVal] of booleanKeys) {
169
+ mergeConfigValue(userConfig, configFileData, key, defaultVal, isBoolean);
170
+ }
171
+
172
+ // String options — config file overrides CLI defaults
173
+ const stringKeys: Array<[keyof UserConfig, unknown]> = [
174
+ ["npmScope", options.npmScope.default],
175
+ ["reporterOutput", options.reporterOutput.default],
176
+ ["depVersionFormat", undefined],
177
+ ["theme", docOptions.theme.default],
178
+ ["sourceLinkTemplate", undefined],
179
+ ["readme", undefined],
180
+ ["jsonDir", undefined],
181
+ ];
182
+ for (const [key, defaultVal] of stringKeys) {
183
+ mergeConfigValue(userConfig, configFileData, key, defaultVal);
184
+ }
185
+
186
+ // Array options — config file overrides CLI defaults
187
+ const arrayKeys: Array<[keyof UserConfig, unknown]> = [
188
+ ["ignore", options.ignore.default],
189
+ ["modules", options.modules.default],
190
+ ];
191
+ for (const [key, defaultVal] of arrayKeys) {
192
+ mergeConfigValue(userConfig, configFileData, key, defaultVal);
193
+ }
194
+
195
+ // girDirectories: rc-file entries are prepended to the current dirs (CLI-provided or
196
+ // system defaults) rather than replacing them. This lets projects add local GIR dirs
197
+ // (e.g. a Vala build output) without having to enumerate all system paths in the rc.
198
+ // To use ONLY the specified dirs (no system fallback), pass --girDirectories on the CLI.
199
+ if (configFileData.girDirectories?.length) {
200
+ const current = userConfig.girDirectories as string[];
201
+ const toAdd = (configFileData.girDirectories as string[]).filter((d) => !current.includes(d));
202
+ if (toAdd.length > 0) {
203
+ userConfig.girDirectories = [...toAdd, ...current];
204
+ }
205
+ }
206
+
207
+ // Special handling for root
208
+ if (userConfig.root === options.root.default && (configFileData.root || configFile?.filepath)) {
209
+ userConfig.root =
210
+ configFileData.root || (configFile?.filepath ? dirname(configFile.filepath) : (options.root.default as string));
211
+ }
212
+
213
+ // Special handling for outdir (override with config file value if still at a default)
214
+ const isDefaultOutdir = userConfig.outdir === options.outdir.default || userConfig.outdir === defaults.docOutdir;
215
+ if (isDefaultOutdir && configFileData.outdir) {
216
+ userConfig.outdir = userConfig.print ? null : configFileData.outdir;
217
+ }
218
+
219
+ // externalPackages is a Record<string, string> in rc files; CLI overrides take precedence.
220
+ if (!externalPackagesFromCli && configFileData.externalPackages) {
221
+ userConfig.externalPackages = configFileData.externalPackages;
222
+ }
223
+ }
224
+
225
+ // Make paths absolute relative to root
226
+ const resolveToRoot = (path: string) => (path.startsWith("/") ? path : resolve(userConfig.root, path));
227
+
228
+ if (userConfig.outdir) {
229
+ userConfig.outdir = resolveToRoot(userConfig.outdir);
230
+ }
231
+ if (userConfig.jsonDir) {
232
+ userConfig.jsonDir = resolveToRoot(userConfig.jsonDir);
233
+ }
234
+ if (userConfig.girDirectories) {
235
+ userConfig.girDirectories = userConfig.girDirectories.map(resolveToRoot);
236
+ }
237
+ return validate(userConfig);
238
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Config writer functionality for ts-for-gir
3
+ */
4
+
5
+ import { writeFile } from "node:fs/promises";
6
+ import { extname, join } from "node:path";
7
+ import type { UserConfig } from "@ts-for-gir/lib";
8
+ import { ERROR_CONFIG_EXTENSION_UNSUPPORTED, Logger, merge } from "@ts-for-gir/lib";
9
+ import { loadConfigFile } from "./config-loader.ts";
10
+ import { defaults } from "./defaults.ts";
11
+
12
+ const logger = new Logger(false, "ConfigWriter");
13
+
14
+ export let configFilePath = join(process.cwd(), defaults.configName);
15
+
16
+ /**
17
+ * Update the config file path when a config is loaded
18
+ */
19
+ export function setConfigFilePath(path: string): void {
20
+ configFilePath = path;
21
+ }
22
+
23
+ /**
24
+ * Overwrites values in the user config file
25
+ * @param configsToAdd Configuration values to add/update
26
+ * @param configName Optional custom config file name
27
+ */
28
+ export async function addToConfig(configsToAdd: Partial<UserConfig>, configName?: string): Promise<void> {
29
+ const userConfig = await loadConfigFile(configName);
30
+ const path = userConfig?.filepath || configFilePath;
31
+ const configToStore = {};
32
+ merge(configToStore, userConfig?.config || {}, configsToAdd);
33
+
34
+ const fileExtension = extname(path);
35
+ let writeConfigString = "";
36
+
37
+ switch (fileExtension) {
38
+ case ".js":
39
+ writeConfigString = `export default ${JSON.stringify(configToStore, null, 4)}`;
40
+ break;
41
+ case ".json":
42
+ writeConfigString = `${JSON.stringify(configToStore, null, 4)}`;
43
+ break;
44
+ default:
45
+ logger.error(ERROR_CONFIG_EXTENSION_UNSUPPORTED);
46
+ break;
47
+ }
48
+
49
+ if (writeConfigString && path) {
50
+ return writeFile(path, writeConfigString);
51
+ }
52
+ }