@gtkx/cli 1.3.0 → 1.5.0

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 (299) hide show
  1. package/README.md +5 -4
  2. package/dist/builder.d.ts +1 -1
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +6 -6
  5. package/dist/builder.js.map +1 -1
  6. package/dist/codegen/run-codegen.d.ts +3 -2
  7. package/dist/codegen/run-codegen.d.ts.map +1 -1
  8. package/dist/codegen/run-codegen.js +41 -12
  9. package/dist/codegen/run-codegen.js.map +1 -1
  10. package/dist/commands/build.d.ts +0 -4
  11. package/dist/commands/build.d.ts.map +1 -1
  12. package/dist/commands/build.js +0 -5
  13. package/dist/commands/build.js.map +1 -1
  14. package/dist/commands/codegen.d.ts.map +1 -1
  15. package/dist/commands/codegen.js +2 -4
  16. package/dist/commands/codegen.js.map +1 -1
  17. package/dist/deploy/depends.d.ts +2 -2
  18. package/dist/deploy/depends.d.ts.map +1 -1
  19. package/dist/deploy/depends.js +30 -15
  20. package/dist/deploy/depends.js.map +1 -1
  21. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  22. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  23. package/dist/deploy/freedesktop/localize.js +107 -0
  24. package/dist/deploy/freedesktop/localize.js.map +1 -0
  25. package/dist/deploy/install-hints.d.ts.map +1 -1
  26. package/dist/deploy/install-hints.js +5 -0
  27. package/dist/deploy/install-hints.js.map +1 -1
  28. package/dist/deploy/nfpm/config.js +1 -1
  29. package/dist/deploy/nfpm/config.js.map +1 -1
  30. package/dist/deploy/node-runtime/elf.d.ts +1 -1
  31. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  32. package/dist/deploy/node-runtime/elf.js +2 -2
  33. package/dist/deploy/node-runtime/elf.js.map +1 -1
  34. package/dist/deploy/node-runtime/index.js +2 -2
  35. package/dist/deploy/node-runtime/index.js.map +1 -1
  36. package/dist/deploy/notices/collect.d.ts +3 -1
  37. package/dist/deploy/notices/collect.d.ts.map +1 -1
  38. package/dist/deploy/notices/collect.js +2 -2
  39. package/dist/deploy/notices/collect.js.map +1 -1
  40. package/dist/deploy/notices/packages.d.ts +2 -1
  41. package/dist/deploy/notices/packages.d.ts.map +1 -1
  42. package/dist/deploy/notices/packages.js +4 -27
  43. package/dist/deploy/notices/packages.js.map +1 -1
  44. package/dist/deploy/payload/icons.d.ts +7 -3
  45. package/dist/deploy/payload/icons.d.ts.map +1 -1
  46. package/dist/deploy/payload/icons.js +46 -28
  47. package/dist/deploy/payload/icons.js.map +1 -1
  48. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  49. package/dist/deploy/payload/launcher.js +2 -0
  50. package/dist/deploy/payload/launcher.js.map +1 -1
  51. package/dist/deploy/payload/stage.d.ts +1 -1
  52. package/dist/deploy/payload/stage.d.ts.map +1 -1
  53. package/dist/deploy/payload/stage.js +23 -13
  54. package/dist/deploy/payload/stage.js.map +1 -1
  55. package/dist/deploy/run-deploy.d.ts.map +1 -1
  56. package/dist/deploy/run-deploy.js +149 -25
  57. package/dist/deploy/run-deploy.js.map +1 -1
  58. package/dist/deploy/settings/build-manifest.d.ts +9 -0
  59. package/dist/deploy/settings/build-manifest.d.ts.map +1 -0
  60. package/dist/deploy/settings/build-manifest.js +80 -0
  61. package/dist/deploy/settings/build-manifest.js.map +1 -0
  62. package/dist/deploy/settings/index.d.ts.map +1 -1
  63. package/dist/deploy/settings/index.js +9 -3
  64. package/dist/deploy/settings/index.js.map +1 -1
  65. package/dist/deploy/settings/libraries.d.ts +8 -0
  66. package/dist/deploy/settings/libraries.d.ts.map +1 -0
  67. package/dist/deploy/settings/libraries.js +39 -0
  68. package/dist/deploy/settings/libraries.js.map +1 -0
  69. package/dist/deploy/settings/paths.d.ts +3 -0
  70. package/dist/deploy/settings/paths.d.ts.map +1 -1
  71. package/dist/deploy/settings/paths.js +6 -32
  72. package/dist/deploy/settings/paths.js.map +1 -1
  73. package/dist/deploy/targets/appimage.d.ts.map +1 -1
  74. package/dist/deploy/targets/appimage.js +19 -13
  75. package/dist/deploy/targets/appimage.js.map +1 -1
  76. package/dist/deploy/targets/flatpak-manifest.d.ts +2 -1
  77. package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
  78. package/dist/deploy/targets/flatpak-manifest.js +5 -1
  79. package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
  80. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  81. package/dist/deploy/targets/flatpak-source.js +30 -16
  82. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  83. package/dist/deploy/tools.d.ts +6 -1
  84. package/dist/deploy/tools.d.ts.map +1 -1
  85. package/dist/deploy/tools.js +26 -1
  86. package/dist/deploy/tools.js.map +1 -1
  87. package/dist/deploy/types.d.ts +4 -4
  88. package/dist/deploy/types.d.ts.map +1 -1
  89. package/dist/deploy/types.js.map +1 -1
  90. package/dist/dev/icon-dir.d.ts +1 -1
  91. package/dist/dev/icon-dir.d.ts.map +1 -1
  92. package/dist/dev/icon-dir.js +20 -7
  93. package/dist/dev/icon-dir.js.map +1 -1
  94. package/dist/dev/locale-dir.d.ts +3 -0
  95. package/dist/dev/locale-dir.d.ts.map +1 -0
  96. package/dist/dev/locale-dir.js +17 -0
  97. package/dist/dev/locale-dir.js.map +1 -0
  98. package/dist/dev/runner-deps.d.ts.map +1 -1
  99. package/dist/dev/runner-deps.js +5 -1
  100. package/dist/dev/runner-deps.js.map +1 -1
  101. package/dist/dev/runner-main.d.ts.map +1 -1
  102. package/dist/dev/runner-main.js +6 -4
  103. package/dist/dev/runner-main.js.map +1 -1
  104. package/dist/dev/runner.d.ts +1 -1
  105. package/dist/dev/runner.d.ts.map +1 -1
  106. package/dist/dev/runner.js +7 -1
  107. package/dist/dev/runner.js.map +1 -1
  108. package/dist/dev/schema-dir.d.ts +1 -1
  109. package/dist/dev/schema-dir.d.ts.map +1 -1
  110. package/dist/dev/schema-dir.js +3 -1
  111. package/dist/dev/schema-dir.js.map +1 -1
  112. package/dist/dev/vite-dev-server.d.ts +1 -0
  113. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  114. package/dist/dev/vite-dev-server.js.map +1 -1
  115. package/dist/i18n/binding-extractor.d.ts +30 -0
  116. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  117. package/dist/i18n/binding-extractor.js +1324 -0
  118. package/dist/i18n/binding-extractor.js.map +1 -0
  119. package/dist/i18n/catalog-template.d.ts +3 -0
  120. package/dist/i18n/catalog-template.d.ts.map +1 -0
  121. package/dist/i18n/catalog-template.js +18 -0
  122. package/dist/i18n/catalog-template.js.map +1 -0
  123. package/dist/i18n/catalogs.d.ts +21 -0
  124. package/dist/i18n/catalogs.d.ts.map +1 -0
  125. package/dist/i18n/catalogs.js +139 -0
  126. package/dist/i18n/catalogs.js.map +1 -0
  127. package/dist/i18n/extractor-plugin.d.ts +8 -0
  128. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  129. package/dist/i18n/extractor-plugin.js +247 -0
  130. package/dist/i18n/extractor-plugin.js.map +1 -0
  131. package/dist/i18n/metadata-templates.d.ts +11 -0
  132. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  133. package/dist/i18n/metadata-templates.js +11 -0
  134. package/dist/i18n/metadata-templates.js.map +1 -0
  135. package/dist/i18n/source-messages.d.ts +23 -0
  136. package/dist/i18n/source-messages.d.ts.map +1 -0
  137. package/dist/i18n/source-messages.js +403 -0
  138. package/dist/i18n/source-messages.js.map +1 -0
  139. package/dist/i18n/types.d.ts +10 -0
  140. package/dist/i18n/types.d.ts.map +1 -0
  141. package/dist/i18n/types.js +108 -0
  142. package/dist/i18n/types.js.map +1 -0
  143. package/dist/internal/build-manifest.d.ts +20 -0
  144. package/dist/internal/build-manifest.d.ts.map +1 -0
  145. package/dist/internal/build-manifest.js +6 -0
  146. package/dist/internal/build-manifest.js.map +1 -0
  147. package/dist/internal/icon-path.d.ts +13 -0
  148. package/dist/internal/icon-path.d.ts.map +1 -0
  149. package/dist/internal/icon-path.js +86 -0
  150. package/dist/internal/icon-path.js.map +1 -0
  151. package/dist/internal/source-imports.d.ts +12 -0
  152. package/dist/internal/source-imports.d.ts.map +1 -0
  153. package/dist/internal/source-imports.js +123 -0
  154. package/dist/internal/source-imports.js.map +1 -0
  155. package/dist/mcp/client.d.ts +0 -1
  156. package/dist/mcp/client.d.ts.map +1 -1
  157. package/dist/mcp/client.js +11 -39
  158. package/dist/mcp/client.js.map +1 -1
  159. package/dist/mcp/handlers.d.ts +2 -1
  160. package/dist/mcp/handlers.d.ts.map +1 -1
  161. package/dist/mcp/handlers.js.map +1 -1
  162. package/dist/mcp/widget-properties.d.ts.map +1 -1
  163. package/dist/mcp/widget-properties.js +3 -12
  164. package/dist/mcp/widget-properties.js.map +1 -1
  165. package/dist/settings/render.d.ts +10 -1
  166. package/dist/settings/render.d.ts.map +1 -1
  167. package/dist/settings/render.js +78 -20
  168. package/dist/settings/render.js.map +1 -1
  169. package/dist/settings/schema.d.ts +4 -3
  170. package/dist/settings/schema.d.ts.map +1 -1
  171. package/dist/settings/schema.js +124 -15
  172. package/dist/settings/schema.js.map +1 -1
  173. package/dist/vite-plugins/asset-emitter.d.ts +1 -1
  174. package/dist/vite-plugins/asset-emitter.d.ts.map +1 -1
  175. package/dist/vite-plugins/asset-emitter.js.map +1 -1
  176. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  177. package/dist/vite-plugins/asset-extensions.js +1 -0
  178. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  179. package/dist/vite-plugins/asset-imports.d.ts +2 -1
  180. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  181. package/dist/vite-plugins/asset-imports.js +39 -28
  182. package/dist/vite-plugins/asset-imports.js.map +1 -1
  183. package/dist/vite-plugins/asset-specifier.d.ts +13 -1
  184. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  185. package/dist/vite-plugins/asset-specifier.js +39 -1
  186. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  187. package/dist/vite-plugins/build-manifest.d.ts +5 -0
  188. package/dist/vite-plugins/build-manifest.d.ts.map +1 -0
  189. package/dist/vite-plugins/{bundled-packages.js → build-manifest.js} +20 -14
  190. package/dist/vite-plugins/build-manifest.js.map +1 -0
  191. package/dist/vite-plugins/built-url.d.ts +1 -1
  192. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  193. package/dist/vite-plugins/built-url.js +37 -24
  194. package/dist/vite-plugins/built-url.js.map +1 -1
  195. package/dist/vite-plugins/i18n.d.ts +5 -0
  196. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  197. package/dist/vite-plugins/i18n.js +88 -0
  198. package/dist/vite-plugins/i18n.js.map +1 -0
  199. package/dist/vite-plugins/icons.d.ts +2 -1
  200. package/dist/vite-plugins/icons.d.ts.map +1 -1
  201. package/dist/vite-plugins/icons.js +25 -12
  202. package/dist/vite-plugins/icons.js.map +1 -1
  203. package/dist/vite-plugins/index.d.ts +2 -1
  204. package/dist/vite-plugins/index.d.ts.map +1 -1
  205. package/dist/vite-plugins/index.js +9 -5
  206. package/dist/vite-plugins/index.js.map +1 -1
  207. package/dist/vite-plugins/native.d.ts.map +1 -1
  208. package/dist/vite-plugins/native.js +23 -4
  209. package/dist/vite-plugins/native.js.map +1 -1
  210. package/dist/vite-plugins/resource-init-module.d.ts +7 -3
  211. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  212. package/dist/vite-plugins/resource-init-module.js +39 -14
  213. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  214. package/dist/vite-plugins/resource-shared.d.ts +3 -1
  215. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  216. package/dist/vite-plugins/resource-shared.js +3 -1
  217. package/dist/vite-plugins/resource-shared.js.map +1 -1
  218. package/dist/vite-plugins/resources.d.ts +1 -1
  219. package/dist/vite-plugins/resources.d.ts.map +1 -1
  220. package/dist/vite-plugins/resources.js +877 -86
  221. package/dist/vite-plugins/resources.js.map +1 -1
  222. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  223. package/dist/vite-plugins/self-contained.js +128 -135
  224. package/dist/vite-plugins/self-contained.js.map +1 -1
  225. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  226. package/dist/vite-plugins/settings-worker-env.js +6 -3
  227. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  228. package/dist/vite-plugins/settings.d.ts +3 -1
  229. package/dist/vite-plugins/settings.d.ts.map +1 -1
  230. package/dist/vite-plugins/settings.js +63 -23
  231. package/dist/vite-plugins/settings.js.map +1 -1
  232. package/env.d.ts +11 -192
  233. package/package.json +11 -10
  234. package/src/builder.ts +7 -7
  235. package/src/codegen/run-codegen.ts +64 -15
  236. package/src/commands/build.ts +0 -5
  237. package/src/commands/codegen.ts +2 -4
  238. package/src/deploy/depends.ts +47 -18
  239. package/src/deploy/freedesktop/localize.ts +144 -0
  240. package/src/deploy/install-hints.ts +5 -0
  241. package/src/deploy/nfpm/config.ts +1 -1
  242. package/src/deploy/node-runtime/elf.ts +3 -3
  243. package/src/deploy/node-runtime/index.ts +2 -2
  244. package/src/deploy/notices/collect.ts +4 -2
  245. package/src/deploy/notices/packages.ts +5 -43
  246. package/src/deploy/payload/icons.ts +64 -34
  247. package/src/deploy/payload/launcher.ts +2 -0
  248. package/src/deploy/payload/stage.ts +40 -12
  249. package/src/deploy/run-deploy.ts +214 -24
  250. package/src/deploy/settings/build-manifest.ts +122 -0
  251. package/src/deploy/settings/index.ts +9 -4
  252. package/src/deploy/settings/libraries.ts +57 -0
  253. package/src/deploy/settings/paths.ts +10 -47
  254. package/src/deploy/targets/appimage.ts +32 -15
  255. package/src/deploy/targets/flatpak-manifest.ts +7 -1
  256. package/src/deploy/targets/flatpak-source.ts +42 -20
  257. package/src/deploy/tools.ts +35 -0
  258. package/src/deploy/types.ts +4 -4
  259. package/src/dev/icon-dir.ts +31 -7
  260. package/src/dev/locale-dir.ts +22 -0
  261. package/src/dev/runner-deps.ts +5 -1
  262. package/src/dev/runner-main.ts +6 -4
  263. package/src/dev/runner.ts +10 -2
  264. package/src/dev/schema-dir.ts +3 -1
  265. package/src/dev/vite-dev-server.ts +1 -0
  266. package/src/i18n/binding-extractor.ts +1969 -0
  267. package/src/i18n/catalog-template.ts +24 -0
  268. package/src/i18n/catalogs.ts +216 -0
  269. package/src/i18n/extractor-plugin.ts +413 -0
  270. package/src/i18n/metadata-templates.ts +29 -0
  271. package/src/i18n/source-messages.ts +608 -0
  272. package/src/i18n/types.ts +154 -0
  273. package/src/internal/build-manifest.ts +32 -0
  274. package/src/internal/icon-path.ts +127 -0
  275. package/src/internal/source-imports.ts +171 -0
  276. package/src/mcp/client.ts +17 -44
  277. package/src/mcp/handlers.ts +7 -6
  278. package/src/mcp/widget-properties.ts +3 -14
  279. package/src/settings/render.ts +113 -21
  280. package/src/settings/schema.ts +192 -17
  281. package/src/vite-plugins/asset-emitter.ts +1 -1
  282. package/src/vite-plugins/asset-extensions.ts +1 -0
  283. package/src/vite-plugins/asset-imports.ts +62 -33
  284. package/src/vite-plugins/asset-specifier.ts +73 -1
  285. package/src/vite-plugins/{bundled-packages.ts → build-manifest.ts} +30 -19
  286. package/src/vite-plugins/built-url.ts +49 -28
  287. package/src/vite-plugins/i18n.ts +146 -0
  288. package/src/vite-plugins/icons.ts +33 -14
  289. package/src/vite-plugins/index.ts +15 -5
  290. package/src/vite-plugins/native.ts +31 -4
  291. package/src/vite-plugins/resource-init-module.ts +44 -15
  292. package/src/vite-plugins/resource-shared.ts +4 -0
  293. package/src/vite-plugins/resources.ts +1397 -100
  294. package/src/vite-plugins/self-contained.ts +150 -169
  295. package/src/vite-plugins/settings-worker-env.ts +7 -3
  296. package/src/vite-plugins/settings.ts +89 -21
  297. package/dist/vite-plugins/bundled-packages.d.ts +0 -5
  298. package/dist/vite-plugins/bundled-packages.d.ts.map +0 -1
  299. package/dist/vite-plugins/bundled-packages.js.map +0 -1
