@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.
- package/README.md +1 -1
- package/dist/builder.d.ts +1 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +2 -2
- package/dist/builder.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +2 -0
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +30 -6
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/commands/codegen.js +1 -1
- package/dist/commands/codegen.js.map +1 -1
- package/dist/deploy/freedesktop/localize.d.ts +6 -0
- package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
- package/dist/deploy/freedesktop/localize.js +107 -0
- package/dist/deploy/freedesktop/localize.js.map +1 -0
- package/dist/deploy/install-hints.d.ts.map +1 -1
- package/dist/deploy/install-hints.js +5 -0
- package/dist/deploy/install-hints.js.map +1 -1
- package/dist/deploy/payload/launcher.d.ts.map +1 -1
- package/dist/deploy/payload/launcher.js +2 -0
- package/dist/deploy/payload/launcher.js.map +1 -1
- package/dist/deploy/payload/stage.d.ts +1 -1
- package/dist/deploy/payload/stage.d.ts.map +1 -1
- package/dist/deploy/payload/stage.js +20 -11
- package/dist/deploy/payload/stage.js.map +1 -1
- package/dist/deploy/run-deploy.d.ts.map +1 -1
- package/dist/deploy/run-deploy.js +87 -15
- package/dist/deploy/run-deploy.js.map +1 -1
- package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
- package/dist/deploy/targets/flatpak-source.js +21 -10
- package/dist/deploy/targets/flatpak-source.js.map +1 -1
- package/dist/deploy/tools.d.ts +6 -1
- package/dist/deploy/tools.d.ts.map +1 -1
- package/dist/deploy/tools.js +26 -1
- package/dist/deploy/tools.js.map +1 -1
- package/dist/dev/locale-dir.d.ts +3 -0
- package/dist/dev/locale-dir.d.ts.map +1 -0
- package/dist/dev/locale-dir.js +17 -0
- package/dist/dev/locale-dir.js.map +1 -0
- package/dist/dev/runner-main.d.ts.map +1 -1
- package/dist/dev/runner-main.js +2 -0
- package/dist/dev/runner-main.js.map +1 -1
- package/dist/dev/runner.d.ts.map +1 -1
- package/dist/dev/runner.js +6 -0
- package/dist/dev/runner.js.map +1 -1
- package/dist/dev/vite-dev-server.d.ts +1 -0
- package/dist/dev/vite-dev-server.d.ts.map +1 -1
- package/dist/dev/vite-dev-server.js.map +1 -1
- package/dist/i18n/binding-extractor.d.ts +30 -0
- package/dist/i18n/binding-extractor.d.ts.map +1 -0
- package/dist/i18n/binding-extractor.js +1324 -0
- package/dist/i18n/binding-extractor.js.map +1 -0
- package/dist/i18n/catalog-template.d.ts +3 -0
- package/dist/i18n/catalog-template.d.ts.map +1 -0
- package/dist/i18n/catalog-template.js +18 -0
- package/dist/i18n/catalog-template.js.map +1 -0
- package/dist/i18n/catalogs.d.ts +21 -0
- package/dist/i18n/catalogs.d.ts.map +1 -0
- package/dist/i18n/catalogs.js +139 -0
- package/dist/i18n/catalogs.js.map +1 -0
- package/dist/i18n/extractor-plugin.d.ts +8 -0
- package/dist/i18n/extractor-plugin.d.ts.map +1 -0
- package/dist/i18n/extractor-plugin.js +247 -0
- package/dist/i18n/extractor-plugin.js.map +1 -0
- package/dist/i18n/metadata-templates.d.ts +11 -0
- package/dist/i18n/metadata-templates.d.ts.map +1 -0
- package/dist/i18n/metadata-templates.js +11 -0
- package/dist/i18n/metadata-templates.js.map +1 -0
- package/dist/i18n/source-messages.d.ts +23 -0
- package/dist/i18n/source-messages.d.ts.map +1 -0
- package/dist/i18n/source-messages.js +403 -0
- package/dist/i18n/source-messages.js.map +1 -0
- package/dist/i18n/types.d.ts +10 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +108 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/internal/source-imports.d.ts +5 -1
- package/dist/internal/source-imports.d.ts.map +1 -1
- package/dist/internal/source-imports.js +23 -44
- package/dist/internal/source-imports.js.map +1 -1
- package/dist/mcp/client.d.ts +0 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +11 -39
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/handlers.d.ts +2 -1
- package/dist/mcp/handlers.d.ts.map +1 -1
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/settings/render.d.ts +4 -1
- package/dist/settings/render.d.ts.map +1 -1
- package/dist/settings/render.js +11 -2
- package/dist/settings/render.js.map +1 -1
- package/dist/settings/schema.d.ts.map +1 -1
- package/dist/settings/schema.js +3 -1
- package/dist/settings/schema.js.map +1 -1
- package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
- package/dist/vite-plugins/asset-imports.js +2 -12
- package/dist/vite-plugins/asset-imports.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts +5 -0
- package/dist/vite-plugins/i18n.d.ts.map +1 -0
- package/dist/vite-plugins/i18n.js +88 -0
- package/dist/vite-plugins/i18n.js.map +1 -0
- package/dist/vite-plugins/index.d.ts +1 -1
- package/dist/vite-plugins/index.d.ts.map +1 -1
- package/dist/vite-plugins/index.js +3 -1
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/resources.d.ts.map +1 -1
- package/dist/vite-plugins/resources.js +10 -55
- package/dist/vite-plugins/resources.js.map +1 -1
- package/dist/vite-plugins/self-contained.d.ts.map +1 -1
- package/dist/vite-plugins/self-contained.js +128 -135
- package/dist/vite-plugins/self-contained.js.map +1 -1
- package/package.json +10 -9
- package/src/builder.ts +3 -2
- package/src/codegen/run-codegen.ts +53 -7
- package/src/commands/codegen.ts +1 -1
- package/src/deploy/freedesktop/localize.ts +144 -0
- package/src/deploy/install-hints.ts +5 -0
- package/src/deploy/payload/launcher.ts +2 -0
- package/src/deploy/payload/stage.ts +35 -10
- package/src/deploy/run-deploy.ts +128 -13
- package/src/deploy/targets/flatpak-source.ts +33 -10
- package/src/deploy/tools.ts +35 -0
- package/src/dev/locale-dir.ts +22 -0
- package/src/dev/runner-main.ts +2 -0
- package/src/dev/runner.ts +8 -0
- package/src/dev/vite-dev-server.ts +1 -0
- package/src/i18n/binding-extractor.ts +1969 -0
- package/src/i18n/catalog-template.ts +24 -0
- package/src/i18n/catalogs.ts +216 -0
- package/src/i18n/extractor-plugin.ts +413 -0
- package/src/i18n/metadata-templates.ts +29 -0
- package/src/i18n/source-messages.ts +608 -0
- package/src/i18n/types.ts +154 -0
- package/src/internal/source-imports.ts +28 -57
- package/src/mcp/client.ts +17 -44
- package/src/mcp/handlers.ts +7 -6
- package/src/settings/render.ts +16 -2
- package/src/settings/schema.ts +3 -1
- package/src/vite-plugins/asset-imports.ts +3 -15
- package/src/vite-plugins/i18n.ts +146 -0
- package/src/vite-plugins/index.ts +3 -0
- package/src/vite-plugins/resources.ts +10 -72
- 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 {
|
|
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:
|
|
96
|
-
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
102
|
+
const templateImportSource = (node: ESTree.TemplateLiteral): string | null => {
|
|
103
|
+
const [quasi] = node.quasis;
|
|
99
104
|
|
|
100
|
-
|
|
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:
|
|
111
|
-
if (
|
|
112
|
-
return
|
|
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 === "
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
116
|
+
const collectDynamicImportSources = (program: ESTree.Program, found: string[]): void => {
|
|
117
|
+
new Visitor({
|
|
118
|
+
ImportExpression: (node) => {
|
|
119
|
+
const source = literalImportSource(node.source);
|
|
131
120
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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 {
|
|
4
|
-
|
|
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.
|
|
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
|
|
162
|
-
const
|
|
163
|
-
const connection = this.connection;
|
|
155
|
+
private handleRequest(request: JSONRPCRequest): Promise<Result> {
|
|
156
|
+
const defaultApp = Gio.Application.getDefault();
|
|
164
157
|
|
|
165
|
-
if (!
|
|
166
|
-
|
|
158
|
+
if (!(defaultApp instanceof Gtk.Application)) {
|
|
159
|
+
throw new TypeError("Application not initialized");
|
|
167
160
|
}
|
|
168
161
|
|
|
169
|
-
|
|
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
|
-
|
|
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?.
|
|
198
|
+
void Promise.allSettled([this.connection?.send("app.unregister")]);
|
|
226
199
|
this.socket.destroy();
|
|
227
200
|
this.socket = null;
|
|
228
201
|
}
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
273
|
+
const dispatch = async (method: string, params: unknown, ctx: HandlerContext): Promise<Result> => {
|
|
273
274
|
if (!isServerInitiatedMethod(method)) {
|
|
274
275
|
throw methodNotFoundError(method);
|
|
275
276
|
}
|
package/src/settings/render.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/src/settings/schema.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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 =
|
|
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 };
|