@gtkx/cli 2.0.0-beta.1 → 2.0.0-beta.2

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 (165) hide show
  1. package/dist/builder.d.ts +1 -0
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +17 -5
  4. package/dist/builder.js.map +1 -1
  5. package/dist/cli.js +2 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/codegen/run-codegen.d.ts +4 -2
  8. package/dist/codegen/run-codegen.d.ts.map +1 -1
  9. package/dist/codegen/run-codegen.js +13 -8
  10. package/dist/codegen/run-codegen.js.map +1 -1
  11. package/dist/codegen/store-resolver.d.ts +1 -1
  12. package/dist/codegen/store-resolver.d.ts.map +1 -1
  13. package/dist/codegen/store-resolver.js +2 -2
  14. package/dist/codegen/store-resolver.js.map +1 -1
  15. package/dist/commands/build.d.ts +5 -0
  16. package/dist/commands/build.d.ts.map +1 -1
  17. package/dist/commands/build.js +83 -10
  18. package/dist/commands/build.js.map +1 -1
  19. package/dist/commands/codegen.d.ts +1 -0
  20. package/dist/commands/codegen.d.ts.map +1 -1
  21. package/dist/commands/codegen.js +6 -5
  22. package/dist/commands/codegen.js.map +1 -1
  23. package/dist/commands/deploy.d.ts +1 -0
  24. package/dist/commands/deploy.d.ts.map +1 -1
  25. package/dist/commands/deploy.js +4 -2
  26. package/dist/commands/deploy.js.map +1 -1
  27. package/dist/commands/dev.d.ts +8 -0
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +19 -1
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/deploy/freedesktop/metainfo.d.ts.map +1 -1
  32. package/dist/deploy/freedesktop/metainfo.js +41 -2
  33. package/dist/deploy/freedesktop/metainfo.js.map +1 -1
  34. package/dist/deploy/nfpm/config.d.ts.map +1 -1
  35. package/dist/deploy/nfpm/config.js +5 -2
  36. package/dist/deploy/nfpm/config.js.map +1 -1
  37. package/dist/deploy/nfpm/contents.d.ts.map +1 -1
  38. package/dist/deploy/nfpm/contents.js +8 -0
  39. package/dist/deploy/nfpm/contents.js.map +1 -1
  40. package/dist/deploy/node-runtime/elf.d.ts +2 -1
  41. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  42. package/dist/deploy/node-runtime/elf.js +32 -5
  43. package/dist/deploy/node-runtime/elf.js.map +1 -1
  44. package/dist/deploy/node-runtime/index.d.ts +2 -1
  45. package/dist/deploy/node-runtime/index.d.ts.map +1 -1
  46. package/dist/deploy/node-runtime/index.js +73 -4
  47. package/dist/deploy/node-runtime/index.js.map +1 -1
  48. package/dist/deploy/notices/node-runtime.js +2 -2
  49. package/dist/deploy/notices/node-runtime.js.map +1 -1
  50. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  51. package/dist/deploy/payload/launcher.js +14 -1
  52. package/dist/deploy/payload/launcher.js.map +1 -1
  53. package/dist/deploy/payload/stage.js +1 -1
  54. package/dist/deploy/payload/stage.js.map +1 -1
  55. package/dist/deploy/run-deploy.d.ts +1 -0
  56. package/dist/deploy/run-deploy.d.ts.map +1 -1
  57. package/dist/deploy/run-deploy.js +100 -14
  58. package/dist/deploy/run-deploy.js.map +1 -1
  59. package/dist/deploy/settings/build-manifest.d.ts +6 -1
  60. package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
  61. package/dist/deploy/settings/build-manifest.js +15 -4
  62. package/dist/deploy/settings/build-manifest.js.map +1 -1
  63. package/dist/deploy/settings/extra-files.d.ts +1 -1
  64. package/dist/deploy/settings/extra-files.d.ts.map +1 -1
  65. package/dist/deploy/settings/extra-files.js +22 -1
  66. package/dist/deploy/settings/extra-files.js.map +1 -1
  67. package/dist/deploy/settings/index.d.ts +1 -0
  68. package/dist/deploy/settings/index.d.ts.map +1 -1
  69. package/dist/deploy/settings/index.js +2 -1
  70. package/dist/deploy/settings/index.js.map +1 -1
  71. package/dist/deploy/settings/paths.d.ts +2 -1
  72. package/dist/deploy/settings/paths.d.ts.map +1 -1
  73. package/dist/deploy/settings/paths.js +113 -5
  74. package/dist/deploy/settings/paths.js.map +1 -1
  75. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  76. package/dist/deploy/targets/flatpak-source.js +11 -2
  77. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  78. package/dist/deploy/types.d.ts +1 -0
  79. package/dist/deploy/types.d.ts.map +1 -1
  80. package/dist/deploy/types.js.map +1 -1
  81. package/dist/dev/headless.d.ts +3 -0
  82. package/dist/dev/headless.d.ts.map +1 -0
  83. package/dist/dev/headless.js +35 -0
  84. package/dist/dev/headless.js.map +1 -0
  85. package/dist/dev/runner-deps.js +1 -1
  86. package/dist/dev/runner-deps.js.map +1 -1
  87. package/dist/i18n/source-messages.d.ts.map +1 -1
  88. package/dist/i18n/source-messages.js +835 -1
  89. package/dist/i18n/source-messages.js.map +1 -1
  90. package/dist/internal/build-manifest.d.ts +5 -2
  91. package/dist/internal/build-manifest.d.ts.map +1 -1
  92. package/dist/internal/build-manifest.js +15 -2
  93. package/dist/internal/build-manifest.js.map +1 -1
  94. package/dist/internal/build-output.d.ts +8 -0
  95. package/dist/internal/build-output.d.ts.map +1 -0
  96. package/dist/internal/build-output.js +69 -0
  97. package/dist/internal/build-output.js.map +1 -0
  98. package/dist/internal/entry-arg.d.ts +4 -1
  99. package/dist/internal/entry-arg.d.ts.map +1 -1
  100. package/dist/internal/entry-arg.js +7 -1
  101. package/dist/internal/entry-arg.js.map +1 -1
  102. package/dist/internal/output-directory.d.ts +15 -0
  103. package/dist/internal/output-directory.d.ts.map +1 -0
  104. package/dist/internal/output-directory.js +490 -0
  105. package/dist/internal/output-directory.js.map +1 -0
  106. package/dist/internal/prepare-project.d.ts +2 -0
  107. package/dist/internal/prepare-project.d.ts.map +1 -1
  108. package/dist/internal/prepare-project.js +3 -3
  109. package/dist/internal/prepare-project.js.map +1 -1
  110. package/dist/internal/source-imports.js +1 -1
  111. package/dist/internal/source-imports.js.map +1 -1
  112. package/dist/mcp/client.js +2 -2
  113. package/dist/mcp/client.js.map +1 -1
  114. package/dist/vite-plugins/build-manifest.d.ts +5 -1
  115. package/dist/vite-plugins/build-manifest.d.ts.map +1 -1
  116. package/dist/vite-plugins/build-manifest.js +2 -1
  117. package/dist/vite-plugins/build-manifest.js.map +1 -1
  118. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  119. package/dist/vite-plugins/i18n.js +58 -6
  120. package/dist/vite-plugins/i18n.js.map +1 -1
  121. package/dist/vite-plugins/index.d.ts +8 -1
  122. package/dist/vite-plugins/index.d.ts.map +1 -1
  123. package/dist/vite-plugins/index.js +6 -2
  124. package/dist/vite-plugins/index.js.map +1 -1
  125. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  126. package/dist/vite-plugins/self-contained.js +159 -158
  127. package/dist/vite-plugins/self-contained.js.map +1 -1
  128. package/package.json +9 -9
  129. package/src/builder.ts +18 -5
  130. package/src/cli.ts +3 -0
  131. package/src/codegen/run-codegen.ts +15 -7
  132. package/src/codegen/store-resolver.ts +2 -2
  133. package/src/commands/build.ts +13 -2
  134. package/src/commands/codegen.ts +6 -5
  135. package/src/commands/deploy.ts +4 -2
  136. package/src/commands/dev.ts +20 -1
  137. package/src/deploy/freedesktop/metainfo.ts +73 -21
  138. package/src/deploy/nfpm/config.ts +5 -2
  139. package/src/deploy/nfpm/contents.ts +8 -0
  140. package/src/deploy/node-runtime/elf.ts +43 -5
  141. package/src/deploy/node-runtime/index.ts +105 -5
  142. package/src/deploy/notices/node-runtime.ts +2 -2
  143. package/src/deploy/payload/launcher.ts +20 -1
  144. package/src/deploy/payload/stage.ts +1 -1
  145. package/src/deploy/run-deploy.ts +56 -16
  146. package/src/deploy/settings/build-manifest.ts +28 -3
  147. package/src/deploy/settings/extra-files.ts +26 -2
  148. package/src/deploy/settings/index.ts +3 -1
  149. package/src/deploy/settings/paths.ts +70 -5
  150. package/src/deploy/targets/flatpak-source.ts +13 -2
  151. package/src/deploy/types.ts +1 -0
  152. package/src/dev/headless.ts +40 -0
  153. package/src/dev/runner-deps.ts +1 -1
  154. package/src/i18n/source-messages.ts +1220 -2
  155. package/src/internal/build-manifest.ts +25 -1
  156. package/src/internal/build-output.ts +99 -0
  157. package/src/internal/entry-arg.ts +8 -1
  158. package/src/internal/output-directory.ts +699 -0
  159. package/src/internal/prepare-project.ts +5 -5
  160. package/src/internal/source-imports.ts +1 -1
  161. package/src/mcp/client.ts +2 -2
  162. package/src/vite-plugins/build-manifest.ts +7 -1
  163. package/src/vite-plugins/i18n.ts +86 -5
  164. package/src/vite-plugins/index.ts +14 -7
  165. package/src/vite-plugins/self-contained.ts +184 -185
