@gtkx/cli 2.0.0-beta.4 → 2.0.0-beta.6
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/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +28 -8
- package/dist/builder.js.map +1 -1
- package/dist/commands/dev.js +2 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
- package/dist/deploy/freedesktop/validate.js +50 -13
- package/dist/deploy/freedesktop/validate.js.map +1 -1
- package/dist/dev/catalog-writes.d.ts +8 -0
- package/dist/dev/catalog-writes.d.ts.map +1 -0
- package/dist/dev/catalog-writes.js +26 -0
- package/dist/dev/catalog-writes.js.map +1 -0
- package/dist/dev/runner-deps.d.ts.map +1 -1
- package/dist/dev/runner-deps.js +10 -6
- package/dist/dev/runner-deps.js.map +1 -1
- package/dist/dev/runner.d.ts +1 -0
- package/dist/dev/runner.d.ts.map +1 -1
- package/dist/dev/runner.js +8 -2
- package/dist/dev/runner.js.map +1 -1
- package/dist/dev/supervisor.d.ts.map +1 -1
- package/dist/dev/supervisor.js +26 -20
- package/dist/dev/supervisor.js.map +1 -1
- package/dist/i18n/catalogs.d.ts +6 -2
- package/dist/i18n/catalogs.d.ts.map +1 -1
- package/dist/i18n/catalogs.js +9 -7
- package/dist/i18n/catalogs.js.map +1 -1
- package/dist/internal/agent-rules.d.ts.map +1 -1
- package/dist/internal/agent-rules.js +1 -0
- package/dist/internal/agent-rules.js.map +1 -1
- package/dist/internal/banner.d.ts +4 -2
- package/dist/internal/banner.d.ts.map +1 -1
- package/dist/internal/banner.js +16 -5
- package/dist/internal/banner.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts +11 -2
- package/dist/vite-plugins/i18n.d.ts.map +1 -1
- package/dist/vite-plugins/i18n.js +3 -2
- package/dist/vite-plugins/i18n.js.map +1 -1
- package/dist/vite-plugins/icons.d.ts.map +1 -1
- package/dist/vite-plugins/icons.js +6 -6
- package/dist/vite-plugins/icons.js.map +1 -1
- package/dist/vite-plugins/index.d.ts +2 -0
- package/dist/vite-plugins/index.d.ts.map +1 -1
- package/dist/vite-plugins/index.js +8 -2
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/settings.d.ts.map +1 -1
- package/dist/vite-plugins/settings.js +6 -6
- package/dist/vite-plugins/settings.js.map +1 -1
- package/package.json +10 -10
- package/src/builder.ts +28 -8
- package/src/commands/dev.ts +2 -2
- package/src/deploy/freedesktop/validate.ts +70 -14
- package/src/dev/catalog-writes.ts +35 -0
- package/src/dev/runner-deps.ts +14 -6
- package/src/dev/runner.ts +11 -2
- package/src/dev/supervisor.ts +38 -21
- package/src/i18n/catalogs.ts +20 -8
- package/src/internal/agent-rules.ts +1 -0
- package/src/internal/banner.ts +23 -6
- package/src/vite-plugins/i18n.ts +20 -6
- package/src/vite-plugins/icons.ts +7 -7
- package/src/vite-plugins/index.ts +15 -6
- package/src/vite-plugins/settings.ts +7 -7
|
@@ -6,6 +6,12 @@ type ToolResult = {
|
|
|
6
6
|
status: number;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
type Diagnostic = {
|
|
10
|
+
severity: string;
|
|
11
|
+
rule: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
9
15
|
type MetainfoResult = {
|
|
10
16
|
subject: string;
|
|
11
17
|
output: string;
|
|
@@ -13,6 +19,7 @@ type MetainfoResult = {
|
|
|
13
19
|
errors: string[];
|
|
14
20
|
warnings: string[];
|
|
15
21
|
rules: string[];
|
|
22
|
+
notes: string[];
|
|
16
23
|
areWarningsFatal: boolean;
|
|
17
24
|
};
|
|
18
25
|
|
|
@@ -22,11 +29,19 @@ type ToolRequest = {
|
|
|
22
29
|
subject: string;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
|
-
const DIAGNOSTIC = /^(?<severity>[EIPW]): \S+ (?<rule>[\w-]+)
|
|
32
|
+
const DIAGNOSTIC = /^(?<severity>[EIPW]): \S+ (?<rule>[\w-]+)(?: (?<detail>.*))?$/;
|
|
33
|
+
const SUCCESS_SUMMARY = /^\W*Validation was successful/u;
|
|
26
34
|
const ERROR_SEVERITY = "E";
|
|
27
35
|
const WARNING_SEVERITY = "W";
|
|
28
36
|
const INFO_SEVERITY = "I";
|
|
29
37
|
const FATAL_WARNING_RULES: Set<string> = new Set(["unknown-tag"]);
|
|
38
|
+
const DESKTOP_ERROR = /(?:^|:\s)error:/iu;
|
|
39
|
+
|
|
40
|
+
const FATAL_RULE_NOTES: Record<string, (detail: string) => string> = {
|
|
41
|
+
"unknown-tag": (element) =>
|
|
42
|
+
"GTKX treats unknown-tag as fatal for every target, whatever severity appstreamcli assigns it; " +
|
|
43
|
+
`the unsupported element is <${element}>`,
|
|
44
|
+
};
|
|
30
45
|
|
|
31
46
|
const REMEDY_FOR_RULE: Record<string, string> = {
|
|
32
47
|
"component-summary-missing": "set `deploy.summary`",
|
|
@@ -49,12 +64,31 @@ const runTool = ({ tool, args, subject }: ToolRequest): ToolResult => {
|
|
|
49
64
|
return { output: [result.stdout, result.stderr].join("\n").trim(), status: result.status ?? 0 };
|
|
50
65
|
};
|
|
51
66
|
|
|
52
|
-
const
|
|
67
|
+
const parseDiagnostics = (output: string): Diagnostic[] =>
|
|
68
|
+
output.split("\n").flatMap((line) => {
|
|
69
|
+
const groups = DIAGNOSTIC.exec(line)?.groups;
|
|
70
|
+
|
|
71
|
+
return groups === undefined
|
|
72
|
+
? []
|
|
73
|
+
: [{ severity: groups.severity ?? "", rule: groups.rule ?? "", detail: (groups.detail ?? "").trim() }];
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const rulesIn = (diagnostics: Diagnostic[], severities: string[]): string[] =>
|
|
77
|
+
diagnostics.filter((diagnostic) => severities.includes(diagnostic.severity)).map((diagnostic) => diagnostic.rule);
|
|
78
|
+
|
|
79
|
+
const fatalNotes = (diagnostics: Diagnostic[]): string[] =>
|
|
80
|
+
diagnostics
|
|
81
|
+
.filter((diagnostic) => diagnostic.severity !== ERROR_SEVERITY && FATAL_WARNING_RULES.has(diagnostic.rule))
|
|
82
|
+
.map((diagnostic) =>
|
|
83
|
+
FATAL_RULE_NOTES[diagnostic.rule]?.(diagnostic.detail) ??
|
|
84
|
+
`GTKX treats ${diagnostic.rule} as fatal for every target`);
|
|
85
|
+
|
|
86
|
+
const withoutSuccessSummary = (output: string): string =>
|
|
53
87
|
output
|
|
54
88
|
.split("\n")
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.
|
|
89
|
+
.filter((line) => !SUCCESS_SUMMARY.test(line))
|
|
90
|
+
.join("\n")
|
|
91
|
+
.trimEnd();
|
|
58
92
|
|
|
59
93
|
const remedyLines = (rules: string[]): string[] => {
|
|
60
94
|
const remedies = sortStrings([...new Set(rules)])
|
|
@@ -64,8 +98,13 @@ const remedyLines = (rules: string[]): string[] => {
|
|
|
64
98
|
return remedies.length === 0 ? [] : ["", "Fix it in gtkx.config.ts:", ...remedies];
|
|
65
99
|
};
|
|
66
100
|
|
|
67
|
-
const invalid = (subject: string, output: string, rules: string[]): Error =>
|
|
68
|
-
new Error([
|
|
101
|
+
const invalid = (subject: string, output: string, rules: string[], notes: string[]): Error =>
|
|
102
|
+
new Error([
|
|
103
|
+
`${subject} is not valid:`,
|
|
104
|
+
output.length > 0 ? output : "no output",
|
|
105
|
+
...notes,
|
|
106
|
+
...remedyLines(rules),
|
|
107
|
+
].join("\n"));
|
|
69
108
|
|
|
70
109
|
const isFatalResult = ({ status, errors, warnings, rules, areWarningsFatal }: MetainfoResult): boolean => {
|
|
71
110
|
if (errors.length > 0) {
|
|
@@ -85,7 +124,7 @@ const isFatalResult = ({ status, errors, warnings, rules, areWarningsFatal }: Me
|
|
|
85
124
|
|
|
86
125
|
const assertNotFatal = (result: MetainfoResult): void => {
|
|
87
126
|
if (isFatalResult(result)) {
|
|
88
|
-
throw invalid(result.subject, result.output, result.rules);
|
|
127
|
+
throw invalid(result.subject, result.output, result.rules, result.notes);
|
|
89
128
|
}
|
|
90
129
|
};
|
|
91
130
|
|
|
@@ -108,8 +147,14 @@ const validateDesktopEntry = (path: string): void => {
|
|
|
108
147
|
subject: "The desktop entry",
|
|
109
148
|
});
|
|
110
149
|
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
const diagnostics = output.split("\n").filter((line) => line.length > 0);
|
|
151
|
+
|
|
152
|
+
if (status !== 0 || diagnostics.some((line) => DESKTOP_ERROR.test(line))) {
|
|
153
|
+
throw invalid("The desktop entry", output, [], []);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const diagnostic of diagnostics) {
|
|
157
|
+
warn(diagnostic);
|
|
113
158
|
}
|
|
114
159
|
};
|
|
115
160
|
|
|
@@ -122,11 +167,22 @@ const validateMetainfo = (path: string, areWarningsFatal: boolean): void => {
|
|
|
122
167
|
subject,
|
|
123
168
|
});
|
|
124
169
|
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
170
|
+
const diagnostics = parseDiagnostics(output);
|
|
171
|
+
const errors = rulesIn(diagnostics, [ERROR_SEVERITY]);
|
|
172
|
+
const warnings = rulesIn(diagnostics, [WARNING_SEVERITY]);
|
|
173
|
+
const infos = rulesIn(diagnostics, [INFO_SEVERITY]);
|
|
128
174
|
const rules = [...errors, ...warnings, ...infos];
|
|
129
|
-
|
|
175
|
+
const notes = fatalNotes(diagnostics);
|
|
176
|
+
assertNotFatal({
|
|
177
|
+
subject,
|
|
178
|
+
output: withoutSuccessSummary(output),
|
|
179
|
+
status,
|
|
180
|
+
errors,
|
|
181
|
+
warnings,
|
|
182
|
+
rules,
|
|
183
|
+
notes,
|
|
184
|
+
areWarningsFatal,
|
|
185
|
+
});
|
|
130
186
|
reportDiagnostics(subject, [...warnings, ...infos]);
|
|
131
187
|
};
|
|
132
188
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { WrittenCatalog } from "../i18n/catalogs.js";
|
|
4
|
+
|
|
5
|
+
type CatalogWrites = {
|
|
6
|
+
record: (catalogs: WrittenCatalog[]) => void;
|
|
7
|
+
hasWritten: (path: string) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const hasContent = (path: string, content: Buffer): boolean => {
|
|
11
|
+
try {
|
|
12
|
+
return readFileSync(path).equals(content);
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const createCatalogWrites = (): CatalogWrites => {
|
|
19
|
+
const contents: Map<string, Buffer> = new Map();
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
record: (catalogs) => {
|
|
23
|
+
for (const catalog of catalogs) {
|
|
24
|
+
contents.set(resolve(catalog.path), catalog.content);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
hasWritten: (path) => {
|
|
28
|
+
const content = contents.get(resolve(path));
|
|
29
|
+
|
|
30
|
+
return content !== undefined && hasContent(path, content);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export { type CatalogWrites, createCatalogWrites };
|
package/src/dev/runner-deps.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { isRefreshBoundary, performRefresh, staleExportName } from "../refresh-r
|
|
|
17
17
|
import { gtkxFastRefresh } from "../vite-plugins/fast-refresh/swc-refresh.js";
|
|
18
18
|
import { gtkxVitePlugins } from "../vite-plugins/index.js";
|
|
19
19
|
import { gtkxReactDomPrebundle } from "../vite-plugins/react-dom-prebundle.js";
|
|
20
|
+
import { type CatalogWrites, createCatalogWrites } from "./catalog-writes.js";
|
|
20
21
|
|
|
21
22
|
const DEV_MODE = "development";
|
|
22
23
|
const APPLICATION_POLL_INTERVAL_MS = 50;
|
|
@@ -43,7 +44,14 @@ const waitForApplicationId = async (timeoutMs: number, shouldKeepWaiting: () =>
|
|
|
43
44
|
|
|
44
45
|
const readFileRevision = (path: string): Promise<string> => readFile(path, "utf8");
|
|
45
46
|
|
|
46
|
-
const
|
|
47
|
+
const devPlugins = (configFile: string, catalogWrites: CatalogWrites): DevRunnerDeps["plugins"] =>
|
|
48
|
+
(entryPath) => [
|
|
49
|
+
...gtkxVitePlugins({ mode: DEV_MODE, entryPath, configFile, onCatalogsWritten: catalogWrites.record }),
|
|
50
|
+
...gtkxFastRefresh(),
|
|
51
|
+
gtkxReactDomPrebundle(),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const createDevRunnerDeps = (configFile: string, catalogWrites: CatalogWrites): DevRunnerDeps => ({
|
|
47
55
|
createServer,
|
|
48
56
|
waitForApplicationId,
|
|
49
57
|
getConfiguredApplicationId: async (root: string) => {
|
|
@@ -86,13 +94,13 @@ const defaultDevRunnerDeps = (configFile: string): DevRunnerDeps => ({
|
|
|
86
94
|
isRefreshBoundary,
|
|
87
95
|
staleExportName,
|
|
88
96
|
readFileRevision,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
...gtkxFastRefresh(),
|
|
92
|
-
gtkxReactDomPrebundle(),
|
|
93
|
-
],
|
|
97
|
+
hasWrittenCatalog: catalogWrites.hasWritten,
|
|
98
|
+
plugins: devPlugins(configFile, catalogWrites),
|
|
94
99
|
log: info,
|
|
95
100
|
exit: (code: number): never => process.exit(code),
|
|
96
101
|
});
|
|
97
102
|
|
|
103
|
+
const defaultDevRunnerDeps = (configFile: string): DevRunnerDeps =>
|
|
104
|
+
createDevRunnerDeps(configFile, createCatalogWrites());
|
|
105
|
+
|
|
98
106
|
export { defaultDevRunnerDeps };
|
package/src/dev/runner.ts
CHANGED
|
@@ -33,6 +33,7 @@ type DevRunnerDeps = {
|
|
|
33
33
|
isRefreshBoundary(module: Record<string, unknown>): boolean;
|
|
34
34
|
staleExportName(previous: Record<string, unknown>, current: Record<string, unknown>): string | null;
|
|
35
35
|
readFileRevision(path: string): Promise<string>;
|
|
36
|
+
hasWrittenCatalog(path: string): boolean;
|
|
36
37
|
plugins(entryPath: string): Plugin[];
|
|
37
38
|
log(message: string): void;
|
|
38
39
|
exit(code: number): never;
|
|
@@ -370,14 +371,22 @@ const restartForServerConfig = async (session: DevSession, changedPath: string):
|
|
|
370
371
|
await requestRestart(session);
|
|
371
372
|
};
|
|
372
373
|
|
|
374
|
+
const restartForCatalog = async (session: DevSession, changedPath: string): Promise<void> => {
|
|
375
|
+
if (session.deps.hasWrittenCatalog(changedPath)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
session.deps.log(`Translation catalog changed: ${changedPath}`);
|
|
380
|
+
await requestRestart(session);
|
|
381
|
+
};
|
|
382
|
+
|
|
373
383
|
const applyChange = async (session: DevSession, change: WatchedChange): Promise<void> => {
|
|
374
384
|
if (session.controller.isShuttingDown()) {
|
|
375
385
|
return;
|
|
376
386
|
}
|
|
377
387
|
|
|
378
388
|
if (isCatalogSource(session.server.config.root, change.path)) {
|
|
379
|
-
session
|
|
380
|
-
await requestRestart(session);
|
|
389
|
+
await restartForCatalog(session, change.path);
|
|
381
390
|
|
|
382
391
|
return;
|
|
383
392
|
}
|
package/src/dev/supervisor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { error, exitCodeForSignal, info, installGracefulShutdown } from "@gtkx/utils";
|
|
2
2
|
import { fork as nodeFork } from "node:child_process";
|
|
3
3
|
import { type FSWatcher, statSync, watch as watchFs } from "node:fs";
|
|
4
|
-
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { DEV_CONFIG_ENV, DEV_ENTRY_ENV } from "./entry-env.js";
|
|
7
7
|
|
|
@@ -30,6 +30,7 @@ type SupervisorState = {
|
|
|
30
30
|
args: string[];
|
|
31
31
|
watch: DevWatch | undefined;
|
|
32
32
|
watchers: FSWatcher[];
|
|
33
|
+
changedPaths: Set<string>;
|
|
33
34
|
restartTimer: DebounceTimer;
|
|
34
35
|
fork: ForkRunner;
|
|
35
36
|
child: SupervisedChild | null;
|
|
@@ -174,7 +175,7 @@ const reconcileConfigWatchers = (
|
|
|
174
175
|
paths: string[],
|
|
175
176
|
previous: ReadonlyMap<string, string | null>,
|
|
176
177
|
): void => {
|
|
177
|
-
const
|
|
178
|
+
const changed = changedWatchPaths(previous, paths);
|
|
178
179
|
|
|
179
180
|
closeWatchers(state);
|
|
180
181
|
|
|
@@ -188,11 +189,18 @@ const reconcileConfigWatchers = (
|
|
|
188
189
|
|
|
189
190
|
installConfigWatchers(state);
|
|
190
191
|
|
|
191
|
-
if (
|
|
192
|
-
scheduleRestart(state);
|
|
192
|
+
if (changed.length > 0 && !state.isRestartPending) {
|
|
193
|
+
scheduleRestart(state, changed);
|
|
193
194
|
}
|
|
194
195
|
};
|
|
195
196
|
|
|
197
|
+
const consumeChangedPaths = (state: SupervisorState): string => {
|
|
198
|
+
const changed = [...state.changedPaths].map((path) => relative(state.cwd, path));
|
|
199
|
+
state.changedPaths.clear();
|
|
200
|
+
|
|
201
|
+
return changed.length === 0 ? basename(state.configFile) : changed.join(", ");
|
|
202
|
+
};
|
|
203
|
+
|
|
196
204
|
const restart = async (state: SupervisorState): Promise<void> => {
|
|
197
205
|
const watch = state.watch;
|
|
198
206
|
|
|
@@ -207,7 +215,7 @@ const restart = async (state: SupervisorState): Promise<void> => {
|
|
|
207
215
|
}
|
|
208
216
|
|
|
209
217
|
state.isRestarting = true;
|
|
210
|
-
info(`${
|
|
218
|
+
info(`${consumeChangedPaths(state)} changed; regenerating bindings...`);
|
|
211
219
|
closeWatchers(state);
|
|
212
220
|
watch.paths = watch.resolvePaths();
|
|
213
221
|
const previous = snapshotWatchPaths(watch.paths);
|
|
@@ -246,9 +254,13 @@ const restart = async (state: SupervisorState): Promise<void> => {
|
|
|
246
254
|
forwardSignal(current, "SIGTERM");
|
|
247
255
|
};
|
|
248
256
|
|
|
249
|
-
const scheduleRestart = (state: SupervisorState): void => {
|
|
257
|
+
const scheduleRestart = (state: SupervisorState, changedPaths: string[] = []): void => {
|
|
250
258
|
const timer = state.restartTimer;
|
|
251
259
|
|
|
260
|
+
for (const path of changedPaths) {
|
|
261
|
+
state.changedPaths.add(path);
|
|
262
|
+
}
|
|
263
|
+
|
|
252
264
|
if (timer.handle !== null) {
|
|
253
265
|
clearTimeout(timer.handle);
|
|
254
266
|
}
|
|
@@ -259,12 +271,19 @@ const scheduleRestart = (state: SupervisorState): void => {
|
|
|
259
271
|
}, CONFIG_DEBOUNCE_MS);
|
|
260
272
|
};
|
|
261
273
|
|
|
262
|
-
const
|
|
274
|
+
const watchedChangePath = (
|
|
275
|
+
state: SupervisorState,
|
|
276
|
+
directory: string,
|
|
277
|
+
names: Set<string>,
|
|
278
|
+
filename: string | Buffer | null,
|
|
279
|
+
): string | undefined => {
|
|
263
280
|
if (filename === null || state.isShuttingDown) {
|
|
264
|
-
return
|
|
281
|
+
return undefined;
|
|
265
282
|
}
|
|
266
283
|
|
|
267
|
-
|
|
284
|
+
const name = basename(filename.toString());
|
|
285
|
+
|
|
286
|
+
return names.has(name) ? join(directory, name) : undefined;
|
|
268
287
|
};
|
|
269
288
|
|
|
270
289
|
const isDirectory = (path: string): boolean => {
|
|
@@ -315,20 +334,15 @@ const watchPathState = (path: string): string | null => {
|
|
|
315
334
|
const snapshotWatchPaths = (paths: string[]): Map<string, string | null> =>
|
|
316
335
|
new Map([...watchTargets(paths)].map((path) => [path, watchPathState(path)]));
|
|
317
336
|
|
|
318
|
-
const
|
|
337
|
+
const changedWatchPaths = (
|
|
319
338
|
previous: ReadonlyMap<string, string | null>,
|
|
320
339
|
paths: string[],
|
|
321
|
-
):
|
|
322
|
-
|
|
340
|
+
): string[] =>
|
|
341
|
+
[...watchTargets(paths)].filter((path) => {
|
|
323
342
|
const current = watchPathState(path);
|
|
324
343
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return false;
|
|
331
|
-
};
|
|
344
|
+
return previous.has(path) ? previous.get(path) !== current : current !== null;
|
|
345
|
+
});
|
|
332
346
|
|
|
333
347
|
const groupWatchNamesByDirectory = (paths: string[]): Map<string, Set<string>> => {
|
|
334
348
|
const namesByDirectory: Map<string, Set<string>> = new Map();
|
|
@@ -350,8 +364,10 @@ const watchConfigDirectory = (
|
|
|
350
364
|
): void => {
|
|
351
365
|
try {
|
|
352
366
|
const watcher = watchFs(directory, (_event, filename) => {
|
|
353
|
-
|
|
354
|
-
|
|
367
|
+
const changed = watchedChangePath(state, directory, names, filename);
|
|
368
|
+
|
|
369
|
+
if (changed !== undefined) {
|
|
370
|
+
scheduleRestart(state, [changed]);
|
|
355
371
|
}
|
|
356
372
|
});
|
|
357
373
|
|
|
@@ -427,6 +443,7 @@ const runDevSupervisor = async (options: DevSupervisorOptions): Promise<never> =
|
|
|
427
443
|
args,
|
|
428
444
|
watch,
|
|
429
445
|
watchers: [],
|
|
446
|
+
changedPaths: new Set(),
|
|
430
447
|
restartTimer: { handle: null },
|
|
431
448
|
fork,
|
|
432
449
|
child: null,
|
package/src/i18n/catalogs.ts
CHANGED
|
@@ -28,11 +28,17 @@ type CatalogProject = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
type PreparedCatalog = {
|
|
31
|
+
content: Buffer;
|
|
31
32
|
isChanged: boolean;
|
|
32
33
|
output: string;
|
|
33
34
|
target: string;
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
type WrittenCatalog = {
|
|
38
|
+
content: Buffer;
|
|
39
|
+
path: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
36
42
|
const PO_DIRNAME = "po";
|
|
37
43
|
const LOCALE_DIRNAME = "locale";
|
|
38
44
|
const LINGUAS_FILENAME = "LINGUAS";
|
|
@@ -165,16 +171,25 @@ const prepareCatalog = (
|
|
|
165
171
|
initializeCatalog(catalog, template, output);
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
const content = readFileSync(output);
|
|
175
|
+
|
|
168
176
|
return {
|
|
169
|
-
|
|
177
|
+
content,
|
|
178
|
+
isChanged: !isExisting || !content.equals(readFileSync(catalog.path)),
|
|
170
179
|
output,
|
|
171
180
|
target: catalog.path,
|
|
172
181
|
};
|
|
173
182
|
};
|
|
174
183
|
|
|
175
|
-
const
|
|
184
|
+
const replaceCatalog = (catalog: PreparedCatalog): WrittenCatalog => {
|
|
185
|
+
renameSync(catalog.output, catalog.target);
|
|
186
|
+
|
|
187
|
+
return { content: catalog.content, path: catalog.target };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const synchronizeCatalogs = (project: CatalogProject): WrittenCatalog[] => {
|
|
176
191
|
if (project.catalogs.length === 0) {
|
|
177
|
-
return;
|
|
192
|
+
return [];
|
|
178
193
|
}
|
|
179
194
|
|
|
180
195
|
const template = join(project.poDir, `${project.domain}.pot`);
|
|
@@ -185,11 +200,7 @@ const synchronizeCatalogs = (project: CatalogProject): void => {
|
|
|
185
200
|
const merged = project.catalogs.map((catalog, index) =>
|
|
186
201
|
prepareCatalog(catalog, index, template, stagingDir));
|
|
187
202
|
|
|
188
|
-
|
|
189
|
-
if (catalog.isChanged) {
|
|
190
|
-
renameSync(catalog.output, catalog.target);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
203
|
+
return merged.filter((catalog) => catalog.isChanged).map((catalog) => replaceCatalog(catalog));
|
|
193
204
|
} finally {
|
|
194
205
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
195
206
|
}
|
|
@@ -215,4 +226,5 @@ export {
|
|
|
215
226
|
requiresCatalogInitialization,
|
|
216
227
|
resolveCatalogProject,
|
|
217
228
|
synchronizeCatalogs,
|
|
229
|
+
type WrittenCatalog,
|
|
218
230
|
};
|
|
@@ -37,6 +37,7 @@ const rulesBody = (heading: string): string[] => [
|
|
|
37
37
|
"| --- | --- |",
|
|
38
38
|
"| `gtkx dev` | Run the app with fast refresh |",
|
|
39
39
|
"| `gtkx codegen` | Regenerate bindings and this reference |",
|
|
40
|
+
"| `gtkx cleanup` | Remove stale headless runtime directories |",
|
|
40
41
|
"| `tsc --noEmit` | Typecheck |",
|
|
41
42
|
"| `vitest run` | Run the tests |",
|
|
42
43
|
"",
|
package/src/internal/banner.ts
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import type { Rollup } from "vite";
|
|
2
|
+
import { posix } from "node:path";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
type Banner = string | Rollup.AddonFunction;
|
|
5
|
+
|
|
6
|
+
const renderBanner = async (banner: Banner, chunk: Rollup.RenderedChunk): Promise<string> =>
|
|
7
|
+
typeof banner === "function" ? banner(chunk) : banner;
|
|
8
|
+
|
|
9
|
+
const prependBanner = (options: Rollup.OutputOptions, banner: Banner): Rollup.OutputOptions => {
|
|
4
10
|
const existing = options.banner;
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
return {
|
|
13
|
+
...options,
|
|
14
|
+
banner: async (chunk) => {
|
|
15
|
+
const prefix = await renderBanner(banner, chunk);
|
|
16
|
+
const suffix = existing === undefined ? "" : await renderBanner(existing, chunk);
|
|
17
|
+
|
|
18
|
+
return suffix.length === 0 ? prefix : `${prefix}\n${suffix}`;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const outputRootUrlExpression = (chunk: Rollup.RenderedChunk): string => {
|
|
24
|
+
const relativeRoot = posix.relative(posix.dirname(chunk.fileName), ".");
|
|
25
|
+
const specifier = relativeRoot.length === 0 ? "./" : `${relativeRoot}/`;
|
|
9
26
|
|
|
10
|
-
return
|
|
27
|
+
return `decodeURIComponent(new URL(${JSON.stringify(specifier)}, import.meta.url).pathname)`;
|
|
11
28
|
};
|
|
12
29
|
|
|
13
|
-
export { prependBanner };
|
|
30
|
+
export { outputRootUrlExpression, prependBanner };
|
package/src/vite-plugins/i18n.ts
CHANGED
|
@@ -11,12 +11,23 @@ import {
|
|
|
11
11
|
LOCALE_DIRNAME,
|
|
12
12
|
resolveCatalogProject,
|
|
13
13
|
synchronizeCatalogs,
|
|
14
|
+
type WrittenCatalog,
|
|
14
15
|
} from "../i18n/catalogs.js";
|
|
15
16
|
import { extractSourceCatalog, SourceExtractionError } from "../i18n/source-messages.js";
|
|
16
17
|
import { emitI18nTypes } from "../i18n/types.js";
|
|
17
18
|
import { discoverSourceFiles, sourceLanguage } from "../internal/source-imports.js";
|
|
18
19
|
import { stripQuery } from "./strip-query.js";
|
|
19
20
|
|
|
21
|
+
type CatalogWriteListener = (catalogs: WrittenCatalog[]) => void;
|
|
22
|
+
|
|
23
|
+
type I18nPluginOptions = {
|
|
24
|
+
entryPath: string;
|
|
25
|
+
loadConfig?: ConfigLoader | undefined;
|
|
26
|
+
onCatalogsWritten?: CatalogWriteListener | undefined;
|
|
27
|
+
shouldPreserveMetadataMessages?: boolean | undefined;
|
|
28
|
+
shouldRecoverExtractionErrors?: boolean | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
20
31
|
type I18nState = {
|
|
21
32
|
entryPath: string;
|
|
22
33
|
i18nRoot: string;
|
|
@@ -24,6 +35,7 @@ type I18nState = {
|
|
|
24
35
|
project: CatalogProject | null;
|
|
25
36
|
extraction: Promise<void>;
|
|
26
37
|
hotUpdateTimestamp: number | null;
|
|
38
|
+
onCatalogsWritten: CatalogWriteListener;
|
|
27
39
|
};
|
|
28
40
|
|
|
29
41
|
const BOOTSTRAP_SPECIFIER = "@gtkx/i18n/bootstrap";
|
|
@@ -110,7 +122,7 @@ const extractProjectMessages = async (
|
|
|
110
122
|
await extractSourceCatalog(project, sourceFiles, shouldPreserveMetadataMessages);
|
|
111
123
|
|
|
112
124
|
if (shouldSynchronizeCatalogs) {
|
|
113
|
-
synchronizeCatalogs(project);
|
|
125
|
+
state.onCatalogsWritten(synchronizeCatalogs(project));
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
await emitI18nTypes(project.root);
|
|
@@ -184,12 +196,13 @@ const recoverInitialSourceExtraction = async (
|
|
|
184
196
|
}
|
|
185
197
|
};
|
|
186
198
|
|
|
187
|
-
const gtkxI18n = (
|
|
188
|
-
entryPath
|
|
189
|
-
loadConfig
|
|
199
|
+
const gtkxI18n = ({
|
|
200
|
+
entryPath,
|
|
201
|
+
loadConfig = createConfigLoader(),
|
|
202
|
+
onCatalogsWritten = (): void => undefined,
|
|
190
203
|
shouldPreserveMetadataMessages = true,
|
|
191
204
|
shouldRecoverExtractionErrors = false,
|
|
192
|
-
): Plugin => {
|
|
205
|
+
}: I18nPluginOptions): Plugin => {
|
|
193
206
|
const state: I18nState = {
|
|
194
207
|
entryPath,
|
|
195
208
|
extraction: Promise.resolve(),
|
|
@@ -197,6 +210,7 @@ const gtkxI18n = (
|
|
|
197
210
|
i18nRoot: "",
|
|
198
211
|
outDir: "",
|
|
199
212
|
project: null,
|
|
213
|
+
onCatalogsWritten,
|
|
200
214
|
};
|
|
201
215
|
|
|
202
216
|
return {
|
|
@@ -241,4 +255,4 @@ const gtkxI18n = (
|
|
|
241
255
|
};
|
|
242
256
|
};
|
|
243
257
|
|
|
244
|
-
export { gtkxI18n };
|
|
258
|
+
export { type CatalogWriteListener, gtkxI18n };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ConfigLoader } from "@gtkx/config";
|
|
2
|
-
import type { Plugin, UserConfig } from "vite";
|
|
2
|
+
import type { Plugin, Rollup, UserConfig } from "vite";
|
|
3
3
|
import { createConfigLoader } from "@gtkx/config/internal";
|
|
4
4
|
import { info } from "@gtkx/utils";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import type { AssetEmitter } from "./asset-emitter.js";
|
|
8
|
-
import { prependBanner } from "../internal/banner.js";
|
|
8
|
+
import { outputRootUrlExpression, prependBanner } from "../internal/banner.js";
|
|
9
9
|
import {
|
|
10
10
|
relativeIconPath,
|
|
11
11
|
resolveApplicationIcon,
|
|
@@ -20,10 +20,10 @@ type PluginState = {
|
|
|
20
20
|
|
|
21
21
|
const ICONS_DIR = "icons";
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
"process.env.XDG_DATA_DIRS = [",
|
|
25
|
-
|
|
26
|
-
" process.env.XDG_DATA_DIRS || \"/usr/local/share:/usr/share\",",
|
|
23
|
+
const xdgEnvBanner = (chunk: Rollup.RenderedChunk): string => [
|
|
24
|
+
"globalThis.process.env.XDG_DATA_DIRS = [",
|
|
25
|
+
` ${outputRootUrlExpression(chunk)},`,
|
|
26
|
+
" globalThis.process.env.XDG_DATA_DIRS || \"/usr/local/share:/usr/share\",",
|
|
27
27
|
"].join(\":\");",
|
|
28
28
|
].join("\n");
|
|
29
29
|
|
|
@@ -78,7 +78,7 @@ function gtkxIcons(loadConfig: ConfigLoader = createConfigLoader()): Plugin {
|
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
return prependBanner(options,
|
|
81
|
+
return prependBanner(options, xdgEnvBanner);
|
|
82
82
|
},
|
|
83
83
|
|
|
84
84
|
buildEnd() {
|
|
@@ -5,7 +5,7 @@ import type { BuildManifestCollector } from "../internal/build-manifest.js";
|
|
|
5
5
|
import { gtkxAssetImports } from "./asset-imports.js";
|
|
6
6
|
import { gtkxBuiltUrl } from "./built-url.js";
|
|
7
7
|
import { gtkxCss } from "./css.js";
|
|
8
|
-
import { gtkxI18n } from "./i18n.js";
|
|
8
|
+
import { type CatalogWriteListener, gtkxI18n } from "./i18n.js";
|
|
9
9
|
import { gtkxIcons } from "./icons.js";
|
|
10
10
|
import { gtkxReactCompiler } from "./react-compiler.js";
|
|
11
11
|
import { gtkxResources } from "./resources.js";
|
|
@@ -18,11 +18,19 @@ type GtkxVitePluginOptions = {
|
|
|
18
18
|
configFile?: string | undefined;
|
|
19
19
|
entryPath?: string | undefined;
|
|
20
20
|
mode?: string | undefined;
|
|
21
|
+
onCatalogsWritten?: CatalogWriteListener | undefined;
|
|
21
22
|
shouldPreserveI18nMetadata?: boolean | undefined;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const gtkxVitePlugins = (options: GtkxVitePluginOptions = {}): Plugin[] => {
|
|
25
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
buildManifest,
|
|
28
|
+
configFile,
|
|
29
|
+
entryPath,
|
|
30
|
+
mode,
|
|
31
|
+
onCatalogsWritten,
|
|
32
|
+
shouldPreserveI18nMetadata = true,
|
|
33
|
+
} = options;
|
|
26
34
|
const loadConfig = createConfigLoader({
|
|
27
35
|
...(mode !== undefined && { mode }),
|
|
28
36
|
...(configFile !== undefined && { configFile }),
|
|
@@ -32,12 +40,13 @@ const gtkxVitePlugins = (options: GtkxVitePluginOptions = {}): Plugin[] => {
|
|
|
32
40
|
createConfigPlugin({ name: "gtkx:config", loadConfig }),
|
|
33
41
|
...(entryPath === undefined
|
|
34
42
|
? []
|
|
35
|
-
: [gtkxI18n(
|
|
43
|
+
: [gtkxI18n({
|
|
36
44
|
entryPath,
|
|
37
45
|
loadConfig,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
onCatalogsWritten,
|
|
47
|
+
shouldPreserveMetadataMessages: shouldPreserveI18nMetadata,
|
|
48
|
+
shouldRecoverExtractionErrors: mode === "development",
|
|
49
|
+
})]),
|
|
41
50
|
gtkxStoreLinks(),
|
|
42
51
|
gtkxUndeclaredLibrary(loadConfig),
|
|
43
52
|
gtkxSettings(buildManifest),
|