@gtkx/cli 1.4.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 (143) hide show
  1. package/README.md +1 -1
  2. package/dist/builder.d.ts +1 -0
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +2 -2
  5. package/dist/builder.js.map +1 -1
  6. package/dist/codegen/run-codegen.d.ts +2 -0
  7. package/dist/codegen/run-codegen.d.ts.map +1 -1
  8. package/dist/codegen/run-codegen.js +30 -6
  9. package/dist/codegen/run-codegen.js.map +1 -1
  10. package/dist/commands/codegen.js +1 -1
  11. package/dist/commands/codegen.js.map +1 -1
  12. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  13. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  14. package/dist/deploy/freedesktop/localize.js +107 -0
  15. package/dist/deploy/freedesktop/localize.js.map +1 -0
  16. package/dist/deploy/install-hints.d.ts.map +1 -1
  17. package/dist/deploy/install-hints.js +5 -0
  18. package/dist/deploy/install-hints.js.map +1 -1
  19. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  20. package/dist/deploy/payload/launcher.js +2 -0
  21. package/dist/deploy/payload/launcher.js.map +1 -1
  22. package/dist/deploy/payload/stage.d.ts +1 -1
  23. package/dist/deploy/payload/stage.d.ts.map +1 -1
  24. package/dist/deploy/payload/stage.js +20 -11
  25. package/dist/deploy/payload/stage.js.map +1 -1
  26. package/dist/deploy/run-deploy.d.ts.map +1 -1
  27. package/dist/deploy/run-deploy.js +87 -15
  28. package/dist/deploy/run-deploy.js.map +1 -1
  29. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  30. package/dist/deploy/targets/flatpak-source.js +21 -10
  31. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  32. package/dist/deploy/tools.d.ts +6 -1
  33. package/dist/deploy/tools.d.ts.map +1 -1
  34. package/dist/deploy/tools.js +26 -1
  35. package/dist/deploy/tools.js.map +1 -1
  36. package/dist/dev/locale-dir.d.ts +3 -0
  37. package/dist/dev/locale-dir.d.ts.map +1 -0
  38. package/dist/dev/locale-dir.js +17 -0
  39. package/dist/dev/locale-dir.js.map +1 -0
  40. package/dist/dev/runner-main.d.ts.map +1 -1
  41. package/dist/dev/runner-main.js +2 -0
  42. package/dist/dev/runner-main.js.map +1 -1
  43. package/dist/dev/runner.d.ts.map +1 -1
  44. package/dist/dev/runner.js +6 -0
  45. package/dist/dev/runner.js.map +1 -1
  46. package/dist/dev/vite-dev-server.d.ts +1 -0
  47. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  48. package/dist/dev/vite-dev-server.js.map +1 -1
  49. package/dist/i18n/binding-extractor.d.ts +30 -0
  50. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  51. package/dist/i18n/binding-extractor.js +1324 -0
  52. package/dist/i18n/binding-extractor.js.map +1 -0
  53. package/dist/i18n/catalog-template.d.ts +3 -0
  54. package/dist/i18n/catalog-template.d.ts.map +1 -0
  55. package/dist/i18n/catalog-template.js +18 -0
  56. package/dist/i18n/catalog-template.js.map +1 -0
  57. package/dist/i18n/catalogs.d.ts +21 -0
  58. package/dist/i18n/catalogs.d.ts.map +1 -0
  59. package/dist/i18n/catalogs.js +139 -0
  60. package/dist/i18n/catalogs.js.map +1 -0
  61. package/dist/i18n/extractor-plugin.d.ts +8 -0
  62. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  63. package/dist/i18n/extractor-plugin.js +247 -0
  64. package/dist/i18n/extractor-plugin.js.map +1 -0
  65. package/dist/i18n/metadata-templates.d.ts +11 -0
  66. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  67. package/dist/i18n/metadata-templates.js +11 -0
  68. package/dist/i18n/metadata-templates.js.map +1 -0
  69. package/dist/i18n/source-messages.d.ts +23 -0
  70. package/dist/i18n/source-messages.d.ts.map +1 -0
  71. package/dist/i18n/source-messages.js +403 -0
  72. package/dist/i18n/source-messages.js.map +1 -0
  73. package/dist/i18n/types.d.ts +10 -0
  74. package/dist/i18n/types.d.ts.map +1 -0
  75. package/dist/i18n/types.js +108 -0
  76. package/dist/i18n/types.js.map +1 -0
  77. package/dist/internal/source-imports.d.ts +5 -1
  78. package/dist/internal/source-imports.d.ts.map +1 -1
  79. package/dist/internal/source-imports.js +23 -44
  80. package/dist/internal/source-imports.js.map +1 -1
  81. package/dist/mcp/client.d.ts +0 -1
  82. package/dist/mcp/client.d.ts.map +1 -1
  83. package/dist/mcp/client.js +11 -39
  84. package/dist/mcp/client.js.map +1 -1
  85. package/dist/mcp/handlers.d.ts +2 -1
  86. package/dist/mcp/handlers.d.ts.map +1 -1
  87. package/dist/mcp/handlers.js.map +1 -1
  88. package/dist/settings/render.d.ts +4 -1
  89. package/dist/settings/render.d.ts.map +1 -1
  90. package/dist/settings/render.js +11 -2
  91. package/dist/settings/render.js.map +1 -1
  92. package/dist/settings/schema.d.ts.map +1 -1
  93. package/dist/settings/schema.js +3 -1
  94. package/dist/settings/schema.js.map +1 -1
  95. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  96. package/dist/vite-plugins/asset-imports.js +2 -12
  97. package/dist/vite-plugins/asset-imports.js.map +1 -1
  98. package/dist/vite-plugins/i18n.d.ts +5 -0
  99. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  100. package/dist/vite-plugins/i18n.js +88 -0
  101. package/dist/vite-plugins/i18n.js.map +1 -0
  102. package/dist/vite-plugins/index.d.ts +1 -1
  103. package/dist/vite-plugins/index.d.ts.map +1 -1
  104. package/dist/vite-plugins/index.js +3 -1
  105. package/dist/vite-plugins/index.js.map +1 -1
  106. package/dist/vite-plugins/resources.d.ts.map +1 -1
  107. package/dist/vite-plugins/resources.js +10 -55
  108. package/dist/vite-plugins/resources.js.map +1 -1
  109. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  110. package/dist/vite-plugins/self-contained.js +128 -135
  111. package/dist/vite-plugins/self-contained.js.map +1 -1
  112. package/package.json +10 -9
  113. package/src/builder.ts +3 -2
  114. package/src/codegen/run-codegen.ts +53 -7
  115. package/src/commands/codegen.ts +1 -1
  116. package/src/deploy/freedesktop/localize.ts +144 -0
  117. package/src/deploy/install-hints.ts +5 -0
  118. package/src/deploy/payload/launcher.ts +2 -0
  119. package/src/deploy/payload/stage.ts +35 -10
  120. package/src/deploy/run-deploy.ts +128 -13
  121. package/src/deploy/targets/flatpak-source.ts +33 -10
  122. package/src/deploy/tools.ts +35 -0
  123. package/src/dev/locale-dir.ts +22 -0
  124. package/src/dev/runner-main.ts +2 -0
  125. package/src/dev/runner.ts +8 -0
  126. package/src/dev/vite-dev-server.ts +1 -0
  127. package/src/i18n/binding-extractor.ts +1969 -0
  128. package/src/i18n/catalog-template.ts +24 -0
  129. package/src/i18n/catalogs.ts +216 -0
  130. package/src/i18n/extractor-plugin.ts +413 -0
  131. package/src/i18n/metadata-templates.ts +29 -0
  132. package/src/i18n/source-messages.ts +608 -0
  133. package/src/i18n/types.ts +154 -0
  134. package/src/internal/source-imports.ts +28 -57
  135. package/src/mcp/client.ts +17 -44
  136. package/src/mcp/handlers.ts +7 -6
  137. package/src/settings/render.ts +16 -2
  138. package/src/settings/schema.ts +3 -1
  139. package/src/vite-plugins/asset-imports.ts +3 -15
  140. package/src/vite-plugins/i18n.ts +146 -0
  141. package/src/vite-plugins/index.ts +3 -0
  142. package/src/vite-plugins/resources.ts +10 -72
  143. package/src/vite-plugins/self-contained.ts +150 -169
