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

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 (68) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/codegen/config-dependencies.d.ts +3 -0
  4. package/dist/codegen/config-dependencies.d.ts.map +1 -0
  5. package/dist/codegen/config-dependencies.js +355 -0
  6. package/dist/codegen/config-dependencies.js.map +1 -0
  7. package/dist/codegen/run-codegen.d.ts +3 -2
  8. package/dist/codegen/run-codegen.d.ts.map +1 -1
  9. package/dist/codegen/run-codegen.js +31 -6
  10. package/dist/codegen/run-codegen.js.map +1 -1
  11. package/dist/codegen/store-resolver.d.ts +1 -0
  12. package/dist/codegen/store-resolver.d.ts.map +1 -1
  13. package/dist/codegen/store-resolver.js +4 -2
  14. package/dist/codegen/store-resolver.js.map +1 -1
  15. package/dist/commands/cleanup.d.ts +9 -0
  16. package/dist/commands/cleanup.d.ts.map +1 -0
  17. package/dist/commands/cleanup.js +31 -0
  18. package/dist/commands/cleanup.js.map +1 -0
  19. package/dist/commands/dev.d.ts.map +1 -1
  20. package/dist/commands/dev.js +4 -2
  21. package/dist/commands/dev.js.map +1 -1
  22. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  23. package/dist/deploy/freedesktop/validate.js +1 -0
  24. package/dist/deploy/freedesktop/validate.js.map +1 -1
  25. package/dist/deploy/payload/stage.d.ts.map +1 -1
  26. package/dist/deploy/payload/stage.js +24 -3
  27. package/dist/deploy/payload/stage.js.map +1 -1
  28. package/dist/deploy/settings/extra-files.d.ts.map +1 -1
  29. package/dist/deploy/settings/extra-files.js +4 -3
  30. package/dist/deploy/settings/extra-files.js.map +1 -1
  31. package/dist/dev/supervisor.d.ts +2 -1
  32. package/dist/dev/supervisor.d.ts.map +1 -1
  33. package/dist/dev/supervisor.js +120 -24
  34. package/dist/dev/supervisor.js.map +1 -1
  35. package/dist/i18n/source-messages.d.ts +3 -1
  36. package/dist/i18n/source-messages.d.ts.map +1 -1
  37. package/dist/i18n/source-messages.js +14 -2
  38. package/dist/i18n/source-messages.js.map +1 -1
  39. package/dist/internal/build-output.d.ts.map +1 -1
  40. package/dist/internal/build-output.js +45 -8
  41. package/dist/internal/build-output.js.map +1 -1
  42. package/dist/internal/prepare-project.d.ts +1 -0
  43. package/dist/internal/prepare-project.d.ts.map +1 -1
  44. package/dist/internal/prepare-project.js +8 -2
  45. package/dist/internal/prepare-project.js.map +1 -1
  46. package/dist/vite-plugins/i18n.d.ts +1 -1
  47. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  48. package/dist/vite-plugins/i18n.js +18 -5
  49. package/dist/vite-plugins/i18n.js.map +1 -1
  50. package/dist/vite-plugins/index.d.ts.map +1 -1
  51. package/dist/vite-plugins/index.js +3 -1
  52. package/dist/vite-plugins/index.js.map +1 -1
  53. package/package.json +12 -10
  54. package/src/cli.ts +5 -0
  55. package/src/codegen/config-dependencies.ts +470 -0
  56. package/src/codegen/run-codegen.ts +37 -5
  57. package/src/codegen/store-resolver.ts +5 -2
  58. package/src/commands/cleanup.ts +36 -0
  59. package/src/commands/dev.ts +9 -2
  60. package/src/deploy/freedesktop/validate.ts +1 -0
  61. package/src/deploy/payload/stage.ts +42 -3
  62. package/src/deploy/settings/extra-files.ts +5 -3
  63. package/src/dev/supervisor.ts +158 -27
  64. package/src/i18n/source-messages.ts +19 -3
  65. package/src/internal/build-output.ts +67 -9
  66. package/src/internal/prepare-project.ts +9 -3
  67. package/src/vite-plugins/i18n.ts +22 -3
  68. package/src/vite-plugins/index.ts +8 -1
