@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.
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen/config-dependencies.d.ts +3 -0
- package/dist/codegen/config-dependencies.d.ts.map +1 -0
- package/dist/codegen/config-dependencies.js +355 -0
- package/dist/codegen/config-dependencies.js.map +1 -0
- package/dist/codegen/run-codegen.d.ts +3 -2
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +31 -6
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/codegen/store-resolver.d.ts +1 -0
- package/dist/codegen/store-resolver.d.ts.map +1 -1
- package/dist/codegen/store-resolver.js +4 -2
- package/dist/codegen/store-resolver.js.map +1 -1
- package/dist/commands/cleanup.d.ts +9 -0
- package/dist/commands/cleanup.d.ts.map +1 -0
- package/dist/commands/cleanup.js +31 -0
- package/dist/commands/cleanup.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +4 -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 +1 -0
- package/dist/deploy/freedesktop/validate.js.map +1 -1
- package/dist/deploy/payload/stage.d.ts.map +1 -1
- package/dist/deploy/payload/stage.js +24 -3
- package/dist/deploy/payload/stage.js.map +1 -1
- package/dist/deploy/settings/extra-files.d.ts.map +1 -1
- package/dist/deploy/settings/extra-files.js +4 -3
- package/dist/deploy/settings/extra-files.js.map +1 -1
- package/dist/dev/supervisor.d.ts +2 -1
- package/dist/dev/supervisor.d.ts.map +1 -1
- package/dist/dev/supervisor.js +120 -24
- package/dist/dev/supervisor.js.map +1 -1
- package/dist/i18n/source-messages.d.ts +3 -1
- package/dist/i18n/source-messages.d.ts.map +1 -1
- package/dist/i18n/source-messages.js +14 -2
- package/dist/i18n/source-messages.js.map +1 -1
- package/dist/internal/build-output.d.ts.map +1 -1
- package/dist/internal/build-output.js +45 -8
- package/dist/internal/build-output.js.map +1 -1
- package/dist/internal/prepare-project.d.ts +1 -0
- package/dist/internal/prepare-project.d.ts.map +1 -1
- package/dist/internal/prepare-project.js +8 -2
- package/dist/internal/prepare-project.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts +1 -1
- package/dist/vite-plugins/i18n.d.ts.map +1 -1
- package/dist/vite-plugins/i18n.js +18 -5
- package/dist/vite-plugins/i18n.js.map +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/package.json +12 -10
- package/src/cli.ts +5 -0
- package/src/codegen/config-dependencies.ts +470 -0
- package/src/codegen/run-codegen.ts +37 -5
- package/src/codegen/store-resolver.ts +5 -2
- package/src/commands/cleanup.ts +36 -0
- package/src/commands/dev.ts +9 -2
- package/src/deploy/freedesktop/validate.ts +1 -0
- package/src/deploy/payload/stage.ts +42 -3
- package/src/deploy/settings/extra-files.ts +5 -3
- package/src/dev/supervisor.ts +158 -27
- package/src/i18n/source-messages.ts +19 -3
- package/src/internal/build-output.ts +67 -9
- package/src/internal/prepare-project.ts +9 -3
- package/src/vite-plugins/i18n.ts +22 -3
- 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
|
|
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[] => {
|
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
|
-
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
|
-
|
|
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
|
|
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 ||
|
|
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(
|
|
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
|
|
222
|
+
runPendingRestart(state);
|
|
172
223
|
|
|
173
224
|
return;
|
|
174
225
|
}
|
|
175
226
|
|
|
176
|
-
if (state
|
|
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
|
|
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(() =>
|
|
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 =
|
|
337
|
+
const { directory, name } = nearestWatchTarget(path);
|
|
219
338
|
const names = namesByDirectory.get(directory) ?? new Set<string>();
|
|
220
|
-
names.add(
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
+
return ancestor;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
ancestor = dirname(ancestor);
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
return
|
|
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)
|
|
75
|
-
throw
|
|
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
|
|
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
|
|
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 {
|
|
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 };
|
package/src/vite-plugins/i18n.ts
CHANGED
|
@@ -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,
|
|
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: () =>
|
|
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
|
|
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),
|