@@ -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 };
@@ -1,8 +1,8 @@
1
- import type { ParseResult, ParserOptions } from "vite";
2
- import { isRecord, sortStringsBy } from "@gtkx/utils";
1
+ import type { ESTree, ParseResult, ParserOptions } from "vite";
2
+ import { sortStringsBy } from "@gtkx/utils";
3
3
  import { type Dirent, readdirSync, readFileSync } from "node:fs";
4
4
  import { extname, join } from "node:path";
5
- import { parseSync } from "vite";
5
+ import { parseSync, Visitor } from "vite";
6
6
 
7
7
  type SourceImport = {
8
8
  importer: string;
@@ -71,6 +71,13 @@ const sourceFilesIn = (dir: string, found: string[]): void => {
71
71
  }
72
72
  };
73
73
 
74
+ const discoverSourceFiles = (dir: string): string[] => {
75
+ const files: string[] = [];
76
+ sourceFilesIn(dir, files);
77
+
78
+ return sortStringsBy(files, (path) => path);
79
+ };
80
+
74
81
  const staticImportSources = (module: ParsedModule): string[] => [
75
82
  ...module.staticImports.map((statement) => statement.moduleRequest.value),
76
83
  ...module.staticExports.flatMap((statement) =>
@@ -92,65 +99,30 @@ const staticRuntimeImportSources = (module: ParsedModule): string[] => [
92
99
  ),
93
100
  ];
94
101
 
95
- const templateImportSource = (node: Record<string, unknown>): string | null => {
96
- if (!Array.isArray(node.expressions) || node.expressions.length > 0 || !Array.isArray(node.quasis)) {
97
- return null;
98
- }
102
+ const templateImportSource = (node: ESTree.TemplateLiteral): string | null => {
103
+ const [quasi] = node.quasis;
99
104
 
100
- const quasis: unknown[] = node.quasis;
101
- const first: unknown = quasis[0];
102
-
103
- if (!isRecord(first) || !isRecord(first.value)) {
104
- return null;
105
- }
106
-
107
- return typeof first.value.cooked === "string" ? first.value.cooked : null;
105
+ return quasi === undefined || node.expressions.length > 0 ? null : quasi.value.cooked;
108
106
  };
109
107
 
110
- const literalImportSource = (node: unknown): string | null => {
111
- if (!isRecord(node)) {
112
- return null;
113
- }
114
-
115
- if (node.type === "Literal") {
116
- return typeof node.value === "string" ? node.value : null;
108
+ const literalImportSource = (node: ESTree.Expression): string | null => {
109
+ if (node.type === "TemplateLiteral") {
110
+ return templateImportSource(node);
117
111
  }
118
112
 
119
- return node.type === "TemplateLiteral" ? templateImportSource(node) : null;
120
- };
121
-
122
- const collectDynamicImportArray = (nodes: unknown[], found: string[]): void => {
123
- for (const child of nodes) {
124
- collectDynamicImportSources(child, found);
125
- }
113
+ return node.type === "Literal" && typeof node.value === "string" ? node.value : null;
126
114
  };
127
115
 
128
- const collectDynamicImportRecord = (node: Record<string, unknown>, found: string[]): void => {
129
- if (node.type === "ImportExpression") {
130
- const source = literalImportSource(node.source);
116
+ const collectDynamicImportSources = (program: ESTree.Program, found: string[]): void => {
117
+ new Visitor({
118
+ ImportExpression: (node) => {
119
+ const source = literalImportSource(node.source);
131
120
 
132
- if (source !== null) {
133
- found.push(source);
134
- }
135
-
136
- return;
137
- }
138
-
139
- collectDynamicImportArray(Object.values(node), found);
140
- };
141
-
142
- const collectDynamicImportSources = (node: unknown, found: string[]): void => {
143
- if (Array.isArray(node)) {
144
- collectDynamicImportArray(node, found);
145
-
146
- return;
147
- }
148
-
149
- if (!isRecord(node)) {
150
- return;
151
- }
152
-
153
- collectDynamicImportRecord(node, found);
121
+ if (source !== null) {
122
+ found.push(source);
123
+ }
124
+ },
125
+ }).visit(program);
154
126
  };
155
127
 
156
128
  const parseImportsInWith = (
@@ -190,11 +162,10 @@ const importsIn = (path: string): SourceImport[] => parseImportsIn(path) ?? [];
190
162
  const importKey = (entry: SourceImport): string => `${entry.importer}\0${entry.source}`;
191
163
 
192
164
  const discoverSourceImports = (dir: string): SourceImport[] => {
193
- const files: string[] = [];
194
- sourceFilesIn(dir, files);
165
+ const files = discoverSourceFiles(dir);
195
166
  const imports = files.flatMap((path) => importsIn(path));
196
167
 
197
168
  return sortStringsBy(imports, importKey);
198
169
  };
199
170
 
200
- export { discoverSourceImports, parseRuntimeImportsIn, type SourceImport };
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
  }
@@ -13,6 +13,10 @@ type EnvAssetDeclarations = {
13
13
  resources: string[];
14
14
  };
15
15
 
16
+ type EnvModuleOptions = {
17
+ references?: string[];
18
+ };
19
+
16
20
  const GTKX_ENV_MODULE_HEADER = `/**
17
21
  * Project import declarations generated by GTKX. Regenerated by
18
22
  * \`gtkx codegen\`, \`gtkx dev\`, and \`gtkx build\`; do not edit.
@@ -226,6 +230,7 @@ const renderBlockedAssetModule = (specifier: string): string => [
226
230
  const renderEnvModule = (
227
231
  files: ParsedSchemaFile[],
228
232
  assetDeclarations?: EnvAssetDeclarations,
233
+ options: EnvModuleOptions = {},
229
234
  ): string => {
230
235
  const assets = assetDeclarations ?? { blocked: [], icons: [], legacy: [], resources: [] };
231
236
  const usedNames: Set<string> = new Set();
@@ -238,8 +243,17 @@ const renderEnvModule = (
238
243
  const icons = assets.icons.map((specifier) => renderIconModule(specifier));
239
244
  const legacy = assets.legacy.map((specifier) => renderLegacyAssetModule(specifier));
240
245
  const resources = assets.resources.map((specifier) => renderResourceModule(specifier));
241
-
242
- return `${[GTKX_ENV_MODULE_HEADER, ...blocked, ...icons, ...legacy, ...resources, ...schemas].join("\n\n")}\n`;
246
+ const references = (options.references ?? []).map((path) => `/// <reference path=${JSON.stringify(path)} />`);
247
+
248
+ return `${[
249
+ GTKX_ENV_MODULE_HEADER,
250
+ ...references,
251
+ ...blocked,
252
+ ...icons,
253
+ ...legacy,
254
+ ...resources,
255
+ ...schemas,
256
+ ].join("\n\n")}\n`;
243
257
  };
244
258
 
245
259
  export { renderRuntimeModule, renderEnvModule };
@@ -13,6 +13,7 @@ import {
13
13
  } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
16
+ import { I18N_TYPES_FILENAME, i18nTypesPath } from "../i18n/types.js";
16
17
  import { DATA_IMPORT_PREFIX } from "../internal/data-dir.js";
17
18
  import { discoverSourceImports, type SourceImport } from "../internal/source-imports.js";
18
19
  import { removeTempDir } from "../internal/staging-dir.js";
@@ -323,7 +324,8 @@ const emitSchemaEnv = (
323
324
  assertUniqueSchemaBasenames(importedFiles);
324
325
  const imported = parseProjectSchemas(importedFiles, getRelativeModuleSpecifier);
325
326
  const assets = findAssetModuleSpecifiers(imports, isV2ResourceImports);
326
- const content = renderEnvModule([...legacy, ...imported], assets);
327
+ const references = existsSync(i18nTypesPath(rootDir)) ? [`./${I18N_TYPES_FILENAME}`] : [];
328
+ const content = renderEnvModule([...legacy, ...imported], assets, { references });
327
329
  const path = schemaEnvPath(rootDir);
328
330
  const isWritten = didWriteChanges(path, content);
329
331
 
@@ -1,8 +1,8 @@
1
1
  import type { ConfigLoader } from "@gtkx/config";
2
- import type { ParseResult, ParserOptions, Plugin, UserConfig } from "vite";
2
+ import type { ParseResult, Plugin, UserConfig } from "vite";
3
3
  import { createConfigLoader } from "@gtkx/config/internal";
4
- import { extname } from "node:path";
5
4
  import { parseSync } from "vite";
5
+ import { sourceLanguage } from "../internal/source-imports.js";
6
6
  import { ASSET_MENTION_RE } from "./asset-extensions.js";
7
7
  import {
8
8
  DATA_PREFIX,
@@ -15,7 +15,6 @@ import {
15
15
  import { RESOURCE_PATH_EXPORT } from "./resource-shared.js";
16
16
  import { stripQuery } from "./strip-query.js";
17
17
 
18
- type SourceLanguage = NonNullable<ParserOptions["lang"]>;
19
18
  type ParsedModule = ParseResult["module"];
20
19
  type ImportStatement = ParsedModule["staticImports"][number];
21
20
  type ExportStatement = ParsedModule["staticExports"][number];
@@ -24,17 +23,6 @@ type BindingEntry = { importName: { name: string | null }; isType: boolean };
24
23
  type NamedBinding = { name: string; source: string };
25
24
  type PluginState = { isV2: boolean };
26
25
 
27
- const SOURCE_LANGUAGES: Map<string, SourceLanguage> = new Map([
28
- [".cjs", "jsx"],
29
- [".cts", "ts"],
30
- [".js", "jsx"],
31
- [".jsx", "jsx"],
32
- [".mjs", "jsx"],
33
- [".mts", "ts"],
34
- [".ts", "ts"],
35
- [".tsx", "tsx"],
36
- ]);
37
-
38
26
  const NODE_MODULES = /(?:^|\/)node_modules\//;
39
27
  const VIRTUAL_PREFIX = "\0";
40
28
  const DEFAULT_BINDING = "default";
@@ -135,7 +123,7 @@ const hasCheckedAssetMention = (code: string): boolean => ASSET_MENTION_RE.test(
135
123
 
136
124
  const checkAssetImports = (code: string, id: string, isV2: boolean): void => {
137
125
  const path = stripQuery(id);
138
- const lang = SOURCE_LANGUAGES.get(extname(path).toLowerCase());
126
+ const lang = sourceLanguage(path);
139
127
 
140
128
  if (lang === undefined || !isCheckedSource(path) || !hasCheckedAssetMention(code)) {
141
129
  return;
@@ -0,0 +1,146 @@
1
+ import type { ConfigLoader } from "@gtkx/config";
2
+ import type { Plugin, ResolvedConfig } from "vite";
3
+ import { createConfigLoader } from "@gtkx/config/internal";
4
+ import { existsSync, mkdirSync } from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import { dirname, join, posix, relative, resolve } from "node:path";
7
+ import {
8
+ type CatalogProject,
9
+ compileCatalogs,
10
+ LOCALE_DIRNAME,
11
+ resolveCatalogProject,
12
+ synchronizeCatalogs,
13
+ } from "../i18n/catalogs.js";
14
+ import { extractSourceCatalog } from "../i18n/source-messages.js";
15
+ import { discoverSourceFiles } from "../internal/source-imports.js";
16
+ import { stripQuery } from "./strip-query.js";
17
+
18
+ type I18nState = {
19
+ entryPath: string;
20
+ i18nRoot: string;
21
+ outDir: string;
22
+ project: CatalogProject | null;
23
+ };
24
+
25
+ const BOOTSTRAP_SPECIFIER = "@gtkx/i18n/bootstrap";
26
+ const LOCALE_URL_PLACEHOLDER = "__GTKX_BUNDLE_LOCALE_DIR__";
27
+ const LOCALE_MODULE_PATHS = new Set(["dist/locale.js", "src/locale.ts"]);
28
+ const LOCALE_URL_PATTERN = /new URL\((["'])locale\1,\s*import\.meta\.url\)/;
29
+
30
+ const isI18nLocaleModule = (state: I18nState, id: string): boolean => {
31
+ if (state.i18nRoot.length === 0) {
32
+ return false;
33
+ }
34
+
35
+ const path = relative(state.i18nRoot, stripQuery(id)).replaceAll("\\", "/");
36
+
37
+ return LOCALE_MODULE_PATHS.has(path);
38
+ };
39
+
40
+ const markLocaleUrl = (code: string): string =>
41
+ code.replace(
42
+ LOCALE_URL_PATTERN,
43
+ () => `new URL(${JSON.stringify(LOCALE_URL_PLACEHOLDER)}, import.meta.url)`,
44
+ );
45
+
46
+ const localeUrlForChunk = (fileName: string): string => {
47
+ const path = posix.relative(posix.dirname(fileName), LOCALE_DIRNAME);
48
+
49
+ return path.length === 0 ? "." : path;
50
+ };
51
+
52
+ const rebaseLocaleUrl = (state: I18nState, code: string, fileName: string, moduleIds: string[]): string | null => {
53
+ if (moduleIds.every((id) => !isI18nLocaleModule(state, id))) {
54
+ return null;
55
+ }
56
+
57
+ const placeholder = JSON.stringify(LOCALE_URL_PLACEHOLDER);
58
+
59
+ if (!code.includes(placeholder)) {
60
+ throw new Error(`Unable to locate the @gtkx/i18n locale fallback in ${fileName}`);
61
+ }
62
+
63
+ return code.replace(placeholder, () => JSON.stringify(localeUrlForChunk(fileName)));
64
+ };
65
+
66
+ const applyResolvedConfig = async (
67
+ state: I18nState,
68
+ config: ResolvedConfig,
69
+ loadConfig: ConfigLoader,
70
+ ): Promise<void> => {
71
+ state.entryPath = resolve(config.root, state.entryPath);
72
+ state.outDir = resolve(config.root, config.build.outDir);
73
+ const application = await loadConfig.resolve(config.root);
74
+ state.project = resolveCatalogProject(config.root, application.applicationId);
75
+
76
+ if (state.project !== null) {
77
+ const projectRequire = createRequire(resolve(config.root, "package.json"));
78
+ state.i18nRoot = dirname(projectRequire.resolve("@gtkx/i18n/package.json"));
79
+ }
80
+ };
81
+
82
+ const buildCatalogs = (state: I18nState): void => {
83
+ const project = state.project;
84
+
85
+ if (project === null) {
86
+ return;
87
+ }
88
+
89
+ mkdirSync(state.outDir, { recursive: true });
90
+ compileCatalogs(project, resolve(state.outDir, LOCALE_DIRNAME));
91
+ };
92
+
93
+ const extractProjectMessages = async (
94
+ state: I18nState,
95
+ shouldPreserveMetadataMessages: boolean,
96
+ ): Promise<void> => {
97
+ const project = state.project;
98
+
99
+ if (project === null) {
100
+ return;
101
+ }
102
+
103
+ const srcDir = join(project.root, "src");
104
+ const sourceFiles = discoverSourceFiles(existsSync(srcDir) ? srcDir : project.root);
105
+ await extractSourceCatalog(project, sourceFiles, shouldPreserveMetadataMessages);
106
+ synchronizeCatalogs(project);
107
+ };
108
+
109
+ const gtkxI18n = (
110
+ entryPath: string,
111
+ loadConfig: ConfigLoader = createConfigLoader(),
112
+ shouldPreserveMetadataMessages = true,
113
+ ): Plugin => {
114
+ const state: I18nState = { entryPath, i18nRoot: "", outDir: "", project: null };
115
+
116
+ return {
117
+ name: "gtkx:i18n",
118
+ enforce: "pre",
119
+
120
+ configResolved: (config) => applyResolvedConfig(state, config, loadConfig),
121
+
122
+ buildStart: () => extractProjectMessages(state, shouldPreserveMetadataMessages),
123
+
124
+ transform(code, id) {
125
+ if (isI18nLocaleModule(state, id)) {
126
+ return markLocaleUrl(code);
127
+ }
128
+
129
+ if (state.project === null || resolve(stripQuery(id)) !== state.entryPath) {
130
+ return;
131
+ }
132
+
133
+ return `import ${JSON.stringify(BOOTSTRAP_SPECIFIER)};\n${code}`;
134
+ },
135
+
136
+ renderChunk(code, chunk) {
137
+ return rebaseLocaleUrl(state, code, chunk.fileName, chunk.moduleIds);
138
+ },
139
+
140
+ generateBundle() {
141
+ buildCatalogs(state);
142
+ },
143
+ };
144
+ };
145
+
146
+ export { gtkxI18n };