@@ -0,0 +1,154 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import type { SourceMessage } from "./source-messages.js";
4
+
5
+ type I18nTypesResult = {
6
+ isWritten: boolean;
7
+ path: string;
8
+ };
9
+
10
+ const I18N_TYPES_FILENAME = "i18n.d.ts";
11
+ const INTERPOLATION_PATTERN = /\{\{[^{}]+\}\}/u;
12
+
13
+ const stringType = (values: string[]): string =>
14
+ [...new Set(values)].map((value) => JSON.stringify(value)).join(" | ");
15
+
16
+ const contextField = (context: string | null): string =>
17
+ context === null ? "" : ` context: ${JSON.stringify(context)};`;
18
+
19
+ const hasInterpolation = (values: string[]): boolean => values.some((value) => INTERPOLATION_PATTERN.test(value));
20
+
21
+ const pointVariant = (message: SourceMessage): string => {
22
+ const isRequired = message.context !== null || hasInterpolation([message.msgid]);
23
+ const isInterpolated = hasInterpolation([message.msgid]);
24
+
25
+ return (
26
+ `{${contextField(message.context)} interpolated: ${String(isInterpolated)}; kind: "point"; ` +
27
+ `required: ${String(isRequired)}; value: ${JSON.stringify(message.msgid)} }`
28
+ );
29
+ };
30
+
31
+ const defaultVariant = (message: SourceMessage): string => {
32
+ const values = [message.singular];
33
+ const isInterpolated = hasInterpolation(values);
34
+ const isRequired = message.context !== null || isInterpolated;
35
+
36
+ return (
37
+ `{${contextField(message.context)} defaultValue: ${JSON.stringify(message.singular)}; ` +
38
+ `interpolated: ${String(isInterpolated)}; kind: "default"; required: ${String(isRequired)}; ` +
39
+ `value: ${stringType(values)} }`
40
+ );
41
+ };
42
+
43
+ const requirePlural = (message: SourceMessage): string => {
44
+ if (message.plural === null) {
45
+ throw new Error("Plural translation metadata requires a plural source string");
46
+ }
47
+
48
+ return message.plural;
49
+ };
50
+
51
+ const pluralVariant = (message: SourceMessage): string => {
52
+ const plural = requirePlural(message);
53
+ const values = [message.singular, plural];
54
+ const isInterpolated = hasInterpolation(values);
55
+
56
+ return (
57
+ `{${contextField(message.context)} defaultValue: ${JSON.stringify(plural)}; ` +
58
+ `interpolated: ${String(isInterpolated)}; kind: "plural"; required: true; ` +
59
+ `value: ${stringType(values)} }`
60
+ );
61
+ };
62
+
63
+ const pluralDefaultsVariant = (message: SourceMessage): string => {
64
+ const plural = requirePlural(message);
65
+ const values = [message.singular, plural];
66
+ const isInterpolated = hasInterpolation(values);
67
+
68
+ return (
69
+ `{${contextField(message.context)} defaultValueOne: ${JSON.stringify(message.singular)}; ` +
70
+ `defaultValueOther: ${JSON.stringify(plural)}; interpolated: ${String(isInterpolated)}; ` +
71
+ `kind: "pluralDefaults"; required: true; value: ${stringType(values)} }`
72
+ );
73
+ };
74
+
75
+ const variantsFor = (message: SourceMessage): string[] => {
76
+ if (message.kind === "plural") {
77
+ return [pluralVariant(message)];
78
+ }
79
+
80
+ if (message.kind === "pluralDefaults") {
81
+ return [pluralDefaultsVariant(message)];
82
+ }
83
+
84
+ return message.kind === "default" ? [defaultVariant(message)] : [pointVariant(message)];
85
+ };
86
+
87
+ const renderRegistry = (messages: SourceMessage[]): string[] => {
88
+ const entries: Map<string, Set<string>> = new Map();
89
+
90
+ for (const message of messages) {
91
+ const variants = entries.get(message.msgid) ?? new Set<string>();
92
+
93
+ for (const variant of variantsFor(message)) {
94
+ variants.add(variant);
95
+ }
96
+
97
+ entries.set(message.msgid, variants);
98
+ }
99
+
100
+ return [...entries]
101
+ .toSorted(([left], [right]) => left.localeCompare(right))
102
+ .map(([msgid, variants]) => ` ${JSON.stringify(msgid)}: ${[...variants].join(" | ")};`);
103
+ };
104
+
105
+ const generatedHeader = (): string[] => [
106
+ "/**",
107
+ " * Translation declarations generated by GTKX. Regenerated by",
108
+ " * `gtkx codegen`, `gtkx dev`, and `gtkx build`; do not edit.",
109
+ " */",
110
+ ];
111
+
112
+ const renderI18nTypes = (messages: SourceMessage[], isEnabled = true): string => {
113
+ if (!isEnabled) {
114
+ return [...generatedHeader(), "export {};", ""].join("\n");
115
+ }
116
+
117
+ return [
118
+ ...generatedHeader(),
119
+ 'import "@gtkx/i18n";',
120
+ "",
121
+ 'declare module "@gtkx/i18n" {',
122
+ " interface TranslationRegistry {",
123
+ ...renderRegistry(messages),
124
+ " }",
125
+ "}",
126
+ "",
127
+ ].join("\n");
128
+ };
129
+
130
+ const i18nTypesPath = (root: string): string => join(root, "node_modules", ".gtkx", I18N_TYPES_FILENAME);
131
+
132
+ const readFileOrNull = (path: string): string | null => {
133
+ try {
134
+ return readFileSync(path, "utf8");
135
+ } catch {
136
+ return null;
137
+ }
138
+ };
139
+
140
+ const emitI18nTypes = (root: string, messages: SourceMessage[], isEnabled = true): I18nTypesResult => {
141
+ const path = i18nTypesPath(root);
142
+ const content = renderI18nTypes(messages, isEnabled);
143
+
144
+ if (readFileOrNull(path) === content) {
145
+ return { isWritten: false, path };
146
+ }
147
+
148
+ mkdirSync(dirname(path), { recursive: true });
149
+ writeFileSync(path, content);
150
+
151
+ return { isWritten: true, path };
152
+ };
153
+
154
+ export { I18N_TYPES_FILENAME, emitI18nTypes, i18nTypesPath };
@@ -0,0 +1,32 @@
1
+ type RecordedPackage = {
2
+ name: string;
3
+ version: string | null;
4
+ dir: string;
5
+ };
6
+
7
+ type BuildManifest = {
8
+ generator: typeof BUILD_MANIFEST_GENERATOR;
9
+ formatVersion: typeof BUILD_MANIFEST_FORMAT_VERSION;
10
+ schemas: string[];
11
+ packages: RecordedPackage[];
12
+ };
13
+
14
+ type BuildManifestCollector = {
15
+ schemas: string[];
16
+ };
17
+
18
+ const BUILD_MANIFEST_FILENAME = "gtkx-schemas.json";
19
+ const BUILD_MANIFEST_GENERATOR = "gtkx-build";
20
+ const BUILD_MANIFEST_FORMAT_VERSION = 1;
21
+
22
+ const createBuildManifestCollector = (): BuildManifestCollector => ({ schemas: [] });
23
+
24
+ export {
25
+ BUILD_MANIFEST_FILENAME,
26
+ BUILD_MANIFEST_FORMAT_VERSION,
27
+ BUILD_MANIFEST_GENERATOR,
28
+ createBuildManifestCollector,
29
+ type BuildManifest,
30
+ type BuildManifestCollector,
31
+ type RecordedPackage,
32
+ };
@@ -0,0 +1,127 @@
1
+ import { type Stats, statSync } from "node:fs";
2
+ import { basename, extname, isAbsolute, join, relative, resolve } from "node:path";
3
+
4
+ type ResolvedApplicationIcon = { kind: "file"; path: string } |
5
+ { kind: "none" } |
6
+ { kind: "theme"; path: string };
7
+
8
+ const ICON_EXTENSIONS: Set<string> = new Set([".svg", ".png", ".xpm"]);
9
+ const SCALABLE_DIR = "hicolor/scalable/apps";
10
+ const SIZE_PATTERN = /^(?<width>[1-9]\d{0,3})x(?<height>[1-9]\d{0,3})$/;
11
+ const SIZE_SEPARATORS = /[-_.]/;
12
+
13
+ const isInside = (parent: string, candidate: string): boolean => {
14
+ const rel = relative(parent, candidate);
15
+
16
+ return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
17
+ };
18
+
19
+ const getStats = (path: string): Stats | undefined => {
20
+ try {
21
+ return statSync(path, { throwIfNoEntry: false });
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ };
26
+
27
+ const iconExtension = (file: string): string => {
28
+ const extension = extname(file).toLowerCase();
29
+
30
+ if (!ICON_EXTENSIONS.has(extension)) {
31
+ throw new Error(`Cannot use ${file} as an application icon: expected an SVG, PNG, or XPM file`);
32
+ }
33
+
34
+ return extension;
35
+ };
36
+
37
+ const defaultIconFiles = (root: string, applicationId: string): string[] =>
38
+ [...ICON_EXTENSIONS]
39
+ .map((extension) => join(root, `${applicationId}${extension}`))
40
+ .filter((path) => getStats(path)?.isFile() === true);
41
+
42
+ const resolveDefaultApplicationIcon = (root: string, applicationId: string): ResolvedApplicationIcon => {
43
+ const files = defaultIconFiles(root, applicationId);
44
+
45
+ if (files.length === 0) {
46
+ return { kind: "none" };
47
+ }
48
+
49
+ if (files.length > 1) {
50
+ throw new Error(
51
+ `Found multiple default application icons for ${applicationId}; set \`applicationIcon\` to choose one`,
52
+ );
53
+ }
54
+
55
+ const path = files[0];
56
+
57
+ if (path === undefined) {
58
+ return { kind: "none" };
59
+ }
60
+
61
+ return { kind: "file", path };
62
+ };
63
+
64
+ const resolveApplicationIcon = (
65
+ root: string,
66
+ applicationId: string,
67
+ configured: string | undefined,
68
+ ): ResolvedApplicationIcon => {
69
+ if (configured === undefined) {
70
+ return resolveDefaultApplicationIcon(root, applicationId);
71
+ }
72
+
73
+ if (isAbsolute(configured)) {
74
+ throw new Error(`Cannot use "${configured}" as the application icon: expected a project-relative path`);
75
+ }
76
+
77
+ const path = resolve(root, configured);
78
+
79
+ if (!isInside(root, path)) {
80
+ throw new Error(`Cannot use "${configured}" as the application icon: it is outside ${root}`);
81
+ }
82
+
83
+ const stats = getStats(path);
84
+
85
+ if (stats?.isDirectory() === true) {
86
+ return { kind: "theme", path };
87
+ }
88
+
89
+ if (stats?.isFile() !== true) {
90
+ throw new Error(`Cannot read the application icon "${configured}": no such file or directory under ${root}`);
91
+ }
92
+
93
+ iconExtension(path);
94
+
95
+ return { kind: "file", path };
96
+ };
97
+
98
+ const squareSize = (token: string): number | null => {
99
+ const groups = SIZE_PATTERN.exec(token)?.groups;
100
+
101
+ if (groups === undefined || groups.width !== groups.height) {
102
+ return null;
103
+ }
104
+
105
+ return Number(groups.width);
106
+ };
107
+
108
+ const iconThemeDir = (file: string, extension: string): string => {
109
+ if (extension === ".svg") {
110
+ return SCALABLE_DIR;
111
+ }
112
+
113
+ const size = basename(file)
114
+ .split(SIZE_SEPARATORS)
115
+ .map((token) => squareSize(token))
116
+ .find((value) => value !== null);
117
+
118
+ return size === undefined ? SCALABLE_DIR : `hicolor/${String(size)}x${String(size)}/apps`;
119
+ };
120
+
121
+ const relativeIconPath = (applicationId: string, file: string): string => {
122
+ const extension = iconExtension(file);
123
+
124
+ return `${iconThemeDir(file, extension)}/${applicationId}${extension}`;
125
+ };
126
+
127
+ export { relativeIconPath, resolveApplicationIcon, type ResolvedApplicationIcon };
@@ -0,0 +1,171 @@
1
+ import type { ESTree, ParseResult, ParserOptions } from "vite";
2
+ import { sortStringsBy } from "@gtkx/utils";
3
+ import { type Dirent, readdirSync, readFileSync } from "node:fs";
4
+ import { extname, join } from "node:path";
5
+ import { parseSync, Visitor } from "vite";
6
+
7
+ type SourceImport = {
8
+ importer: string;
9
+ source: string;
10
+ };
11
+
12
+ type SourceLanguage = NonNullable<ParserOptions["lang"]>;
13
+ type ParsedModule = ParseResult["module"];
14
+
15
+ const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
16
+ [".cjs", "jsx"],
17
+ [".cts", "ts"],
18
+ [".js", "jsx"],
19
+ [".jsx", "jsx"],
20
+ [".mjs", "jsx"],
21
+ [".mts", "ts"],
22
+ [".ts", "ts"],
23
+ [".tsx", "tsx"],
24
+ ]);
25
+
26
+ const EXCLUDED_DIRECTORIES: Set<string> = new Set([
27
+ ".git",
28
+ ".gtkx",
29
+ "build",
30
+ "coverage",
31
+ "dist",
32
+ "node_modules",
33
+ "out-tsc",
34
+ ]);
35
+
36
+ const sourceLanguage = (path: string): SourceLanguage | undefined => {
37
+ if (path.endsWith(".d.ts") || path.endsWith(".d.cts") || path.endsWith(".d.mts")) {
38
+ return undefined;
39
+ }
40
+
41
+ return SOURCE_LANGUAGES.get(extname(path).toLowerCase());
42
+ };
43
+
44
+ const collectSourceEntry = (dir: string, entry: Dirent, found: string[]): void => {
45
+ const path = join(dir, entry.name);
46
+
47
+ if (entry.isDirectory()) {
48
+ if (!EXCLUDED_DIRECTORIES.has(entry.name)) {
49
+ sourceFilesIn(path, found);
50
+ }
51
+
52
+ return;
53
+ }
54
+
55
+ if (entry.isFile() && sourceLanguage(path) !== undefined) {
56
+ found.push(path);
57
+ }
58
+ };
59
+
60
+ const sourceFilesIn = (dir: string, found: string[]): void => {
61
+ let entries: Dirent[];
62
+
63
+ try {
64
+ entries = readdirSync(dir, { withFileTypes: true });
65
+ } catch {
66
+ return;
67
+ }
68
+
69
+ for (const entry of entries) {
70
+ collectSourceEntry(dir, entry, found);
71
+ }
72
+ };
73
+
74
+ const discoverSourceFiles = (dir: string): string[] => {
75
+ const files: string[] = [];
76
+ sourceFilesIn(dir, files);
77
+
78
+ return sortStringsBy(files, (path) => path);
79
+ };
80
+
81
+ const staticImportSources = (module: ParsedModule): string[] => [
82
+ ...module.staticImports.map((statement) => statement.moduleRequest.value),
83
+ ...module.staticExports.flatMap((statement) =>
84
+ statement.entries
85
+ .map((entry) => entry.moduleRequest?.value)
86
+ .filter((source): source is string => source !== undefined),
87
+ ),
88
+ ];
89
+
90
+ const staticRuntimeImportSources = (module: ParsedModule): string[] => [
91
+ ...module.staticImports
92
+ .filter((statement) => statement.entries.length === 0 || statement.entries.some((entry) => !entry.isType))
93
+ .map((statement) => statement.moduleRequest.value),
94
+ ...module.staticExports.flatMap((statement) =>
95
+ statement.entries
96
+ .filter((entry) => !entry.isType)
97
+ .map((entry) => entry.moduleRequest?.value)
98
+ .filter((source): source is string => source !== undefined),
99
+ ),
100
+ ];
101
+
102
+ const templateImportSource = (node: ESTree.TemplateLiteral): string | null => {
103
+ const [quasi] = node.quasis;
104
+
105
+ return quasi === undefined || node.expressions.length > 0 ? null : quasi.value.cooked;
106
+ };
107
+
108
+ const literalImportSource = (node: ESTree.Expression): string | null => {
109
+ if (node.type === "TemplateLiteral") {
110
+ return templateImportSource(node);
111
+ }
112
+
113
+ return node.type === "Literal" && typeof node.value === "string" ? node.value : null;
114
+ };
115
+
116
+ const collectDynamicImportSources = (program: ESTree.Program, found: string[]): void => {
117
+ new Visitor({
118
+ ImportExpression: (node) => {
119
+ const source = literalImportSource(node.source);
120
+
121
+ if (source !== null) {
122
+ found.push(source);
123
+ }
124
+ },
125
+ }).visit(program);
126
+ };
127
+
128
+ const parseImportsInWith = (
129
+ path: string,
130
+ staticSources: (module: ParsedModule) => string[],
131
+ ): SourceImport[] | null => {
132
+ const lang = sourceLanguage(path);
133
+
134
+ if (lang === undefined) {
135
+ return [];
136
+ }
137
+
138
+ let parsed: ParseResult;
139
+
140
+ try {
141
+ parsed = parseSync(path, readFileSync(path, "utf8"), { lang });
142
+ } catch {
143
+ return null;
144
+ }
145
+
146
+ if (parsed.errors.length > 0) {
147
+ return null;
148
+ }
149
+
150
+ const sources = staticSources(parsed.module);
151
+ collectDynamicImportSources(parsed.program, sources);
152
+
153
+ return [...new Set(sources)].map((source) => ({ importer: path, source }));
154
+ };
155
+
156
+ const parseImportsIn = (path: string): SourceImport[] | null => parseImportsInWith(path, staticImportSources);
157
+
158
+ const parseRuntimeImportsIn = (path: string): SourceImport[] | null =>
159
+ parseImportsInWith(path, staticRuntimeImportSources);
160
+
161
+ const importsIn = (path: string): SourceImport[] => parseImportsIn(path) ?? [];
162
+ const importKey = (entry: SourceImport): string => `${entry.importer}\0${entry.source}`;
163
+
164
+ const discoverSourceImports = (dir: string): SourceImport[] => {
165
+ const files = discoverSourceFiles(dir);
166
+ const imports = files.flatMap((path) => importsIn(path));
167
+
168
+ return sortStringsBy(imports, importKey);
169
+ };
170
+
171
+ export { discoverSourceFiles, discoverSourceImports, parseRuntimeImportsIn, type SourceImport, sourceLanguage };
package/src/mcp/client.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import * as Gio from "@gtkx/gi/gio";
2
2
  import * as Gtk from "@gtkx/gi/gtk";
