@gtkx/cli 2.0.0-beta.4 → 2.0.0-beta.5

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 (41) hide show
  1. package/dist/commands/dev.js +2 -2
  2. package/dist/commands/dev.js.map +1 -1
  3. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  4. package/dist/deploy/freedesktop/validate.js +44 -12
  5. package/dist/deploy/freedesktop/validate.js.map +1 -1
  6. package/dist/dev/catalog-writes.d.ts +8 -0
  7. package/dist/dev/catalog-writes.d.ts.map +1 -0
  8. package/dist/dev/catalog-writes.js +26 -0
  9. package/dist/dev/catalog-writes.js.map +1 -0
  10. package/dist/dev/runner-deps.d.ts.map +1 -1
  11. package/dist/dev/runner-deps.js +10 -6
  12. package/dist/dev/runner-deps.js.map +1 -1
  13. package/dist/dev/runner.d.ts +1 -0
  14. package/dist/dev/runner.d.ts.map +1 -1
  15. package/dist/dev/runner.js +8 -2
  16. package/dist/dev/runner.js.map +1 -1
  17. package/dist/dev/supervisor.d.ts.map +1 -1
  18. package/dist/dev/supervisor.js +26 -20
  19. package/dist/dev/supervisor.js.map +1 -1
  20. package/dist/i18n/catalogs.d.ts +6 -2
  21. package/dist/i18n/catalogs.d.ts.map +1 -1
  22. package/dist/i18n/catalogs.js +9 -7
  23. package/dist/i18n/catalogs.js.map +1 -1
  24. package/dist/vite-plugins/i18n.d.ts +11 -2
  25. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  26. package/dist/vite-plugins/i18n.js +3 -2
  27. package/dist/vite-plugins/i18n.js.map +1 -1
  28. package/dist/vite-plugins/index.d.ts +2 -0
  29. package/dist/vite-plugins/index.d.ts.map +1 -1
  30. package/dist/vite-plugins/index.js +8 -2
  31. package/dist/vite-plugins/index.js.map +1 -1
  32. package/package.json +10 -10
  33. package/src/commands/dev.ts +2 -2
  34. package/src/deploy/freedesktop/validate.ts +62 -13
  35. package/src/dev/catalog-writes.ts +35 -0
  36. package/src/dev/runner-deps.ts +14 -6
  37. package/src/dev/runner.ts +11 -2
  38. package/src/dev/supervisor.ts +38 -21
  39. package/src/i18n/catalogs.ts +20 -8
  40. package/src/vite-plugins/i18n.ts +20 -6
  41. package/src/vite-plugins/index.ts +15 -6
@@ -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,12 +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"]);
30
38
 