@@ -1,4 +1,4 @@
1
- import { loadConfig } from "@gtkx/config";
1
+ import { type Config, loadConfig } from "@gtkx/config";
2
2
  import { info, warn } from "@gtkx/utils";
3
3
  import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative } from "node:path";
@@ -22,6 +22,7 @@ import {
22
22
  synchronizeCatalogs,
23
23
  } from "../i18n/catalogs.js";
24
24
  import { extractSourceCatalogTo } from "../i18n/source-messages.js";
25
+ import { prepareBuildOutDir, resolveBuildOutDir } from "../internal/build-output.js";
25
26
  import { discoverSourceFiles } from "../internal/source-imports.js";
26
27
  import { renderDesktopEntry } from "./freedesktop/desktop-entry.js";
27
28
  import { extractMetadataMessages, localizeMetadata } from "./freedesktop/localize.js";
@@ -35,6 +36,7 @@ import { DEFAULT_TARGETS, parseTargetList, targetsFor } from "./registry.js";
35
36
  import { readBuildManifest } from "./settings/build-manifest.js";
36
37
  import { resolveDeploySettings } from "./settings/index.js";
37
38
  import { readPackageManifest } from "./settings/package-manifest.js";
39
+ import { prepareDeployOutDir } from "./settings/paths.js";
38
40
  import { missingDeployError } from "./settings/starter.js";
