@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
@@ -0,0 +1,470 @@
1
+ import { parseSync, traverse, types } from "@babel/core";
2
+ import { parseJSON, parseJSON5, parseJSONC, parseTOML, parseYAML } from "confbox";
3
+ import { createJiti, type Jiti } from "jiti";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+
8
+ const MODULE_EXTENSIONS: ReadonlySet<string> = new Set([
9
+ ".cjs",
10
+ ".cts",
11
+ ".js",
12
+ ".mjs",
13
+ ".mts",
14
+ ".ts",
15
+ ]);
16
+ const RESOLVABLE_EXTENSIONS = [
17
+ ".js",
18
+ ".ts",
19
+ ".mjs",
20
+ ".cjs",
21
+ ".mts",
22
+ ".cts",
23
+ ".json",
24
+ ".jsonc",
25
+ ".json5",
26
+ ".yaml",
27
+ ".yml",
28
+ ".toml",
29
+ ];
30
+ const JAVASCRIPT_FALLBACKS: Readonly<Record<string, string[]>> = {
31
+ ".cjs": [".cts"],
32
+ ".js": [".ts"],
33
+ ".jsx": [".tsx"],
34
+ ".mjs": [".mts"],
35
+ };
36
+ const DATA_PARSERS: Readonly<Record<string, (source: string) => unknown>> = {
37
+ ".json": parseJSON,
38
+ ".json5": parseJSON5,
39
+ ".jsonc": parseJSONC,
40
+ ".toml": parseTOML,
41
+ ".yaml": parseYAML,
42
+ ".yml": parseYAML,
43
+ };
44
+
45
+ type PackageImports = { file: string; imports: Record<string, unknown>; root: string };
46
+ type ModuleSources = { all: string[]; extended: string[] };
47
+
48
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
49
+ typeof value === "object" && value !== null && !Array.isArray(value);
50
+
51
+ const parserPlugins = (path: string): ("decorators" | "jsx" | "typescript")[] => {
52
+ const extension = extname(path).toLowerCase();
53
+
54
+ return [
55
+ "decorators",
56
+ ...([".cts", ".mts", ".ts"].includes(extension) ? ["typescript" as const] : []),
57
+ ];
58
+ };
59
+
60
+ function expressionSources(value: types.Expression): string[] {
61
+ if (types.isStringLiteral(value)) {
62
+ return [value.value];
63
+ }
64
+
65
+ if (types.isArrayExpression(value)) {
66
+ return value.elements.flatMap((element) =>
67
+ types.isExpression(element) ? expressionSources(element) : []);
68
+ }
69
+
70
+ return types.isObjectExpression(value)
71
+ ? value.properties.flatMap((property) => sourcePropertySources(property))
72
+ : [];
73
+ }
74
+
75
+ const isNamedProperty = (property: types.ObjectProperty, name: string): boolean =>
76
+ (types.isIdentifier(property.key) && property.key.name === name) ||
77
+ (types.isStringLiteral(property.key) && property.key.value === name);
78
+
79
+ function sourcePropertySources(
80
+ property: types.ObjectMethod | types.ObjectProperty | types.SpreadElement,
81
+ ): string[] {
82
+ return types.isObjectProperty(property) &&
83
+ isNamedProperty(property, "source") &&
84
+ types.isExpression(property.value)
85
+ ? expressionSources(property.value)
86
+ : [];
87
+ }
88
+
89
+ const extendsSources = (value: types.Expression): string[] => expressionSources(value);
90
+
91
+ const isExtendsProperty = (property: types.ObjectProperty): boolean =>
92
+ isNamedProperty(property, "extends");
93
+
94
+ type ParsedModule = NonNullable<ReturnType<typeof parseSync>>;
95
+
96
+ const parseModule = (path: string): ParsedModule | undefined => {
97
+ try {
98
+ return parseSync(readFileSync(path, "utf8"), {
99
+ ast: true,
100
+ babelrc: false,
101
+ configFile: false,
102
+ filename: path,
103
+ parserOpts: { allowReturnOutsideFunction: true, plugins: parserPlugins(path) },
104
+ sourceType: "unambiguous",
105
+ }) ?? undefined;
106
+ } catch {
107
+ return undefined;
108
+ }
109
+ };
110
+
111
+ const addSource = (sources: Set<string>, source: types.StringLiteral | null | undefined): void => {
112
+ if (source !== null && source !== undefined) {
113
+ sources.add(source.value);
114
+ }
115
+ };
116
+
117
+ const requireSource = (call: types.CallExpression): types.StringLiteral | undefined =>
118
+ types.isIdentifier(call.callee, { name: "require" }) && types.isStringLiteral(call.arguments[0])
119
+ ? call.arguments[0]
120
+ : undefined;
121
+
122
+ const importEqualsSource = (declaration: types.TSImportEqualsDeclaration): types.StringLiteral | undefined => {
123
+ const { moduleReference } = declaration;
124
+
125
+ return types.isTSExternalModuleReference(moduleReference) ? moduleReference.expression : undefined;
126
+ };
127
+
128
+ const propertySources = (property: types.ObjectProperty): string[] =>
129
+ isExtendsProperty(property) && types.isExpression(property.value)
130
+ ? extendsSources(property.value)
131
+ : [];
132
+
133
+ const collectModuleSources = (ast: ParsedModule): ModuleSources => {
134
+ const sources: Set<string> = new Set();
135
+ const extended: Set<string> = new Set();
136
+
137
+ traverse(ast, {
138
+ CallExpression: (nodePath) => {
139
+ addSource(sources, requireSource(nodePath.node));
140
+ },
141
+ ExportAllDeclaration: (nodePath) => {
142
+ addSource(sources, nodePath.node.source);
143
+ },
144
+ ExportNamedDeclaration: (nodePath) => {
145
+ addSource(sources, nodePath.node.source);
146
+ },
147
+ ImportDeclaration: (nodePath) => {
148
+ addSource(sources, nodePath.node.source);
149
+ },
150
+ ImportExpression: (nodePath) => {
151
+ const { source } = nodePath.node;
152
+ addSource(sources, types.isStringLiteral(source) ? source : undefined);
153
+ },
154
+ ObjectProperty: (nodePath) => {
155
+ for (const source of propertySources(nodePath.node)) {
156
+ sources.add(source);
157
+ extended.add(source);
158
+ }
159
+ },
160
+ TSImportEqualsDeclaration: (nodePath) => {
161
+ addSource(sources, importEqualsSource(nodePath.node));
162
+ },
163
+ });
164
+
165
+ return { all: [...sources], extended: [...extended] };
166
+ };
167
+
168
+ const moduleSources = (path: string): ModuleSources => {
169
+ const ast = parseModule(path);
170
+
171
+ return ast === undefined ? { all: [], extended: [] } : collectModuleSources(ast);
172
+ };
173
+
174
+ function parsedExtendsSources(value: unknown): string[] {
175
+ if (typeof value === "string") {
176
+ return [value];
177
+ }
178
+
179
+ if (Array.isArray(value)) {
180
+ return value.flatMap((item) => parsedExtendsSources(item));
181
+ }
182
+
183
+ return objectExtendsSources(value);
184
+ }
185
+
186
+ const objectExtendsSources = (value: unknown): string[] => {
187
+ if (typeof value !== "object" || value === null) {
188
+ return [];
189
+ }
190
+
191
+ return "source" in value ? parsedExtendsSources(value.source) : [];
192
+ };
193
+
194
+ const configExtendsSources = (value: unknown): string[] => {
195
+ if (typeof value !== "object" || value === null) {
196
+ return [];
197
+ }
198
+
199
+ return "extends" in value ? parsedExtendsSources(value.extends) : [];
200
+ };
201
+
202
+ const dataSources = (path: string): string[] => {
203
+ const parser = DATA_PARSERS[extname(path).toLowerCase()];
204
+
205
+ if (parser === undefined) {
206
+ return [];
207
+ }
208
+
209
+ try {
210
+ return configExtendsSources(parser(readFileSync(path, "utf8")));
211
+ } catch {
212
+ return [];
213
+ }
214
+ };
215
+
216
+ const extensionCandidates = (path: string): string[] => {
217
+ const extension = extname(path).toLowerCase();
218
+
219
+ if (extension.length === 0) {
220
+ return RESOLVABLE_EXTENSIONS.map((candidate) => `${path}${candidate}`);
221
+ }
222
+
223
+ const stem = path.slice(0, -extension.length);
224
+
225
+ return (JAVASCRIPT_FALLBACKS[extension] ?? []).map((candidate) => `${stem}${candidate}`);
226
+ };
227
+
228
+ const targetCandidates = (target: string, configName: string): string[] => {
229
+ const extension = extname(target);
230
+
231
+ if (extension.length > 0) {
232
+ return [target, ...extensionCandidates(target)];
233
+ }
234
+
235
+ return [
236
+ target,
237
+ ...extensionCandidates(target),
238
+ join(target, configName),
239
+ ...RESOLVABLE_EXTENSIONS.map((candidate) => join(target, `index${candidate}`)),
240
+ ];
241
+ };
242
+
243
+ const localSourceTarget = (source: string, importer: string): string | undefined => {
244
+ const clean = source.split(/[?#]/u, 1)[0] ?? "";
245
+
246
+ if (clean.startsWith("file:")) {
247
+ return fileURLToPath(clean);
248
+ }
249
+
250
+ if (!isAbsolute(clean) && !clean.startsWith(".")) {
251
+ return undefined;
252
+ }
253
+
254
+ return resolve(dirname(importer), clean);
255
+ };
256
+
257
+ const unresolvedCandidates = (source: string, importer: string, configName: string): string[] => {
258
+ const target = localSourceTarget(source, importer);
259
+
260
+ if (target === undefined) {
261
+ return [];
262
+ }
263
+
264
+ return targetCandidates(target, configName);
265
+ };
266
+
267
+ const nearestPackageFile = (importer: string): string | undefined => {
268
+ let directory = dirname(importer);
269
+ let previous = "";
270
+
271
+ while (directory !== previous) {
272
+ const candidate = join(directory, "package.json");
273
+
274
+ if (existsSync(candidate)) {
275
+ return candidate;
276
+ }
277
+
278
+ previous = directory;
279
+ directory = dirname(directory);
280
+ }
281
+
282
+ return undefined;
283
+ };
284
+
285
+ const readPackageImports = (importer: string): PackageImports | undefined => {
286
+ const file = nearestPackageFile(importer);
287
+
288
+ if (file === undefined) {
289
+ return undefined;
290
+ }
291
+
292
+ try {
293
+ const manifest = parseJSON(readFileSync(file, "utf8"));
294
+
295
+ return isRecord(manifest) && isRecord(manifest.imports)
296
+ ? { file, imports: manifest.imports, root: dirname(file) }
297
+ : undefined;
298
+ } catch {
299
+ return undefined;
300
+ }
301
+ };
302
+
303
+ function stringTargets(value: unknown): string[] {
304
+ if (typeof value === "string") {
305
+ return [value];
306
+ }
307
+
308
+ if (Array.isArray(value)) {
309
+ return value.flatMap((item) => stringTargets(item));
310
+ }
311
+
312
+ return isRecord(value)
313
+ ? Object.values(value).flatMap((item) => stringTargets(item))
314
+ : [];
315
+ }
316
+
317
+ const importPatternMatch = (pattern: string, source: string): string | undefined => {
318
+ if (pattern === source) {
319
+ return "";
320
+ }
321
+
322
+ const wildcard = pattern.indexOf("*");
323
+
324
+ if (wildcard === -1) {
325
+ return undefined;
326
+ }
327
+
328
+ const prefix = pattern.slice(0, wildcard);
329
+ const suffix = pattern.slice(wildcard + 1);
330
+
331
+ return source.startsWith(prefix) && source.endsWith(suffix)
332
+ ? source.slice(prefix.length, source.length - suffix.length)
333
+ : undefined;
334
+ };
335
+
336
+ const packageImportTargets = (source: string, imports: Record<string, unknown>): string[] =>
337
+ Object.entries(imports).flatMap(([pattern, value]) => {
338
+ const match = importPatternMatch(pattern, source);
339
+
340
+ return match === undefined
341
+ ? []
342
+ : stringTargets(value).map((target) => target.split("*").join(match));
343
+ });
344
+
345
+ const packageImportDependencies = (source: string, importer: string, configName: string): string[] => {
346
+ if (!source.startsWith("#")) {
347
+ return [];
348
+ }
349
+
350
+ const packageImports = readPackageImports(importer);
351
+
352
+ if (packageImports === undefined) {
353
+ return [];
354
+ }
355
+
356
+ const targets = packageImportTargets(source, packageImports.imports)
357
+ .filter((target) => target.startsWith("."))
358
+ .flatMap((target) => targetCandidates(resolve(packageImports.root, target), configName));
359
+
360
+ return [packageImports.file, ...targets];
361
+ };
362
+
363
+ const normalizedResolvedPath = (path: string): string =>
364
+ path.startsWith("file:") ? fileURLToPath(path) : path;
365
+
366
+ const resolvedSourcePath = (source: string, importer: string, jiti: Jiti): string | undefined => {
367
+ try {
368
+ const path = jiti.esmResolve(source, {
369
+ parentURL: pathToFileURL(importer).href,
370
+ try: true,
371
+ });
372
+
373
+ return path === undefined ? undefined : normalizedResolvedPath(path);
374
+ } catch {
375
+ return undefined;
376
+ }
377
+ };
378
+
379
+ const sourceDependencies = (
380
+ source: string,
381
+ importer: string,
382
+ configName: string,
383
+ jiti: Jiti,
384
+ ): string[] => {
385
+ const dependency = resolvedSourcePath(source, importer, jiti);
386
+ const packageDependencies = packageImportDependencies(source, importer, configName);
387
+
388
+ return dependency === undefined
389
+ ? [...unresolvedCandidates(source, importer, configName), ...packageDependencies]
390
+ : [dependency, ...packageDependencies];
391
+ };
392
+
393
+ const configLayerDirectory = (path: string, configName: string, configRoot: string): string => {
394
+ const segments = configName.split(/[\\/]/u).filter((segment) => segment.length > 0);
395
+ let directory = resolve(path);
396
+ let remaining = segments.length;
397
+
398
+ while (remaining > 0) {
399
+ directory = dirname(directory);
400
+ remaining -= 1;
401
+ }
402
+
403
+ return resolve(directory, ...segments) === resolve(path) ? directory : resolve(configRoot);
404
+ };
405
+
406
+ const c12SourceDependencies = (
407
+ source: string,
408
+ importer: string,
409
+ configName: string,
410
+ configRoot: string,
411
+ ): string[] => {
412
+ const clean = source.split(/[?#]/u, 1)[0] ?? "";
413
+
414
+ if (clean === "." || (!isAbsolute(clean) && !clean.startsWith("."))) {
415
+ return [];
416
+ }
417
+
418
+ const cwd = configLayerDirectory(importer, configName, configRoot);
419
+ const extension = extname(clean);
420
+ const target = (extension.length === 0 || extension === basename(clean))
421
+ ? resolve(cwd, clean, configName)
422
+ : resolve(cwd, clean);
423
+
424
+ return targetCandidates(target, configName);
425
+ };
426
+
427
+ const directDependencies = (path: string, configName: string, configRoot: string): string[] => {
428
+ const module = MODULE_EXTENSIONS.has(extname(path).toLowerCase()) ? moduleSources(path) : undefined;
429
+ const sources = module?.all ?? dataSources(path);
430
+ const extended = module?.extended ?? sources;
431
+ const jiti = createJiti(path, {
432
+ extensions: RESOLVABLE_EXTENSIONS,
433
+ fsCache: false,
434
+ moduleCache: false,
435
+ });
436
+
437
+ return [
438
+ ...sources.flatMap((source) => sourceDependencies(source, path, configName, jiti)),
439
+ ...extended.flatMap((source) => c12SourceDependencies(source, path, configName, configRoot)),
440
+ ]
441
+ .filter((dependency) => !dependency.includes(`${sep}node_modules${sep}`));
442
+ };
443
+
444
+ const resolveConfigDependencies = (
445
+ configFile: string,
446
+ configName = basename(configFile),
447
+ configRoot = dirname(configFile),
448
+ ): string[] => {
449
+ const dependencies: Set<string> = new Set();
450
+ const pending = [resolve(configFile)];
451
+
452
+ while (pending.length > 0) {
453
+ const path = pending.pop();
454
+
455
+ if (path === undefined) {
456
+ break;
457
+ }
458
+
459
+ if (dependencies.has(path)) {
460
+ continue;
461
+ }
462
+
463
+ dependencies.add(path);
464
+ pending.push(...directDependencies(path, configName, configRoot));
465
+ }
466
+
467
+ return [...dependencies];
468
+ };
469
+
470
+ export { resolveConfigDependencies };
@@ -2,6 +2,7 @@ import { runCodegen as runCodegenCore } from "@gtkx/codegen";
2
2
  import { getShadowingStorePaths, sweepProjectStaging } from "@gtkx/codegen/internal";
3
3
  import { type Config, loadConfig } from "@gtkx/config";
4
4
  import {
5
+ configDependenciesFor,
5
6
  isAgentRulesEnabled,
6
7
  resolveElementComponents,
7
8
  resolveElementProps,
@@ -10,13 +11,14 @@ import {
10
11
  } from "@gtkx/config/internal";
11
12
  import { info } from "@gtkx/utils";
12
13
  import { existsSync, rmSync } from "node:fs";
13
- import { join, resolve } from "node:path";
14
+ import { join, relative, resolve } from "node:path";
14
15
  import { resolveCatalogProject, synchronizeCatalogs } from "../i18n/catalogs.js";
15
16
  import { extractSourceCatalog } from "../i18n/source-messages.js";
16
17
  import { clearI18nTypes, emitI18nTypes } from "../i18n/types.js";
17
18
  import { upsertAgentRules } from "../internal/agent-rules.js";
18
19
  import { discoverSourceFiles } from "../internal/source-imports.js";
19
20
  import { emitSchemaEnv } from "../settings/schema.js";
21
+ import { resolveConfigDependencies } from "./config-dependencies.js";
20
22
  import { type CodegenInputs, isCodegenStale, resolveCodegenInputs } from "./freshness.js";
21
23
  import { type ReferenceResult, writeReference } from "./reference.js";
22
24
  import { type CodegenContext, type CodegenStore, resolveCodegenContext } from "./store-resolver.js";
@@ -260,13 +262,43 @@ const resolveConfigWatch = async (
260
262
  cwd: string,
261
263
  mode?: string,
262
264
  configFile?: string,
263
- ): Promise<{ paths: string[]; regenerate: () => Promise<void> }> => {
264
- const loaded = await loadConfig(cwd, { mode, configFile });
265
+ initialDependencies: string[] = [],
266
+ ): Promise<{ paths: string[]; resolvePaths: () => string[]; regenerate: () => Promise<string[]> }> => {
267
+ let selectedConfigFile: string;
268
+
269
+ if (configFile === undefined) {
270
+ const loaded = await loadConfig(cwd, { mode });
271
+ selectedConfigFile = loaded.configFile;
272
+ } else {
273
+ selectedConfigFile = resolve(cwd, configFile);
274
+ }
275
+
276
+ let knownDependencies = initialDependencies;
277
+ const selectedConfigName = relative(cwd, selectedConfigFile);
278
+ const resolvePaths = (): string[] => [
279
+ ...new Set(
280
+ [selectedConfigFile, ...knownDependencies]
281
+ .flatMap((path) => resolveConfigDependencies(path, selectedConfigName, cwd)),
282
+ ),
283
+ ];
265
284
 
266
285
  return {
267
- paths: [resolve(loaded.root, loaded.configFile)],
286
+ paths: resolvePaths(),
287
+ resolvePaths,
268
288
  regenerate: async () => {
269
- await runCodegen({ cwd: loaded.root, mode, configFile: loaded.configFile });
289
+ let loaded: Awaited<ReturnType<typeof loadConfig>>;
290
+
291
+ try {
292
+ loaded = await loadConfig(cwd, { mode, configFile: selectedConfigFile });
293
+ } catch (error) {
294
+ knownDependencies = [...new Set([...knownDependencies, ...configDependenciesFor(error)])];
295
+ throw error;
296
+ }
297
+
298
+ knownDependencies = configDependenciesFor(loaded);
299
+ await runCodegen({ cwd, mode, resolved: loaded });
300
+
301
+ return resolvePaths();
270
302
  },
271
303
  };
272
304
  };
@@ -1,5 +1,6 @@
1
1
  import { resolveStore } from "@gtkx/codegen";
2
2
  import { type Config, loadConfig } from "@gtkx/config";
3
+ import { configDependenciesFor } from "@gtkx/config/internal";
3
4
  import { existsSync, realpathSync } from "node:fs";
4
5
  import { createRequire } from "node:module";
5
6
  import { dirname, join } from "node:path";
@@ -23,6 +24,7 @@ type CodegenContext = {
23
24
  root: string;
24
25
  config: Config;
25
26
  configFile: string;
27
+ configDependencies: string[];
26
28
  };
27
29
 
28
30
  const hasPackage = (require: NodeJS.Require, dir: string, packageName: string): boolean => {
@@ -64,9 +66,10 @@ const resolveCodegenStore = (dir: string): CodegenStore => {
64
66
  };
65
67
 
66
68
  const resolveCodegenContext = async (cwd: string, mode?: string, selectedConfig?: string): Promise<CodegenContext> => {
67
- const { config, configFile } = await loadConfig(cwd, { mode, configFile: selectedConfig });
69
+ const loaded = await loadConfig(cwd, { mode, configFile: selectedConfig });
70
+ const { config, configFile } = loaded;
68
71
 
69
- return { root: cwd, config, configFile };
72
+ return { root: cwd, config, configFile, configDependencies: configDependenciesFor(loaded) };
70
73
  };
71
74
 
72
75
  export { resolveCodegenStore, resolveCodegenContext, type CodegenContext, type CodegenStore };
@@ -0,0 +1,36 @@
1
+ import { info } from "@gtkx/utils";
2
+ import { findStaleHeadlessDisplays, reapStaleHeadlessDisplays } from "@gtkx/vitest/headless";
3
+ import { defineCommand } from "citty";
4
+ import { cwdArg } from "../internal/entry-arg.js";
5
+
6
+ const cleanup = defineCommand({
7
+ meta: {
8
+ name: "cleanup",
9
+ description: "Remove stale GTKX headless runtime directories",
10
+ },
11
+ args: {
12
+ "dry-run": {
13
+ type: "boolean",
14
+ description: "List stale headless runtime directories without removing them",
15
+ },
16
+ ...cwdArg,
17
+ },
18
+ run({ args }) {
19
+ const candidates = findStaleHeadlessDisplays();
20
+
21
+ for (const candidate of candidates) {
22
+ info(`cleanup: ${candidate.runtimeDir}`);
23
+ }
24
+
25
+ if (args["dry-run"] === true) {
26
+ info(`cleanup: found ${String(candidates.length)} stale headless runtime directories`);
27
+
28
+ return;
29
+ }
30
+
31
+ const removed = reapStaleHeadlessDisplays(candidates);
32
+ info(`cleanup: removed ${String(removed.length)} stale headless runtime directories`);
33
+ },
34
+ });
35
+
36
+ export { cleanup };
@@ -1,4 +1,5 @@
1
1
  import { armParentDeath } from "@gtkx/native/internal";
2
+ import { reapStaleHeadlessDisplays } from "@gtkx/vitest/headless";
2
3
  import { defineCommand } from "citty";
3
4
  import { resolveConfigWatch } from "../codegen/run-codegen.js";
4
5
  import { startHeadlessDevDisplay } from "../dev/headless.js";
@@ -28,6 +29,7 @@ const dev = defineCommand({
28
29
  },
29
30
  },
30
31
  async run({ args }) {
32
+ reapStaleHeadlessDisplays();
31
33
  const initialProcessGroupOwner = getInitialProcessGroupOwner();
32
34
 
33
35
  if (
@@ -44,8 +46,13 @@ const dev = defineCommand({
44
46
  throw new Error("--size requires --headless");
45
47
  }
46
48
 
47
- const { cwd, entry: entryPath, configFile } = await prepareProject(args, DEV_MODE);
48
- const watch: DevWatch | undefined = await resolveConfigWatch(cwd, DEV_MODE, configFile);
49
+ const { cwd, entry: entryPath, configFile, configDependencies } = await prepareProject(args, DEV_MODE);
50
+ const watch: DevWatch | undefined = await resolveConfigWatch(
51
+ cwd,
52
+ DEV_MODE,
53
+ configFile,
54
+ configDependencies,
55
+ );
49
56
  const { applicationArgs } = splitApplicationArgs(process.argv.slice(2));
50
57
  const stopHeadless = args.headless ? await startHeadlessDevDisplay(args.size) : undefined;
51
58
 
@@ -35,6 +35,7 @@ const REMEDY_FOR_RULE: Record<string, string> = {
35
35
  "metainfo-legacy-path": "this file is generated; report it as a gtkx bug",
36
36
  "summary-has-dot-suffix": "drop the trailing period from `deploy.summary`",
37
37
  "summary-too-long": "shorten `deploy.summary`",
38
+ "unknown-tag": "remove the unsupported `deploy.metainfoExtra` element or use a `<custom>` value",
38
39
  "url-homepage-missing": "set `deploy.homepage`, or `homepage` in package.json",
39
40
  };
40
41