39
+ const FATAL_RULE_NOTES: Record<string, (detail: string) => string> = {
40
+ "unknown-tag": (element) =>
41
+ "GTKX treats unknown-tag as fatal for every target, whatever severity appstreamcli assigns it; " +
42
+ `the unsupported element is <${element}>`,
43
+ };
44
+
31
45
  const REMEDY_FOR_RULE: Record<string, string> = {
32
46
  "component-summary-missing": "set `deploy.summary`",
33
47
  "description-first-para-too-short": "open `deploy.description` with a paragraph longer than 80 characters",
@@ -49,12 +63,31 @@ const runTool = ({ tool, args, subject }: ToolRequest): ToolResult => {
49
63
  return { output: [result.stdout, result.stderr].join("\n").trim(), status: result.status ?? 0 };
50
64
  };
51
65
 
52
- const rulesIn = (output: string, severities: string[]): string[] =>
66
+ const parseDiagnostics = (output: string): Diagnostic[] =>
67
+ output.split("\n").flatMap((line) => {
68
+ const groups = DIAGNOSTIC.exec(line)?.groups;
69
+
70
+ return groups === undefined
71
+ ? []
72
+ : [{ severity: groups.severity ?? "", rule: groups.rule ?? "", detail: (groups.detail ?? "").trim() }];
73
+ });
74
+
75
+ const rulesIn = (diagnostics: Diagnostic[], severities: string[]): string[] =>
76
+ diagnostics.filter((diagnostic) => severities.includes(diagnostic.severity)).map((diagnostic) => diagnostic.rule);
77
+
78
+ const fatalNotes = (diagnostics: Diagnostic[]): string[] =>
79
+ diagnostics
80
+ .filter((diagnostic) => diagnostic.severity !== ERROR_SEVERITY && FATAL_WARNING_RULES.has(diagnostic.rule))
81
+ .map((diagnostic) =>
82
+ FATAL_RULE_NOTES[diagnostic.rule]?.(diagnostic.detail) ??
83
+ `GTKX treats ${diagnostic.rule} as fatal for every target`);
84
+
85
+ const withoutSuccessSummary = (output: string): string =>
53
86
  output
54
87
  .split("\n")
55
- .map((line) => DIAGNOSTIC.exec(line)?.groups)
56
- .filter((groups) => groups !== undefined && severities.includes(groups.severity ?? ""))
57
- .map((groups) => groups?.rule ?? "");
88
+ .filter((line) => !SUCCESS_SUMMARY.test(line))
89
+ .join("\n")
90
+ .trimEnd();
58
91
 
59
92
  const remedyLines = (rules: string[]): string[] => {
60
93
  const remedies = sortStrings([...new Set(rules)])
@@ -64,8 +97,13 @@ const remedyLines = (rules: string[]): string[] => {
64
97
  return remedies.length === 0 ? [] : ["", "Fix it in gtkx.config.ts:", ...remedies];
65
98
  };
66
99
 
67
- const invalid = (subject: string, output: string, rules: string[]): Error =>
68
- new Error([`${subject} is not valid:`, output.length > 0 ? output : "no output", ...remedyLines(rules)].join("\n"));
100
+ const invalid = (subject: string, output: string, rules: string[], notes: string[]): Error =>
101
+ new Error([
102
+ `${subject} is not valid:`,
103
+ output.length > 0 ? output : "no output",
104
+ ...notes,
105
+ ...remedyLines(rules),
106
+ ].join("\n"));
69
107
 
70
108
  const isFatalResult = ({ status, errors, warnings, rules, areWarningsFatal }: MetainfoResult): boolean => {
71
109
  if (errors.length > 0) {
@@ -85,7 +123,7 @@ const isFatalResult = ({ status, errors, warnings, rules, areWarningsFatal }: Me
85
123
 
86
124
  const assertNotFatal = (result: MetainfoResult): void => {
87
125
  if (isFatalResult(result)) {
88
- throw invalid(result.subject, result.output, result.rules);
126
+ throw invalid(result.subject, result.output, result.rules, result.notes);
89
127
  }
90
128
  };
91
129
 
@@ -109,7 +147,7 @@ const validateDesktopEntry = (path: string): void => {
109
147
  });
110
148
 
111
149
  if (status !== 0 || output.length > 0) {
112
- throw invalid("The desktop entry", output, []);
150
+ throw invalid("The desktop entry", output, [], []);
113
151
  }
114
152
  };
115
153
 
@@ -122,11 +160,22 @@ const validateMetainfo = (path: string, areWarningsFatal: boolean): void => {
122
160
  subject,
123
161
  });
124
162
 
125
- const errors = rulesIn(output, [ERROR_SEVERITY]);
126
- const warnings = rulesIn(output, [WARNING_SEVERITY]);
127
- const infos = rulesIn(output, [INFO_SEVERITY]);
163
+ const diagnostics = parseDiagnostics(output);
164
+ const errors = rulesIn(diagnostics, [ERROR_SEVERITY]);
165
+ const warnings = rulesIn(diagnostics, [WARNING_SEVERITY]);
166
+ const infos = rulesIn(diagnostics, [INFO_SEVERITY]);
128
167
  const rules = [...errors, ...warnings, ...infos];
129
- assertNotFatal({ subject, output, status, errors, warnings, rules, areWarningsFatal });
168
+ const notes = fatalNotes(diagnostics);
169
+ assertNotFatal({
170
+ subject,
171
+ output: withoutSuccessSummary(output),
172
+ status,
173
+ errors,
174
+ warnings,
175
+ rules,
176
+ notes,
177
+ areWarningsFatal,
178
+ });
130
179
  reportDiagnostics(subject, [...warnings, ...infos]);
131
180
  };
132
181
 
@@ -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 };
@@ -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 defaultDevRunnerDeps = (configFile: string): DevRunnerDeps => ({
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
- plugins: (entryPath) => [
90
- ...gtkxVitePlugins({ mode: DEV_MODE, entryPath, configFile }),
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.deps.log(`Translation catalog changed: ${change.path}`);
380
- await requestRestart(session);
389
+ await restartForCatalog(session, change.path);
381
390
 
382
391
  return;
383
392
  }
@@ -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 didChange = didWatchPathsChange(previous, paths);
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 (didChange && !state.isRestartPending) {
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(`${basename(state.configFile)} changed; regenerating bindings...`);
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 isWatchedChange = (state: SupervisorState, names: Set<string>, filename: string | Buffer | null): boolean => {
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 false;
281
+ return undefined;
265
282
  }
266
283
 
267
- return names.has(basename(filename.toString()));
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 didWatchPathsChange = (
337
+ const changedWatchPaths = (
319
338
  previous: ReadonlyMap<string, string | null>,
320
339
  paths: string[],
321
- ): boolean => {
322
- for (const path of watchTargets(paths)) {
340
+ ): string[] =>
341
+ [...watchTargets(paths)].filter((path) => {
323
342
  const current = watchPathState(path);
324
343
 
325
- if (previous.has(path) ? previous.get(path) !== current : current !== null) {
326
- return true;
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
- if (isWatchedChange(state, names, filename)) {
354
- scheduleRestart(state);
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,
@@ -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
- isChanged: !isExisting || !readFileSync(output).equals(readFileSync(catalog.path)),
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 synchronizeCatalogs = (project: CatalogProject): void => {
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
- for (const catalog of merged) {
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
  };
@@ -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: string,
189
- loadConfig: ConfigLoader = createConfigLoader(),
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 };
@@ -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 { buildManifest, configFile, entryPath, mode, shouldPreserveI18nMetadata = true } = options;
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
- shouldPreserveI18nMetadata,
39
- mode === "development",
40
- )]),
46
+ onCatalogsWritten,
47
+ shouldPreserveMetadataMessages: shouldPreserveI18nMetadata,
48
+ shouldRecoverExtractionErrors: mode === "development",
49
+ })]),
41
50
  gtkxStoreLinks(),
42
51
  gtkxUndeclaredLibrary(loadConfig),
43
52
  gtkxSettings(buildManifest),