39
41
  import { finishArgsFor, hasDisplaySocket, runtimeLabelFor } from "./targets/flatpak-manifest.js";
40
42
  import { detectPackageManager } from "./targets/flatpak-sources.js";
@@ -58,6 +60,7 @@ import {
58
60
  type DeployOptions = {
59
61
  entry: string;
60
62
  cwd: string;
63
+ configFile?: string | undefined;
61
64
  targets?: string | undefined;
62
65
  outDir?: string | undefined;
63
66
  shouldPrintManifests: boolean;
@@ -72,6 +75,20 @@ type PreflightRequest = {
72
75
  shouldSkipBuild: boolean;
73
76
  };
74
77
 
78
+ type LoadedDeploySettings = {
79
+ config: Config;
80
+ configFile: string;
81
+ settings: DeploySettings;
82
+ };
83
+
84
+ type BuildPayloadRequest = {
85
+ options: DeployOptions;
86
+ loaded: LoadedDeploySettings;
87
+ targets: DeployTarget[];
88
+ project: CatalogProject | null;
89
+ buildOutDir: string | null;
90
+ };
91
+
75
92
  const BUILD_MODE = "production";
76
93
  const BYTES_PER_MIB = 1024 * 1024;
77
94
  const WEBKIT_LIBRARY = "WebKit-6.0";
@@ -282,14 +299,26 @@ const resolveTargetNames = (options: DeployOptions, settings: DeploySettings): s
282
299
  return settings.deploy.targets ?? DEFAULT_TARGETS;
283
300
  };
284
301
 
285
- const loadSettings = async (options: DeployOptions): Promise<DeploySettings> => {
286
- const { config, root } = await loadConfig(options.cwd, { mode: BUILD_MODE });
302
+ const loadSettings = async (options: DeployOptions): Promise<LoadedDeploySettings> => {
303
+ const { config, configFile, root } = await loadConfig(options.cwd, {
304
+ mode: BUILD_MODE,
305
+ configFile: options.configFile,
306
+ });
287
307
 
288
308
  if (config.deploy === undefined) {
289
309
  throw missingDeployError(config.applicationId, readPackageManifest(root));
290
310
  }
291
311
 
292
- return resolveDeploySettings({ root, config, outDirOverride: options.outDir });
312
+ return {
313
+ config,
314
+ configFile,
315
+ settings: resolveDeploySettings({
316
+ root,
317
+ config,
318
+ configFile,
319
+ outDirOverride: options.outDir,
320
+ }),
321
+ };
293
322
  };
294
323
 
295
324
  const synchronizeMetadataCatalogs = async (
@@ -318,22 +347,27 @@ const synchronizeMetadataCatalogs = async (
318
347
  }
319
348
  };
320
349
 
321
- const buildPayload = async (
322
- options: DeployOptions,
323
- settings: DeploySettings,
324
- targets: DeployTarget[],
325
- project: CatalogProject | null,
326
- ): Promise<DeployPayload> => {
350
+ const buildPayload = async ({
351
+ options,
352
+ loaded,
353
+ targets,
354
+ project,
355
+ buildOutDir,
356
+ }: BuildPayloadRequest): Promise<DeployPayload> => {
357
+ const settings = loaded.settings;
327
358
  const templates = renderMetadata(settings);
328
359
 
329
- if (!options.shouldSkipBuild) {
360
+ if (buildOutDir !== null) {
330
361
  info(`Building ${options.entry}`);
362
+ using preparedOutput = prepareBuildOutDir(settings.paths.root, buildOutDir);
331
363
 
332
364
  await buildApp({
333
365
  entry: options.entry,
334
- vite: { root: options.cwd },
366
+ configFile: options.configFile,
367
+ vite: { root: options.cwd, build: { outDir: preparedOutput.path, emptyOutDir: false } },
335
368
  });
336
369
 
370
+ preparedOutput.commit();
337
371
  await synchronizeMetadataCatalogs(settings, templates, project);
338
372
  } else if (project !== null) {
339
373
  compileCatalogs(project, join(settings.paths.dist, LOCALE_DIRNAME));
@@ -341,7 +375,7 @@ const buildPayload = async (
341
375
 
342
376
  const metadata = localizeMetadata(templates, project);
343
377
  validateMetadata(settings, metadata, isFlathubSubmission(settings, targets));
344
- const buildManifest = readBuildManifest(settings);
378
+ const buildManifest = readBuildManifest(settings, loaded);
345
379
 
346
380
  const builtSettings: DeploySettings = {
347
381
  ...settings,
@@ -411,9 +445,11 @@ const announce = (settings: DeploySettings, targets: DeployTarget[]): void => {
411
445
  };
412
446
 
413
447
  const runDeploy = async (options: DeployOptions): Promise<void> => {
414
- let settings = await loadSettings(options);
448
+ let loaded = await loadSettings(options);
449
+ let settings = loaded.settings;
415
450
  let project = resolveCatalogProject(settings.paths.root, settings.applicationId);
416
451
  let targets = targetsFor(resolveTargetNames(options, settings));
452
+ let buildOutDir = options.shouldSkipBuild ? null : resolveBuildOutDir(settings.paths.root, settings.paths.dist);
417
453
  announce(settings, targets);
418
454
 
419
455
  preflight({
@@ -428,14 +464,18 @@ const runDeploy = async (options: DeployOptions): Promise<void> => {
428
464
  await ensureGenerated(options.cwd, {
429
465
  shouldAnnounce: true,
430
466
  mode: BUILD_MODE,
467
+ configFile: options.configFile,
431
468
  });
432
469
 
433
- settings = await loadSettings(options);
470
+ loaded = await loadSettings(options);
471
+ settings = loaded.settings;
434
472
  project = resolveCatalogProject(settings.paths.root, settings.applicationId);
435
473
  targets = targetsFor(resolveTargetNames(options, settings));
474
+ buildOutDir = resolveBuildOutDir(settings.paths.root, settings.paths.dist);
436
475
  }
437
476
 
438
- const payload = await buildPayload(options, settings, targets, project);
477
+ prepareDeployOutDir(settings.paths.root, settings.paths.outDir);
478
+ const payload = await buildPayload({ options, loaded, targets, project, buildOutDir });
439
479
  const rendered = renderTargetManifests(targets, payload);
440
480
 
441
481
  if (options.shouldPrintManifests) {
@@ -1,12 +1,14 @@
1
- import { isPathInside, isRecord, sortStrings } from "@gtkx/utils";
1
+ import type { Config } from "@gtkx/config";
2
+ import { isPathInside, isRecord, sortStrings, toPosixPath } from "@gtkx/utils";
2
3
  import { existsSync, readFileSync, statSync } from "node:fs";
3
- import { isAbsolute, join, resolve } from "node:path";
4
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
5
  import type { DeploySettings } from "../types.js";
5
6
  import {
6
7
  BUILD_MANIFEST_FILENAME,
7
8
  BUILD_MANIFEST_FORMAT_VERSION,
8
9
  BUILD_MANIFEST_GENERATOR,
9
10
  type BuildManifest,
11
+ configDigest,
10
12
  type RecordedPackage,
11
13
  } from "../../internal/build-manifest.js";
12
14
  import {
@@ -20,6 +22,11 @@ type ResolvedBuildManifest = {
20
22
  schemaFiles: string[];
21
23
  };
22
24
 
25
+ type ExpectedBuildConfig = {
26
+ config: Config;
27
+ configFile: string;
28
+ };
29
+
23
30
  const invalidManifest = (path: string): Error =>
24
31
  new Error(`Cannot deploy: ${path} is not a valid GTKX build manifest. Run \`gtkx build\` again.`);
25
32
 
@@ -51,6 +58,8 @@ const parseBuildManifest = (value: unknown, path: string): BuildManifest => {
51
58
  !isRecord(value) ||
52
59
  value.generator !== BUILD_MANIFEST_GENERATOR ||
53
60
  value.formatVersion !== BUILD_MANIFEST_FORMAT_VERSION ||
61
+ typeof value.configFile !== "string" ||
62
+ typeof value.configDigest !== "string" ||
54
63
  !Array.isArray(value.schemas) ||
55
64
  !Array.isArray(value.packages)
56
65
  ) {
@@ -62,6 +71,8 @@ const parseBuildManifest = (value: unknown, path: string): BuildManifest => {
62
71
  return {
63
72
  generator: BUILD_MANIFEST_GENERATOR,
64
73
  formatVersion: BUILD_MANIFEST_FORMAT_VERSION,
74
+ configFile: value.configFile,
75
+ configDigest: value.configDigest,
65
76
  schemas: recordedSchemas(value.schemas, path),
66
77
  packages: packages.map((entry) => recordedPackage(entry, path)),
67
78
  };
@@ -91,7 +102,20 @@ const resolveRecordedSchema = (root: string, manifestPath: string, entry: string
91
102
  return filePath;
92
103
  };
93
104
 
94
- const readBuildManifest = (settings: DeploySettings): ResolvedBuildManifest => {
105
+ const assertBuildConfig = (
106
+ settings: DeploySettings,
107
+ manifestPath: string,
108
+ manifest: BuildManifest,
109
+ expected: ExpectedBuildConfig,
110
+ ): void => {
111
+ const expectedFile = toPosixPath(relative(settings.paths.root, expected.configFile));
112
+
113
+ if (manifest.configFile !== expectedFile || manifest.configDigest !== configDigest(expected.config)) {
114
+ throw new Error(`Cannot deploy: ${manifestPath} was built from a different GTKX configuration.`);
115
+ }
116
+ };
117
+
118
+ const readBuildManifest = (settings: DeploySettings, expected: ExpectedBuildConfig): ResolvedBuildManifest => {
95
119
  const manifestPath = join(settings.paths.dist, BUILD_MANIFEST_FILENAME);
96
120
 
97
121
  if (!existsSync(manifestPath)) {
@@ -100,6 +124,7 @@ const readBuildManifest = (settings: DeploySettings): ResolvedBuildManifest => {
100
124
 
101
125
  const parsed: unknown = JSON.parse(readFileSync(manifestPath, "utf8"));
102
126
  const manifest = parseBuildManifest(parsed, manifestPath);
127
+ assertBuildConfig(settings, manifestPath, manifest, expected);
103
128
  const files = manifest.schemas.map((entry) => resolveRecordedSchema(settings.paths.root, manifestPath, entry));
104
129
  const unique = sortStrings(new Set(files));
105
130
  assertUniqueSchemaBasenames(unique);
@@ -1,3 +1,5 @@
1
+ import { statSync } from "node:fs";
2
+ import { resolve } from "node:path";
1
3
  import type { DeployConfig, DeployExtraFile } from "../types.js";
2
4
 
3
5
  type ExtraFiles = NonNullable<DeployConfig["extraFiles"]>;
@@ -5,6 +7,20 @@ type ExtraFileEntry = NonNullable<ExtraFiles[string]>;
5
7
 
6
8
  const OCTAL_RADIX = 8;
7
9
 
10
+ const assertSourceFile = (root: string, file: DeployExtraFile): void => {
11
+ const source = resolve(root, file.source);
12
+
13
+ try {
14
+ if (statSync(source).isFile()) {
15
+ return;
16
+ }
17
+ } catch {
18
+ throw new Error(`Cannot install "${file.destination}": its source file "${file.source}" does not exist`);
19
+ }
20
+
21
+ throw new Error(`Cannot install "${file.destination}": its source "${file.source}" is not a file`);
22
+ };
23
+
8
24
  const resolveExtraFile = (destination: string, entry: ExtraFileEntry): DeployExtraFile => {
9
25
  if (typeof entry === "string") {
10
26
  return { destination, source: entry, mode: null };
@@ -15,7 +31,15 @@ const resolveExtraFile = (destination: string, entry: ExtraFileEntry): DeployExt
15
31
  return { destination, source: entry.source, mode };
16
32
  };
17
33
 
18
- const resolveExtraFiles = (deploy: DeployConfig): DeployExtraFile[] =>
19
- Object.entries(deploy.extraFiles ?? {}).map(([destination, entry]) => resolveExtraFile(destination, entry));
34
+ const resolveExtraFiles = (root: string, deploy: DeployConfig): DeployExtraFile[] => {
35
+ const files = Object.entries(deploy.extraFiles ?? {})
36
+ .map(([destination, entry]) => resolveExtraFile(destination, entry));
37
+
38
+ for (const file of files) {
39
+ assertSourceFile(root, file);
40
+ }
41
+
42
+ return files;
43
+ };
20
44
 
21
45
  export { resolveExtraFiles };
@@ -31,6 +31,7 @@ import { resolveVersions } from "./version.js";
31
31
  type SettingsRequest = {
32
32
  root: string;
33
33
  config: Config;
34
+ configFile: string;
34
35
  outDirOverride?: string | undefined;
35
36
  now?: Date | undefined;
36
37
  };
@@ -174,10 +175,11 @@ const resolveDeploySettings = (request: SettingsRequest): DeploySettings => {
174
175
  const deploy = core.identity.deploy;
175
176
 
176
177
  return {
178
+ configFile: request.configFile,
177
179
  ...identitySlice(core),
178
180
  ...metadataSlice(request.root, core),
179
181
  ...desktopSlice(deploy),
180
- extraFiles: resolveExtraFiles(deploy),
182
+ extraFiles: resolveExtraFiles(request.root, deploy),
181
183
  versions: core.versions,
182
184
  arch: resolveArch(),
183
185
  paths: resolvePaths({
@@ -1,9 +1,10 @@
1
1
  import type { Config } from "@gtkx/config";
2
2
  import { isPathInside } from "@gtkx/utils";
3
- import { statSync } from "node:fs";
4
- import { join, resolve } from "node:path";
3
+ import { lstatSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { join, relative, resolve, sep } from "node:path";
5
5
  import type { DeployConfig, DeployPaths } from "../types.js";
6
6
  import { resolveApplicationIcon } from "../../internal/icon-path.js";
7
+ import { prepareOutputDirectory, readRegularFile } from "../../internal/output-directory.js";
7
8
 
8
9
  type PathsRequest = {
9
10
  root: string;
@@ -15,19 +16,83 @@ type PathsRequest = {
15
16
 
16
17
  const DEFAULT_OUT_DIR = "build";
17
18
  const DIST_DIR = "dist";
19
+ const DEPLOY_MARKER_FILENAME = ".gtkx-deploy.json";
20
+ const DEPLOY_MARKER = `${JSON.stringify({ generator: "gtkx-deploy", formatVersion: 1 })}\n`;
18
21
  const LICENSE_CANDIDATES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "COPYING.md"];
19
22
 
23
+ const hasSymlinkComponent = (root: string, target: string): boolean => {
24
+ let current = root;
25
+
26
+ for (const segment of relative(root, target).split(sep)) {
27
+ current = join(current, segment);
28
+
29
+ if (lstatSync(current, { throwIfNoEntry: false })?.isSymbolicLink() === true) {
30
+ return true;
31
+ }
32
+ }
33
+
34
+ return false;
35
+ };
36
+
37
+ const isDeployMarker = (path: string): boolean => {
38
+ return readRegularFile(path) === DEPLOY_MARKER;
39
+ };
40
+
41
+ const isReusableDeployDirectory = (path: string): boolean => {
42
+ const entry = lstatSync(path, { throwIfNoEntry: false });
43
+
44
+ if (entry === undefined) {
45
+ return true;
46
+ }
47
+
48
+ return entry.isDirectory() &&
49
+ (readdirSync(path).length === 0 || isDeployMarker(join(path, DEPLOY_MARKER_FILENAME)));
50
+ };
51
+
52
+ const deployOutputError = (root: string, configured: string): Error =>
53
+ new Error(
54
+ `Cannot use "${configured}" as the deploy output directory: choose an empty directory or an ` +
55
+ `earlier GTKX deploy directory below ${root}`,
56
+ );
57
+
58
+ const assertSafeDeployOutDir = (root: string, outDir: string, configured: string): void => {
59
+ const dist = join(root, DIST_DIR);
60
+
61
+ if (
62
+ outDir === dist ||
63
+ !isPathInside(root, outDir) ||
64
+ isPathInside(dist, outDir) ||
65
+ hasSymlinkComponent(root, outDir)
66
+ ) {
67
+ throw deployOutputError(root, configured);
68
+ }
69
+ };
70
+
20
71
  const resolveOutDir = ({ root, deploy, outDirOverride }: PathsRequest): string => {
21
72
  const configured = outDirOverride ?? deploy.outDir ?? DEFAULT_OUT_DIR;
22
73
  const outDir = resolve(root, configured);
74
+ assertSafeDeployOutDir(root, outDir, configured);
23
75
 
24
- if (!isPathInside(root, outDir)) {
25
- throw new Error(`Cannot use "${configured}" as the deploy output directory: it is outside ${root}`);
76
+ if (!isReusableDeployDirectory(outDir)) {
77
+ throw deployOutputError(root, configured);
26
78
  }
27
79
 
28
80
  return outDir;
29
81
  };
30
82
 
83
+ const prepareDeployOutDir = (root: string, outDir: string): void => {
84
+ assertSafeDeployOutDir(root, outDir, outDir);
85
+ const prepared = prepareOutputDirectory(root, outDir, isReusableDeployDirectory);
86
+
87
+ if (prepared.status === "unsafe") {
88
+ throw deployOutputError(root, outDir);
89
+ }
90
+
91
+ using transaction = prepared.transaction;
92
+ transaction.commit();
93
+ writeFileSync(join(outDir, DEPLOY_MARKER_FILENAME), DEPLOY_MARKER, { flag: "wx" });
94
+ };
95
+
31
96
  const existingFile = (path: string): string | null =>
32
97
  statSync(path, { throwIfNoEntry: false })?.isFile() === true ? path : null;
33
98
 
@@ -65,4 +130,4 @@ const resolvePaths = (request: PathsRequest): DeployPaths => {
65
130
  };
66
131
  };
67
132
 
68
- export { resolvePaths };
133
+ export { prepareDeployOutDir, resolvePaths };
@@ -244,6 +244,17 @@ const appendPathFor = (settings: DeploySettings, pin: PnpmPin | null, nodeExtens
244
244
  ? `${nodeExtensionPath}/bin`
245
245
  : `${pnpmPathFor(moduleDirFor(settings))}:${nodeExtensionPath}/bin`;
246
246
 
247
+ const sourceBuildCommand = (settings: DeploySettings): string => {
248
+ const configFile = projectRelative(settings, settings.configFile);
249
+ assertInsideProject(settings, {
250
+ destination: "the Flatpak build configuration",
251
+ source: configFile,
252
+ resolved: settings.configFile,
253
+ });
254
+
255
+ return `npx gtkx build --config ${pathArgument(configFile)}`;
256
+ };
257
+
247
258
  const flatpakSourceModule = (payload: DeployPayload): FlatpakModule => {
248
259
  const settings = payload.settings;
249
260
  const manager = detectPackageManager(settings);
@@ -271,14 +282,14 @@ const flatpakSourceModule = (payload: DeployPayload): FlatpakModule => {
271
282
  ],
272
283
  "build-commands": [
273
284
  installCommandFor(settings, manager),
274
- "npx gtkx build",
285
+ sourceBuildCommand(settings),
275
286
  ...runtimeInstallCommands(settings, nodeExtensionPath),
287
+ ...extraFileInstallCommands(settings),
276
288
  ...metadataInstallCommands(settings),
277
289
  ...schemaInstallCommands(settings),
278
290
  ...iconInstallCommands(settings),
279
291
  ...licenseInstallCommands(settings),
280
292
  ...noticesInstallCommands(settings),
281
- ...extraFileInstallCommands(settings),
282
293
  ...(settings.deploy.flatpak?.buildCommands ?? []),
283
294
  ...validationCommands(settings),
284
295
  ],
@@ -114,6 +114,7 @@ type DeployPaths = {
114
114
 
115
115
  type DeploySettings = {
116
116
  applicationId: string;
117
+ configFile: string;
117
118
  binaryName: string;
118
119
  name: string;
119
120
  genericName: string | null;
@@ -0,0 +1,40 @@
1
+ import { MCP_SOCKET_PATH_ENV, resolveMcpSocketPath } from "@gtkx/mcp/internal";
2
+ import { resolveHeadlessOptions, startHeadlessDisplay } from "@gtkx/vitest/headless";
3
+
4
+ const restoreEnvironment = (name: string, value: string | undefined): void => {
5
+ if (value === undefined) {
6
+ Reflect.deleteProperty(process.env, name);
7
+ } else {
8
+ process.env[name] = value;
9
+ }
10
+ };
11
+
12
+ const startHeadlessDevDisplay = async (size?: string): Promise<() => void> => {
13
+ const previousSocketPath = process.env[MCP_SOCKET_PATH_ENV];
14
+ process.env[MCP_SOCKET_PATH_ENV] = resolveMcpSocketPath();
15
+
16
+ try {
17
+ const stopDisplay = await startHeadlessDisplay(resolveHeadlessOptions({ ...(size !== undefined && { size }) }));
18
+ let isStopped = false;
19
+
20
+ const stop = (): void => {
21
+ if (isStopped) {
22
+ return;
23
+ }
24
+
25
+ isStopped = true;
26
+ process.removeListener("exit", stop);
27
+ stopDisplay();
28
+ restoreEnvironment(MCP_SOCKET_PATH_ENV, previousSocketPath);
29
+ };
30
+
31
+ process.once("exit", stop);
32
+
33
+ return stop;
34
+ } catch (error) {
35
+ restoreEnvironment(MCP_SOCKET_PATH_ENV, previousSocketPath);
36
+ throw error;
37
+ }
38
+ };
39
+
40
+ export { startHeadlessDevDisplay };
@@ -87,7 +87,7 @@ const defaultDevRunnerDeps = (): DevRunnerDeps => ({
87
87
  staleExportName,
88
88
  readFileRevision,
89
89
  plugins: (entryPath) => [
90
- ...gtkxVitePlugins(DEV_MODE, entryPath),
90
+ ...gtkxVitePlugins({ mode: DEV_MODE, entryPath }),
91
91
  ...gtkxFastRefresh(),
92
92
  gtkxReactDomPrebundle(),
93
93
  ],