@@ -1,4 +1,4 @@
1
- import { sortStringsBy } from "@gtkx/utils";
1
+ import { sortStringsBy, warn } from "@gtkx/utils";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import type { DeploySettings, DeployTargetName, NodeRuntime, NoticeSection, StagedFile } from "../types.js";
@@ -82,9 +82,48 @@ const stageMetadata = (settings: DeploySettings, root: string, metadata: StagedM
82
82
  : [writeInto(root, join(SHARE_MIME_PACKAGES, `${settings.applicationId}.xml`), metadata.mimePackage)]),
83
83
  ];
84
84
 
85
- const stageExtraFiles = (settings: DeploySettings, root: string): StagedFile[] =>
85
+ const generatedMetadataKind = (
86
+ settings: DeploySettings,
87
+ metadata: StagedMetadata,
88
+ destination: string,
89
+ ): string | null => {
90
+ if (destination === join(SHARE_APPLICATIONS, `${settings.applicationId}.desktop`)) {
91
+ return "desktop entry";
92
+ }
93
+
94
+ if (destination === join(SHARE_METAINFO, `${settings.applicationId}.metainfo.xml`)) {
95
+ return "metainfo";
96
+ }
97
+
98
+ if (
99
+ metadata.mimePackage !== null &&
100
+ destination === join(SHARE_MIME_PACKAGES, `${settings.applicationId}.xml`)
101
+ ) {
102
+ return "MIME package";
103
+ }
104
+
105
+ return null;
106
+ };
107
+
108
+ const warnMetadataCollision = (
109
+ settings: DeploySettings,
110
+ metadata: StagedMetadata,
111
+ file: DeploySettings["extraFiles"][number],
112
+ ): void => {
113
+ const kind = generatedMetadataKind(settings, metadata, file.destination);
114
+
115
+ if (kind !== null) {
116
+ warn(
117
+ `extraFiles source "${file.source}" targeting "${file.destination}" is overridden by the ` +
118
+ `GTKX-generated ${kind}`,
119
+ );
120
+ }
121
+ };
122
+
123
+ const stageExtraFiles = (settings: DeploySettings, root: string, metadata: StagedMetadata): StagedFile[] =>
86
124
  settings.extraFiles.map((file) => {
87
125
  const source = resolve(settings.paths.root, file.source);
126
+ warnMetadataCollision(settings, metadata, file);
88
127
 
89
128
  return copyInto(root, file.destination, source, file.mode ?? executableModeFor(source));
90
129
  });
@@ -108,7 +147,7 @@ const stagePayload = ({ settings, node, metadata }: StageRequest): StagedFile[]
108
147
  ...stageNodeBinary(settings, root, node),
109
148
  ...stageRuntimeFiles(settings, root),
110
149
  ...stageCatalogs(settings, root),
111
- ...stageExtraFiles(settings, root),
150
+ ...stageExtraFiles(settings, root, metadata),
112
151
  ...stageMetadata(settings, root, metadata),
113
152
  ...stageIcons(settings, root),
114
153
  ...stageSchemas(settings, root),