3
- import { DEFAULT_SOCKET_PATH, ErrorCode, ProtocolConnection, ProtocolError, type Request } from "@gtkx/mcp/internal";
4
- import { error, errorMessage, info, normalizeError, warn } from "@gtkx/utils";
3
+ import {
4
+ DEFAULT_SOCKET_PATH,
5
+ isConnectionClosedError,
6
+ type JSONRPCRequest,
7
+ ProtocolConnection,
8
+ type Result,
9
+ } from "@gtkx/mcp/internal";
10
+ import { error, errorMessage, info, normalizeError } from "@gtkx/utils";
5
11
  import * as net from "node:net";
6
12
  import { dispatch } from "./handlers.js";
7
13
  import { WidgetRegistry } from "./widget-registry.js";
@@ -25,11 +31,6 @@ const RECONNECT_DELAY_MS = 2000;
25
31
  const REGISTER_TIMEOUT_MS = 30_000;
26
32
  const DISCONNECT_ERROR_CODES: Set<string> = new Set(["ENOENT", "ECONNREFUSED", "EPIPE", "ECONNRESET"]);
27
33
 
28
- const toResponseError = (error: unknown): { code: number; message: string; data?: unknown } =>
29
- error instanceof ProtocolError
30
- ? error.toErrorObject()
31
- : { code: ErrorCode.INTERNAL_ERROR, message: errorMessage(error) };
32
-
33
34
  const connectSettler = (callbacks: ConnectCallbacks): ConnectSettler => {
34
35
  let isSettled = false;
35
36
 
@@ -82,7 +83,7 @@ class McpClient {
82
83
  private handleSocketError(socketError: Error): void {
83
84
  const code = (socketError as NodeJS.ErrnoException).code;
84
85
 
85
- if (code !== undefined && DISCONNECT_ERROR_CODES.has(code)) {
86
+ if (isConnectionClosedError(socketError) || (code !== undefined && DISCONNECT_ERROR_CODES.has(code))) {
86
87
  this.scheduleReconnect();
87
88
  } else {
88
89
  error("Socket error:", socketError.message);
@@ -119,14 +120,7 @@ class McpClient {
119
120
  },
120
121
  });
121
122
 
122
- connection.on("request", (request) => {
123
- void this.handleRequest(request);
124
- });
125
-
126
- connection.on("invalid", ({ error: parseError }) => {
127
- warn(`Received invalid JSON from MCP server: ${parseError.message}`);
128
- });
129
-
123
+ connection.fallbackRequestHandler = (request) => this.handleRequest(request);
130
124
  this.socket = socket;
131
125
  this.connection = connection;
132
126
  }
@@ -158,35 +152,16 @@ class McpClient {
158
152
  );
159
153
  }
160
154
 
161
- private async respondToRequest(request: Request): Promise<void> {
162
- const { id, method, params } = request;
163
- const connection = this.connection;
155
+ private handleRequest(request: JSONRPCRequest): Promise<Result> {
156
+ const defaultApp = Gio.Application.getDefault();
164
157
 
165
- if (!connection) {
166
- return;
158
+ if (!(defaultApp instanceof Gtk.Application)) {
159
+ throw new TypeError("Application not initialized");
167
160
  }
168
161
 
169
- try {
170
- const defaultApp = Gio.Application.getDefault();
171
-
172
- if (!(defaultApp instanceof Gtk.Application)) {
173
- throw new TypeError("Application not initialized");
174
- }
162
+ this.registry.refresh();
175
163
 
176
- this.registry.refresh();
177
- const result = await dispatch(method, params, { app: defaultApp, registry: this.registry });
178
- connection.write({ id, result });
179
- } catch (dispatchError) {
180
- connection.write({ id, error: toResponseError(dispatchError) });
181
- }
182
- }
183
-
184
- private async handleRequest(request: Request): Promise<void> {
185
- try {
186
- await this.respondToRequest(request);
187
- } catch (requestError) {
188
- error("Error handling request:", requestError);
189
- }
164
+ return dispatch(request.method, request.params, { app: defaultApp, registry: this.registry });
190
165
  }
191
166
 
192
167
  async connect(): Promise<void> {
@@ -219,10 +194,8 @@ class McpClient {
219
194
  this.reconnectTimer = null;
220
195
  }
221
196
 
222
- this.connection?.rejectPending(new Error("Client disconnected"));
223
-
224
197
  if (this.socket) {
225
- this.connection?.write({ id: crypto.randomUUID(), method: "app.unregister" });
198
+ void Promise.allSettled([this.connection?.send("app.unregister")]);
226
199
  this.socket.destroy();
227
200
  this.socket = null;
228
201
  }
@@ -3,6 +3,7 @@ import {
3
3
  invalidRequestError,
4
4
  methodNotFoundError,
5
5
  type ParamsSchema,
6
+ type Result,
6
7
  type ServerInitiatedMethod,
7
8
  type ServerRequestParams,
8
9
  ServerRequestParamsSchemas,
@@ -18,10 +19,10 @@ type HandlerContext = {
18
19
  registry: WidgetRegistry;
19
20
  };
20
21
 
21
- type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<unknown>;
22
+ type ValidatedHandler = (ctx: HandlerContext, params: unknown) => Promise<Result>;
22
23
  type WidgetTarget = { testing: TestingModule; widget: Gtk.Widget };
23
24
  type WidgetParams = { widgetId?: string | undefined };
24
- type TargetedHandler<Params> = (ctx: HandlerContext, target: WidgetTarget, params: Params) => Promise<unknown>;
25
+ type TargetedHandler<Params> = (ctx: HandlerContext, target: WidgetTarget, params: Params) => Promise<Result>;
25
26
  type QueryParams = ServerRequestParams<"widget.query">;
26
27
  type QueryBy = QueryParams["by"];
27
28
  type QueryRunner = (testing: TestingModule, app: Gtk.Application, params: QueryParams) => Promise<Gtk.Widget[]>;
@@ -110,7 +111,7 @@ const SEARCHED_BY: Record<QueryBy, string> = {
110
111
 
111
112
  function validated<Params>(
112
113
  schema: ParamsSchema<Params>,
113
- handler: (ctx: HandlerContext, params: Params) => Promise<unknown>,
114
+ handler: (ctx: HandlerContext, params: Params) => Promise<Result>,
114
115
  ): ValidatedHandler {
115
116
  return (ctx, params) => {
116
117
  const parsed = schema.safeParse(params ?? {});
@@ -229,7 +230,7 @@ const emptyQueryHint = (params: QueryParams): string =>
229
230
  "Call gtkx_get_widget_tree to see what is mounted; by:\"name\" is the widest match, and by:\"role\" accepts " +
230
231
  "options.name to match the accessible name of a known role.";
231
232
 
232
- async function handleQuery({ app, registry }: HandlerContext, params: QueryParams): Promise<unknown> {
233
+ async function handleQuery({ app, registry }: HandlerContext, params: QueryParams): Promise<Result> {
233
234
  const testing = await loadTestingModule();
234
235
  const widgets = await QUERY_RUNNERS[params.by](testing, app, params);
235
236
  const resolveId = (widget: Gtk.Widget): string => registry.getOrCreateId(widget);
@@ -254,7 +255,7 @@ const defaultScreenshotTarget = (registry: WidgetRegistry): Gtk.Widget => {
254
255
  async function handleScreenshot(
255
256
  { registry }: HandlerContext,
256
257
  params: ServerRequestParams<"widget.screenshot">,
257
- ): Promise<unknown> {
258
+ ): Promise<Result> {
258
259
  const testing = await loadTestingModule();
259
260
  const target = params.windowId ? requireWidget(registry, params.windowId) : defaultScreenshotTarget(registry);
260
261
  const result = await testing.screenshot(target, { path: params.path });
@@ -269,7 +270,7 @@ async function handleScreenshot(
269
270
  const isServerInitiatedMethod = (method: string): method is ServerInitiatedMethod =>
270
271
  Object.hasOwn(ServerRequestParamsSchemas, method);
271
272
 
272
- const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<unknown> => {
273
+ const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<Result> => {
273
274
  if (!isServerInitiatedMethod(method)) {
274
275
  throw methodNotFoundError(method);
275
276
  }
@@ -6,13 +6,14 @@ import {
6
6
  getHandle,
7
7
  getInstanceType,
8
8
  type Handle,
9
+ isReadableProperty,
9
10
  TYPE_ENUM,
10
11
  TYPE_FLAGS,
11
12
  typeIsA,
12
13
  typeName,
13
14
  } from "@gtkx/runtime";
14
15
  import { fromValue, getValueType } from "@gtkx/runtime/internal";
15
- import { camelCase, errorMessage, kebabCase } from "@gtkx/utils";
16
+ import { errorMessage, kebabCase } from "@gtkx/utils";
16
17
  import type { WidgetRegistry } from "./widget-registry.js";
17
18
 
18
19
  type PropertyRepresentation = Omit<SerializedProperty, "type">;
@@ -39,18 +40,6 @@ function prototypesFor(widget: Gtk.Widget): object[] {
39
40
  return prototypes;
40
41
  }
41
42
 
42
- function getAccessor(widget: Gtk.Widget, member: string): PropertyDescriptor | undefined {
43
- for (const prototype of prototypesFor(widget)) {
44
- const descriptor = Object.getOwnPropertyDescriptor(prototype, member);
45
-
46
- if (descriptor !== undefined) {
47
- return descriptor;
48
- }
49
- }
50
-
51
- return undefined;
52
- }
53
-
54
43
  function getReadableNames(prototype: object): string[] {
55
44
  const descriptors = Object.entries(Object.getOwnPropertyDescriptors(prototype));
56
45
 
@@ -64,7 +53,7 @@ function readablePropertyNames(widget: Gtk.Widget): string[] {
64
53
  }
65
54
 
66
55
  const requireReadableProperty = (widget: Gtk.Widget, property: string): void => {
67
- if (getAccessor(widget, camelCase(property))?.get !== undefined) {
56
+ if (isReadableProperty(getInstanceType(widget), property)) {
68
57
  return;
69
58
  }
70
59