@@ -1,5 +1,5 @@
1
1
  import { statSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { normalize, resolve } from "node:path";
3
3
  import type { DeployConfig, DeployExtraFile } from "../types.js";
4
4
 
5
5
  type ExtraFiles = NonNullable<DeployConfig["extraFiles"]>;
@@ -22,13 +22,15 @@ const assertSourceFile = (root: string, file: DeployExtraFile): void => {
22
22
  };
23
23
 
24
24
  const resolveExtraFile = (destination: string, entry: ExtraFileEntry): DeployExtraFile => {
25
+ const normalizedDestination = normalize(destination);
26
+
25
27
  if (typeof entry === "string") {
26
- return { destination, source: entry, mode: null };
28
+ return { destination: normalizedDestination, source: entry, mode: null };
27
29
  }
28
30
 
29
31
  const mode = entry.mode === undefined ? null : Number.parseInt(entry.mode, OCTAL_RADIX);
30
32
 
31
- return { destination, source: entry.source, mode };
33
+ return { destination: normalizedDestination, source: entry.source, mode };
32
34
  };
33
35
 
34
36
  const resolveExtraFiles = (root: string, deploy: DeployConfig): DeployExtraFile[] => {
@@ -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
- import { type FSWatcher, watch as watchFs } from "node:fs";
4
- import { basename, dirname } from "node:path";
3
+ import { type FSWatcher, statSync, watch as watchFs } from "node:fs";
4
+ import { basename, dirname, join } 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
 
@@ -18,7 +18,8 @@ type ForkRunner = (modulePath: string, args: string[], env: NodeJS.ProcessEnv, c
18
18
 
19
19
  type DevWatch = {
20
20
  paths: string[];
21
- regenerate: () => Promise<void>;
21
+ resolvePaths: () => string[];
22
+ regenerate: () => Promise<string[]>;
22
23
  };
23
24
 
24
25
  type SupervisorState = {
@@ -29,10 +30,12 @@ type SupervisorState = {
29
30
  args: string[];
30
31
  watch: DevWatch | undefined;
31
32
  watchers: FSWatcher[];
33
+ restartTimer: DebounceTimer;
32
34
  fork: ForkRunner;
33
35
  child: SupervisedChild | null;
34
36
  isShuttingDown: boolean;
35
37
  isRestarting: boolean;
38
+ isRestartPending: boolean;
36
39
  capturedChildExit: number | undefined;
37
40
  };
38
41
 
@@ -141,39 +144,87 @@ const launch = (state: SupervisorState): void => {
141
144
  });
142
145
  };
143
146
 
144
- const isRestartBlocked = (state: SupervisorState): boolean => state.isRestarting || state.isShuttingDown;
145
-
146
- const relaunchAfterExit = (state: SupervisorState): void => {
147
+ const runPendingRestart = (state: SupervisorState): void => {
147
148
  state.isRestarting = false;
148
149
 
150
+ if (!state.isRestartPending || state.isShuttingDown) {
151
+ return;
152
+ }
153
+
154
+ state.isRestartPending = false;
155
+ void restart(state);
156
+ };
157
+
158
+ const relaunchAfterExit = (state: SupervisorState): void => {
149
159
  if (state.isShuttingDown) {
160
+ state.isRestarting = false;
161
+
150
162
  return;
151
163
  }
152
164
 
153
165
  info("Restarting dev runner...");
154
166
  launch(state);
167
+ runPendingRestart(state);
168
+ };
169
+
170
+ const isSupervisorShuttingDown = (state: SupervisorState): boolean => state.isShuttingDown;
171
+
172
+ const reconcileConfigWatchers = (
173
+ state: SupervisorState,
174
+ paths: string[],
175
+ previous: ReadonlyMap<string, string | null>,
176
+ ): void => {
177
+ const didChange = didWatchPathsChange(previous, paths);
178
+
179
+ closeWatchers(state);
180
+
181
+ if (state.watch !== undefined) {
182
+ state.watch.paths = paths;
183
+ }
184
+
185
+ if (state.isShuttingDown) {
186
+ return;
187
+ }
188
+
189
+ installConfigWatchers(state);
190
+
191
+ if (didChange && !state.isRestartPending) {
192
+ scheduleRestart(state);
193
+ }
155
194
  };
156
195
 
157
196
  const restart = async (state: SupervisorState): Promise<void> => {
158
197
  const watch = state.watch;
159
198
 
160
- if (watch === undefined || isRestartBlocked(state)) {
199
+ if (watch === undefined || state.isShuttingDown) {
200
+ return;
201
+ }
202
+
203
+ if (state.isRestarting) {
204
+ state.isRestartPending = true;
205
+
161
206
  return;
162
207
  }
163
208
 
164
209
  state.isRestarting = true;
165
- info("gtkx.config.ts changed; regenerating bindings...");
210
+ info(`${basename(state.configFile)} changed; regenerating bindings...`);
211
+ closeWatchers(state);
212
+ watch.paths = watch.resolvePaths();
213
+ const previous = snapshotWatchPaths(watch.paths);
214
+ installConfigWatchers(state);
166
215
 
167
216
  try {
168
- await watch.regenerate();
217
+ const paths = await watch.regenerate();
218
+ reconcileConfigWatchers(state, paths, previous);
169
219
  } catch (error_) {
220
+ reconcileConfigWatchers(state, watch.resolvePaths(), previous);
170
221
  error("Codegen failed; keeping the current dev runner. Fix the error and save again.", error_);
171
- state.isRestarting = false;
222
+ runPendingRestart(state);
172
223
 
173
224
  return;
174
225
  }
175
226
 
176
- if (state.isShuttingDown) {
227
+ if (isSupervisorShuttingDown(state)) {
177
228
  state.isRestarting = false;
178
229
 
179
230
  return;
@@ -182,8 +233,8 @@ const restart = async (state: SupervisorState): Promise<void> => {
182
233
  const current = state.child;
183
234
 
184
235
  if (current === null) {
185
- state.isRestarting = false;
186
236
  launch(state);
237
+ runPendingRestart(state);
187
238
 
188
239
  return;
189
240
  }
@@ -195,12 +246,17 @@ const restart = async (state: SupervisorState): Promise<void> => {
195
246
  forwardSignal(current, "SIGTERM");
196
247
  };
197
248
 
198
- const scheduleRestart = (state: SupervisorState, timer: DebounceTimer): void => {
249
+ const scheduleRestart = (state: SupervisorState): void => {
250
+ const timer = state.restartTimer;
251
+
199
252
  if (timer.handle !== null) {
200
253
  clearTimeout(timer.handle);
201
254
  }
202
255
 
203
- timer.handle = setTimeout(() => void restart(state), CONFIG_DEBOUNCE_MS);
256
+ timer.handle = setTimeout(() => {
257
+ timer.handle = null;
258
+ void restart(state);
259
+ }, CONFIG_DEBOUNCE_MS);
204
260
  };
205
261
 
206
262
  const isWatchedChange = (state: SupervisorState, names: Set<string>, filename: string | Buffer | null): boolean => {
@@ -211,13 +267,76 @@ const isWatchedChange = (state: SupervisorState, names: Set<string>, filename: s
211
267
  return names.has(basename(filename.toString()));
212
268
  };
213
269
 
270
+ const isDirectory = (path: string): boolean => {
271
+ try {
272
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() === true;
273
+ } catch {
274
+ return false;
275
+ }
276
+ };
277
+
278
+ const nearestWatchTarget = (path: string): { directory: string; name: string } => {
279
+ let directory = dirname(path);
280
+ let name = basename(path);
281
+
282
+ while (!isDirectory(directory)) {
283
+ const parent = dirname(directory);
284
+
285
+ if (parent === directory) {
286
+ break;
287
+ }
288
+
289
+ name = basename(directory);
290
+ directory = parent;
291
+ }
292
+
293
+ return { directory, name };
294
+ };
295
+
296
+ const watchTargets = (paths: string[]): Set<string> =>
297
+ new Set(paths.flatMap((path) => {
298
+ const { directory, name } = nearestWatchTarget(path);
299
+
300
+ return [path, join(directory, name)];
301
+ }));
302
+
303
+ const watchPathState = (path: string): string | null => {
304
+ try {
305
+ const stat = statSync(path, { bigint: true, throwIfNoEntry: false });
306
+
307
+ return stat === undefined
308
+ ? null
309
+ : [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
310
+ } catch {
311
+ return null;
312
+ }
313
+ };
314
+
315
+ const snapshotWatchPaths = (paths: string[]): Map<string, string | null> =>
316
+ new Map([...watchTargets(paths)].map((path) => [path, watchPathState(path)]));
317
+
318
+ const didWatchPathsChange = (
319
+ previous: ReadonlyMap<string, string | null>,
320
+ paths: string[],
321
+ ): boolean => {
322
+ for (const path of watchTargets(paths)) {
323
+ const current = watchPathState(path);
324
+
325
+ if (previous.has(path) ? previous.get(path) !== current : current !== null) {
326
+ return true;
327
+ }
328
+ }
329
+
330
+ return false;
331
+ };
332
+
214
333
  const groupWatchNamesByDirectory = (paths: string[]): Map<string, Set<string>> => {
215
334
  const namesByDirectory: Map<string, Set<string>> = new Map();
216
335
 
217
336
  for (const path of paths) {
218
- const directory = dirname(path);
337
+ const { directory, name } = nearestWatchTarget(path);
219
338
  const names = namesByDirectory.get(directory) ?? new Set<string>();
220
- names.add(basename(path));
339
+ names.add(name);
221
340
  namesByDirectory.set(directory, names);
222
341
  }
223
342
 
@@ -228,16 +347,21 @@ const watchConfigDirectory = (
228
347
  state: SupervisorState,
229
348
  directory: string,
230
349
  names: Set<string>,
231
- timer: DebounceTimer,
232
350
  ): void => {
233
- const watcher = watchFs(directory, (_event, filename) => {
234
- if (isWatchedChange(state, names, filename)) {
235
- scheduleRestart(state, timer);
236
- }
237
- });
351
+ try {
352
+ const watcher = watchFs(directory, (_event, filename) => {
353
+ if (isWatchedChange(state, names, filename)) {
354
+ scheduleRestart(state);
355
+ }
356
+ });
238
357
 
239
- watcher.on("error", (): void => undefined);
240
- state.watchers.push(watcher);
358
+ watcher.on("error", (cause) => {
359
+ error(`Configuration watch failed for ${directory}`, cause);
360
+ });
361
+ state.watchers.push(watcher);
362
+ } catch (error_) {
363
+ error(`Configuration watch failed for ${directory}`, error_);
364
+ }
241
365
  };
242
366
 
243
367
  const installConfigWatchers = (state: SupervisorState): void => {
@@ -247,10 +371,8 @@ const installConfigWatchers = (state: SupervisorState): void => {
247
371
  return;
248
372
  }
249
373
 
250
- const timer: DebounceTimer = { handle: null };
251
-
252
374
  for (const [directory, names] of groupWatchNamesByDirectory(watch.paths)) {
253
- watchConfigDirectory(state, directory, names, timer);
375
+ watchConfigDirectory(state, directory, names);
254
376
  }
255
377
  };
256
378
 
@@ -258,6 +380,8 @@ const closeWatchers = (state: SupervisorState): void => {
258
380
  for (const watcher of state.watchers) {
259
381
  watcher.close();
260
382
  }
383
+
384
+ state.watchers = [];
261
385
  };
262
386
 
263
387
  const shutdownOnSignal = (state: SupervisorState, signal: NodeJS.Signals): Promise<void> =>
@@ -265,6 +389,11 @@ const shutdownOnSignal = (state: SupervisorState, signal: NodeJS.Signals): Promi
265
389
  state.isShuttingDown = true;
266
390
  closeWatchers(state);
267
391
 
392
+ if (state.restartTimer.handle !== null) {
393
+ clearTimeout(state.restartTimer.handle);
394
+ state.restartTimer.handle = null;
395
+ }
396
+
268
397
  if (!state.child) {
269
398
  resolve();
270
399
 
@@ -298,10 +427,12 @@ const runDevSupervisor = async (options: DevSupervisorOptions): Promise<never> =
298
427
  args,
299
428
  watch,
300
429
  watchers: [],
430
+ restartTimer: { handle: null },
301
431
  fork,
302
432
  child: null,
303
433
  isShuttingDown: false,
304
434
  isRestarting: false,
435
+ isRestartPending: false,
305
436
  capturedChildExit: undefined,
306
437
  };
307
438
 
@@ -116,8 +116,24 @@ const STATIC_KEY_WRAPPER_TYPES: ReadonlySet<string> = new Set([
116
116
 
117
117
  class SourceExtractionError extends Error {}
118
118
 
119
- const sourceErrorMessage = (error: unknown): string =>
120
- error instanceof Error ? error.message : String(error);
119
+ const sourceErrorMessage = (error: unknown): string => {
120
+ if (!(error instanceof Error)) {
121
+ return String(error);
122
+ }
123
+
124
+ if (
125
+ "code" in error &&
126
+ error.code === "BABEL_PARSE_ERROR"
127
+ ) {
128
+ const separator = error.message.indexOf(": ");
129
+
130
+ if (separator !== -1) {
131
+ return error.message.slice(separator + 2);
132
+ }
133
+ }
134
+
135
+ return error.message;
136
+ };
121
137
 
122
138
  const projectSourcePath = (root: string, path: string): string => {
123
139
  const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
@@ -1968,4 +1984,4 @@ const extractSourceCatalog = async (
1968
1984
  extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages });
1969
1985
  };
1970
1986
 
1971
- export { extractSourceCatalog, extractSourceCatalogTo };
1987
+ export { extractSourceCatalog, extractSourceCatalogTo, SourceExtractionError };
@@ -53,27 +53,85 @@ const isReusableBuildDirectory = (path: string): boolean => {
53
53
  return stats.isDirectory() && (readdirSync(path).length === 0 || isGtkxBuildDirectory(path));
54
54
  };
55
55
 
56
- const buildOutputError = (root: string): Error =>
57
- new Error(`Build output must be an empty directory or an earlier GTKX build below the project root ${root}`);
56
+ const outputName = (root: string, path: string): string => relative(root, path) || ".";
58
57
 
59
- const hasGtkxBuildAncestor = (root: string, outDir: string): boolean => {
58
+ const gtkxBuildAncestor = (root: string, outDir: string): string | null => {
60
59
  let ancestor = dirname(outDir);
61
60
 
62
61
  while (isPathInside(root, ancestor)) {
63
62
  if (isGtkxBuildDirectory(ancestor)) {
64
- return true;
63
+ return ancestor;
65
64
  }
66
65
 
67
66
  ancestor = dirname(ancestor);
68
67
  }
69
68
 
70
- return false;
69
+ return null;
70
+ };
71
+
72
+ const childDirectories = (parent: string): string[] =>
73
+ readdirSync(parent, { withFileTypes: true })
74
+ .filter((entry) => entry.isDirectory())
75
+ .map((entry) => join(parent, entry.name));
76
+
77
+ const gtkxBuildDescendant = (root: string): string | null => {
78
+ const pending = childDirectories(root);
79
+
80
+ while (pending.length > 0) {
81
+ const path = pending.pop();
82
+
83
+ if (path === undefined) {
84
+ return null;
85
+ }
86
+
87
+ if (isGtkxBuildDirectory(path)) {
88
+ return path;
89
+ }
90
+
91
+ pending.push(...childDirectories(path));
92
+ }
93
+
94
+ return null;
71
95
  };
72
96
 
73
97
  const assertSafeBuildLocation = (root: string, outDir: string): void => {
74
- if (!isPathInside(root, outDir) || hasSymlinkComponent(root, outDir) || hasGtkxBuildAncestor(root, outDir)) {
75
- throw buildOutputError(root);
98
+ if (!isPathInside(root, outDir)) {
99
+ throw new Error(`Build output ${outDir} must be below the project root ${root}`);
76
100
  }
101
+
102
+ if (hasSymlinkComponent(root, outDir)) {
103
+ throw new Error(`Build output ${outputName(root, outDir)} passes through a symbolic link`);
104
+ }
105
+
106
+ const ancestor = gtkxBuildAncestor(root, outDir);
107
+
108
+ if (ancestor !== null) {
109
+ throw new Error(
110
+ `Build output ${outputName(root, outDir)} is nested inside the earlier GTKX build ` +
111
+ outputName(root, ancestor),
112
+ );
113
+ }
114
+ };
115
+
116
+ const nonReusableBuildOutputError = (root: string, outDir: string): Error => {
117
+ const stats = lstatSync(outDir, { throwIfNoEntry: false });
118
+
119
+ if (stats?.isDirectory() !== true) {
120
+ return new Error(`Build output ${outputName(root, outDir)} exists and is not a directory`);
121
+ }
122
+
123
+ const descendant = gtkxBuildDescendant(outDir);
124
+
125
+ if (descendant !== null) {
126
+ return new Error(
127
+ `Build output ${outputName(root, outDir)} contains the earlier GTKX build ` +
128
+ outputName(root, descendant),
129
+ );
130
+ }
131
+
132
+ return new Error(
133
+ `Build output ${outputName(root, outDir)} is nonempty and is not an earlier GTKX build`,
134
+ );
77
135
  };
78
136
 
79
137
  const resolveBuildOutDir = (root: string, configured?: string): string => {
@@ -83,7 +141,7 @@ const resolveBuildOutDir = (root: string, configured?: string): string => {
83
141
  assertSafeBuildLocation(root, outDir);
84
142
 
85
143
  if (!isReusableBuildDirectory(outDir)) {
86
- throw buildOutputError(root);
144
+ throw nonReusableBuildOutputError(root, outDir);
87
145
  }
88
146
 
89
147
  return outDir;
@@ -94,7 +152,7 @@ const prepareBuildOutDir = (root: string, outDir: string): PreparedBuildOutDir =
94
152
  const prepared = prepareOutputDirectory(root, outDir, isReusableBuildDirectory);
95
153
 
96
154
  if (prepared.status === "unsafe") {
97
- throw buildOutputError(root);
155
+ throw new Error(`Build output ${outputName(root, outDir)} became unsafe while preparing it`);
98
156
  }
99
157
 
100
158
  const transaction: OutputDirectoryTransaction = prepared.transaction;
@@ -3,21 +3,27 @@ import { type CodegenContext, resolveCodegenContext } from "../codegen/store-res
3
3
  import { resolveCwd, resolveEntry } from "./entry-arg.js";
4
4
 
5
5
  type ProjectArgs = { entry?: string; cwd?: string; config?: string };
6
- type PreparedProject = { cwd: string; entry: string; configFile: string };
6
+ type PreparedProject = { cwd: string; entry: string; configFile: string; configDependencies: string[] };
7
7
  type ResolvedProject = PreparedProject & { context: CodegenContext };
8
8
 
9
9
  const resolveProject = async (args: ProjectArgs, mode: string): Promise<ResolvedProject> => {
10
10
  const cwd = resolveCwd(args);
11
11
  const context = await resolveCodegenContext(cwd, mode, args.config);
12
12
 
13
- return { cwd, entry: resolveEntry(cwd, args.entry), configFile: context.configFile, context };
13
+ return {
14
+ cwd,
15
+ entry: resolveEntry(cwd, args.entry),
16
+ configFile: context.configFile,
17
+ configDependencies: context.configDependencies,
18
+ context,
19
+ };
14
20
  };
15
21
 
16
22
  const prepareProject = async (args: ProjectArgs, mode: string): Promise<PreparedProject> => {
17
23
  const { cwd, entry, context } = await resolveProject(args, mode);
18
24
  await ensureGeneratedIn(context, { shouldAnnounce: true, mode });
19
25
 
20
- return { cwd, entry, configFile: context.configFile };
26
+ return { cwd, entry, configFile: context.configFile, configDependencies: context.configDependencies };
21
27
  };
22
28
 
23
29
  export { prepareProject, resolveProject };
@@ -12,7 +12,7 @@ import {
12
12
  resolveCatalogProject,
13
13
  synchronizeCatalogs,
14
14
  } from "../i18n/catalogs.js";
15
- import { extractSourceCatalog } from "../i18n/source-messages.js";
15
+ import { extractSourceCatalog, SourceExtractionError } from "../i18n/source-messages.js";
16
16
  import { emitI18nTypes } from "../i18n/types.js";
17
17
  import { discoverSourceFiles, sourceLanguage } from "../internal/source-imports.js";
18
18
  import { stripQuery } from "./strip-query.js";
@@ -160,18 +160,35 @@ const isProjectSource = (state: I18nState, path: string): boolean => {
160
160
  const refreshProjectMessages = async (
161
161
  state: I18nState,
162
162
  shouldPreserveMetadataMessages: boolean,
163
+ shouldSynchronizeCatalogs = false,
163
164
  ): Promise<void> => {
164
165
  try {
165
- await queueExtraction(state, shouldPreserveMetadataMessages, false);
166
+ await queueExtraction(state, shouldPreserveMetadataMessages, shouldSynchronizeCatalogs);
166
167
  } catch (error) {
167
168
  logger.error(error instanceof Error ? error.message : String(error));
168
169
  }
169
170
  };
170
171
 
172
+ const recoverInitialSourceExtraction = async (
173
+ state: I18nState,
174
+ shouldPreserveMetadataMessages: boolean,
175
+ ): Promise<void> => {
176
+ try {
177
+ await queueExtraction(state, shouldPreserveMetadataMessages, true);
178
+ } catch (error) {
179
+ if (!(error instanceof SourceExtractionError)) {
180
+ throw error;
181
+ }
182
+
183
+ logger.error(error.message);
184
+ }
185
+ };
186
+
171
187
  const gtkxI18n = (
172
188
  entryPath: string,
173
189
  loadConfig: ConfigLoader = createConfigLoader(),
174
190
  shouldPreserveMetadataMessages = true,
191
+ shouldRecoverExtractionErrors = false,
175
192
  ): Plugin => {
176
193
  const state: I18nState = {
177
194
  entryPath,
@@ -188,7 +205,9 @@ const gtkxI18n = (
188
205
 
189
206
  configResolved: (config) => applyResolvedConfig(state, config, loadConfig),
190
207
 
191
- buildStart: () => queueExtraction(state, shouldPreserveMetadataMessages, true),
208
+ buildStart: () => shouldRecoverExtractionErrors
209
+ ? recoverInitialSourceExtraction(state, shouldPreserveMetadataMessages)
210
+ : queueExtraction(state, shouldPreserveMetadataMessages, true),
192
211
 
193
212
  hotUpdate(options) {
194
213
  if (!isProjectSource(state, options.file) || state.hotUpdateTimestamp === options.timestamp) {
@@ -30,7 +30,14 @@ const gtkxVitePlugins = (options: GtkxVitePluginOptions = {}): Plugin[] => {
30
30
 
31
31
  return [
32
32
  createConfigPlugin({ name: "gtkx:config", loadConfig }),
33
- ...(entryPath === undefined ? [] : [gtkxI18n(entryPath, loadConfig, shouldPreserveI18nMetadata)]),
33
+ ...(entryPath === undefined
34
+ ? []
35
+ : [gtkxI18n(
36
+ entryPath,
37
+ loadConfig,
38
+ shouldPreserveI18nMetadata,
39
+ mode === "development",
40
+ )]),
34
41
  gtkxStoreLinks(),
35
42
  gtkxUndeclaredLibrary(loadConfig),
36
43
  gtkxSettings(buildManifest),