@gtkx/config 2.0.0-beta.1 → 2.0.0-beta.11

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 (70) hide show
  1. package/README.md +11 -166
  2. package/dist/config-dependencies.d.ts +11 -0
  3. package/dist/config-dependencies.d.ts.map +1 -0
  4. package/dist/config-dependencies.js +84 -0
  5. package/dist/config-dependencies.js.map +1 -0
  6. package/dist/config-error.d.ts +2 -2
  7. package/dist/config-error.d.ts.map +1 -1
  8. package/dist/config-error.js +6 -3
  9. package/dist/config-error.js.map +1 -1
  10. package/dist/config-reloader.d.ts +8 -0
  11. package/dist/config-reloader.d.ts.map +1 -0
  12. package/dist/config-reloader.js +38 -0
  13. package/dist/config-reloader.js.map +1 -0
  14. package/dist/config-source.d.ts +7 -0
  15. package/dist/config-source.d.ts.map +1 -0
  16. package/dist/config-source.js +17 -0
  17. package/dist/config-source.js.map +1 -0
  18. package/dist/config.d.ts +42 -38
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +21 -15
  21. package/dist/config.js.map +1 -1
  22. package/dist/deploy.d.ts +28 -22
  23. package/dist/deploy.d.ts.map +1 -1
  24. package/dist/deploy.js +29 -5
  25. package/dist/deploy.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/internal.d.ts +6 -1
  31. package/dist/internal.d.ts.map +1 -1
  32. package/dist/internal.js +6 -1
  33. package/dist/internal.js.map +1 -1
  34. package/dist/loader.d.ts +2 -2
  35. package/dist/loader.d.ts.map +1 -1
  36. package/dist/loader.js +66 -15
  37. package/dist/loader.js.map +1 -1
  38. package/dist/node-version.d.ts +5 -0
  39. package/dist/node-version.d.ts.map +1 -0
  40. package/dist/node-version.js +13 -0
  41. package/dist/node-version.js.map +1 -0
  42. package/dist/static-dependencies.d.ts +3 -0
  43. package/dist/static-dependencies.d.ts.map +1 -0
  44. package/dist/static-dependencies.js +350 -0
  45. package/dist/static-dependencies.js.map +1 -0
  46. package/dist/virtual.d.ts.map +1 -1
  47. package/dist/virtual.js +0 -2
  48. package/dist/virtual.js.map +1 -1
  49. package/dist/vite-plugin.d.ts.map +1 -1
  50. package/dist/vite-plugin.js +14 -5
  51. package/dist/vite-plugin.js.map +1 -1
  52. package/dist/vite-root.d.ts +9 -0
  53. package/dist/vite-root.d.ts.map +1 -0
  54. package/dist/vite-root.js +3 -0
  55. package/dist/vite-root.js.map +1 -0
  56. package/package.json +23 -6
  57. package/src/config-dependencies.ts +116 -0
  58. package/src/config-error.ts +9 -4
  59. package/src/config-reloader.ts +54 -0
  60. package/src/config-source.ts +25 -0
  61. package/src/config.ts +27 -19
  62. package/src/deploy.ts +61 -8
  63. package/src/index.ts +1 -0
  64. package/src/internal.ts +6 -0
  65. package/src/loader.ts +92 -28
  66. package/src/node-version.ts +17 -0
  67. package/src/static-dependencies.ts +464 -0
  68. package/src/virtual.ts +0 -2
  69. package/src/vite-plugin.ts +16 -8
  70. package/src/vite-root.ts +8 -0
package/src/deploy.ts CHANGED
@@ -10,10 +10,10 @@ import {
10
10
  url,
11
11
  } from "./schema-text.ts";
12
12
 
13
- const APPIMAGE_COMPRESSIONS = ["gzip", "xz", "zstd"] as const;
14
13
  const DEB_COMPRESSIONS = ["gzip", "none", "xz", "zstd"] as const;
15
14
  const DEB_SIGN_METHODS = ["debsign", "dpkg-sig"] as const;
16
15
  const DEB_SIGN_TYPES = ["archive", "maint", "origin"] as const;
16
+ const DEPLOY_ARCH_NAMES = ["arm64", "x64"] as const;
17
17
  const DEPLOY_TARGET_NAMES = ["appimage", "deb", "flatpak", "rpm"] as const;
18
18
  const FLATPAK_MODES = ["prebuilt", "source"] as const;
19
19
  const NODE_SOURCES = ["download", "host", "path"] as const;
@@ -37,16 +37,22 @@ const URL_KINDS = [
37
37
  const BOOLEAN_ERROR = "must be a boolean";
38
38
  const EPOCH_ERROR = "must be a non-negative integer";
39
39
  const EXTRA_FILE_ERROR = "must be a source path or a { source, mode } entry";
40
- const FILE_MODE_ERROR = "must be an octal file mode such as 755";
40
+ const FILE_MODE_ERROR = "must be an octal file mode without setuid or setgid bits, such as 755";
41
41
  const FILE_MODE_PATTERN = /^[0-7]{3,4}$/;
42
+ const FILE_MODE_RADIX = 8;
43
+ const PRIVILEGED_FILE_MODE_MASK = 0o6000;
42
44
  const HEX_COLOR_ERROR = "must be a #rrggbb color";
43
45
  const HEX_COLOR_PATTERN = /^#[\dA-Fa-f]{6}$/;
44
46
  const KEY_FILE_ERROR = "must be a path to a PGP key file";
45
47
  const KEY_ID_ERROR = "must be a PGP key id";
48
+ const LAUNCHER_ENV_ERROR = "must be a record of POSIX environment names to values without null bytes";
49
+ const LAUNCHER_ENV_NAME_ERROR = "must be a POSIX environment name";
50
+ const LAUNCHER_ENV_NAME_PATTERN = /^[A-Za-z_]\w*$/;
46
51
  const MINIMUM_LIBRARY_VERSION_ERROR = "must be a version such as 4.18";
47
52
  const MINIMUM_LIBRARY_VERSION_PATTERN = /^\d+(?:\.\d+)*$/;
48
53
  const MINIMUM_LIBRARY_VERSIONS_ERROR = "must be a record of GIR library ids to a minimum version";
49
- const LIBRARY_ID_ERROR = 'must be a GIR library identifier of the form "Name-Version", such as "Gtk-4.0"';
54
+ const NODE_FLAG_ERROR = "must be a Node.js flag beginning with a hyphen and containing no null bytes";
55
+ const LIBRARY_ID_ERROR = 'must be a GIR library identifier of the form "Name-Version", such as "Adw-1"';
50
56
  const SCRIPT_ERROR = "must be a path to a shell script";
51
57
  const SOURCE_PATH_ERROR = "must be a source path";
52
58
  const SPDX_ERROR = "must be an SPDX license expression";
@@ -110,14 +116,53 @@ const brandingSchema = z.strictObject({
110
116
  dark: hexColorSchema,
111
117
  });
112
118
 
113
- const extraFileSchema = z.strictObject({
119
+ /**
120
+ * A `deploy.extraFiles` entry in object form: the source file, resolved against the project root, that is
121
+ * installed at the entry's prefix-relative destination, and the octal mode it is installed with.
122
+ */
123
+ type DeployExtraFileOptions = Record<"source", string> & {
124
+ /** Octal file mode. Setuid and setgid bits are rejected. */
125
+ mode?: string | undefined;
126
+ };
127
+ const extraFileSchema: z.ZodType<DeployExtraFileOptions> = z.strictObject({
114
128
  source: text(SOURCE_PATH_ERROR),
115
- mode: z.string({ error: FILE_MODE_ERROR }).regex(FILE_MODE_PATTERN, { error: FILE_MODE_ERROR }).optional(),
129
+ mode: z
130
+ .string({ error: FILE_MODE_ERROR })
131
+ .regex(FILE_MODE_PATTERN, { error: FILE_MODE_ERROR })
132
+ .refine((mode) => (Number.parseInt(mode, FILE_MODE_RADIX) & PRIVILEGED_FILE_MODE_MASK) === 0, {
133
+ error: FILE_MODE_ERROR,
134
+ })
135
+ .optional(),
116
136
  });
117
137
 
118
138
  const extraFileEntrySchema = z.union([text(SOURCE_PATH_ERROR), extraFileSchema], { error: EXTRA_FILE_ERROR });
119
139
 
120
- const nodeRuntimeSchema = z.strictObject({
140
+ const launcherEnvSchema = z.record(
141
+ z.string({ error: LAUNCHER_ENV_NAME_ERROR }).regex(LAUNCHER_ENV_NAME_PATTERN, { error: LAUNCHER_ENV_NAME_ERROR }),
142
+ z.string({ error: LAUNCHER_ENV_ERROR }).refine((value) => !value.includes("\0"), { error: LAUNCHER_ENV_ERROR }),
143
+ { error: (issue) => issue.code === "invalid_key" ? LAUNCHER_ENV_NAME_ERROR : LAUNCHER_ENV_ERROR },
144
+ );
145
+
146
+ const nodeFlagsSchema = z.array(
147
+ z
148
+ .string({ error: NODE_FLAG_ERROR })
149
+ .refine((value) => value.startsWith("-") && !value.includes("\0"), { error: NODE_FLAG_ERROR }),
150
+ { error: "must be an array of Node.js flags" },
151
+ );
152
+
153
+ /**
154
+ * The `deploy.node` options: where the Node.js runtime bundled with the application comes from, the version it
155
+ * is expected to be, whether its binary is stripped, and whether the launcher enables the compile cache.
156
+ */
157
+ type DeployNodeOptions = Partial<
158
+ Record<"source", "download" | "host" | "path" | undefined> &
159
+ Record<"path", string | undefined> &
160
+ Record<"shouldStrip" | "shouldUseCompileCache", boolean | undefined>
161
+ > & {
162
+ /** Expected Node.js version. Downloaded runtimes default to exactly 26.8.2 when omitted. */
163
+ version?: string | undefined;
164
+ };
165
+ const nodeRuntimeSchema: z.ZodType<DeployNodeOptions> = z.strictObject({
121
166
  source: z.enum(NODE_SOURCES, { error: "must be one of download, host, path" }).optional(),
122
167
  version: text("must be a Node.js version such as 26.7.0").optional(),
123
168
  path: text("must be a path to a node binary").optional(),
@@ -150,7 +195,7 @@ const rpmSchema = z.strictObject({
150
195
 
151
196
  const appimageSchema = z.strictObject({
152
197
  fileName: text("must be an output file name").optional(),
153
- compression: z.enum(APPIMAGE_COMPRESSIONS, { error: "must be one of gzip, xz, zstd" }).optional(),
198
+ compression: z.literal("zstd", { error: "must be zstd" }).optional(),
154
199
  updateInformation: text("must be an AppImage update information string").optional(),
155
200
  runtimeFile: text("must be a path to an AppImage runtime file").optional(),
156
201
  });
@@ -229,6 +274,11 @@ const deploySchema = z.strictObject({
229
274
  error: "must be an array of deploy targets",
230
275
  })
231
276
  .optional(),
277
+ architectures: z
278
+ .array(z.enum(DEPLOY_ARCH_NAMES, { error: "must be one of arm64, x64" }), {
279
+ error: "must be an array of deploy architectures",
280
+ })
281
+ .optional(),
232
282
  outDir: text("must be a directory path relative to the project root").optional(),
233
283
  name: text("must be the display name shown in the launcher").optional(),
234
284
  genericName: text("must be a generic application name").optional(),
@@ -262,6 +312,8 @@ const deploySchema = z.strictObject({
262
312
  .optional(),
263
313
  releases: z.array(releaseSchema, { error: "must be an array of releases" }).optional(),
264
314
  execArgs: textList("argument", "must be an array of arguments appended to Exec").optional(),
315
+ launcherEnv: launcherEnvSchema.optional(),
316
+ nodeFlags: nodeFlagsSchema.optional(),
265
317
  fileAssociations: z
266
318
  .array(fileAssociationSchema, { error: "must be an array of file associations" })
267
319
  .optional(),
@@ -271,6 +323,7 @@ const deploySchema = z.strictObject({
271
323
  .optional(),
272
324
  desktopEntry: textRecord("must be a desktop entry value", "must be a record of desktop entry keys to values")
273
325
  .optional(),
326
+ metainfoExtra: textList("AppStream XML fragment", "must be an array of AppStream XML fragments").optional(),
274
327
  isDbusActivatable: flag(BOOLEAN_ERROR).optional(),
275
328
  extraFiles: relativePathRecord(
276
329
  "must be a destination path inside the install prefix, without a leading slash or a .. segment",
@@ -289,4 +342,4 @@ const deploySchema = z.strictObject({
289
342
  rpm: rpmSchema.optional(),
290
343
  });
291
344
 
292
- export { deploySchema };
345
+ export { deploySchema, type DeployExtraFileOptions, type DeployNodeOptions };
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { type Config, defineConfig, mergeConfig, type ResolvedConfig } from "./config.ts";
2
+ export { type DeployExtraFileOptions, type DeployNodeOptions } from "./deploy.ts";
2
3
  export { type ConfigLoader, type LoadedConfig, loadConfig } from "./loader.ts";
package/src/internal.ts CHANGED
@@ -1,9 +1,12 @@
1
+ export { configDependenciesFor } from "./config-dependencies.ts";
2
+ export { createConfigReloader } from "./config-reloader.ts";
1
3
  export type { McpSettings, ResolvedReactCompilerOptions } from "./config.ts";
2
4
  export {
3
5
  APPLICATION_ID_MAX_LENGTH,
4
6
  isAgentReferenceEnabled,
5
7
  isAgentRulesEnabled,
6
8
  isValidApplicationId,
9
+ resolveAcceptedChildTypes,
7
10
  resolveElementComponents,
8
11
  resolveElementProps,
9
12
  resolveLazyElements,
@@ -11,4 +14,7 @@ export {
11
14
  resolveOmittedProps,
12
15
  } from "./config.ts";
13
16
  export { createConfigLoader } from "./loader.ts";
17
+ export { assertSupportedNodeVersion, DEFAULT_NODE_VERSION, MINIMUM_NODE_VERSION } from "./node-version.ts";
14
18
  export { resourceBasePath } from "./resource-base-path.ts";
19
+ export { viteProjectRoot } from "./vite-root.ts";
20
+ export { SUPPORTED_EXTENSIONS as CONFIG_EXTENSIONS } from "c12";
package/src/loader.ts CHANGED
@@ -1,8 +1,14 @@
1
- import { warn } from "@gtkx/utils";
1
+ import { isPathInside, warn } from "@gtkx/utils";
2
2
  import { loadConfig as loadConfigFile } from "c12";
3
3
  import { existsSync } from "node:fs";
4
- import { resolve } from "node:path";
4
+ import { relative, resolve } from "node:path";
5
+ import {
6
+ captureConfigDependencies,
7
+ setConfigDependencies,
8
+ transformConfigModule,
9
+ } from "./config-dependencies.ts";
5
10
  import { missingConfigFileError } from "./config-error.ts";
11
+ import { type ConfigResolutionOptions, localConfigSourcePath } from "./config-source.ts";
6
12
  import {
7
13
  type Config,
8
14
  graduatedFutureKeys,
@@ -10,6 +16,7 @@ import {
10
16
  type ResolvedConfig,
11
17
  validateConfig,
12
18
  } from "./config.ts";
19
+ import { assertSupportedNodeVersion } from "./node-version.ts";
13
20
 
14
21
  /** Result of loading a project's `gtkx.config.ts` file. */
15
22
  type LoadedConfig = {
@@ -28,7 +35,7 @@ type LoadConfigOptions = {
28
35
  * top-level values, and the name is passed to a config authored as a function.
29
36
  */
30
37
  mode?: string | undefined;
31
- };
38
+ } & Partial<Record<"configFile", string | undefined>>;
32
39
 
33
40
  /** Reads a project's configuration once per directory, caching what it loads and what it resolves. */
34
41
  type ConfigLoader = {
@@ -41,6 +48,40 @@ type ConfigLoader = {
41
48
  const GRADUATED_FUTURE_ENV = "GTKX_GRADUATED_FUTURE_SHOWN";
42
49
  const graduatedFutureWarnings: Map<string, string> = new Map();
43
50
 
51
+ const rejectMissingLocalConfig = (source: string, options: ConfigResolutionOptions): undefined => {
52
+ const path = localConfigSourcePath(source, options);
53
+
54
+ if (path !== undefined && !existsSync(path)) {
55
+ throw new Error(`Extended configuration does not exist at ${path}`);
56
+ }
57
+ };
58
+
59
+ const withConfigDependencies = <T>(dependencies: string[], operation: () => T): T => {
60
+ try {
61
+ return operation();
62
+ } catch (error) {
63
+ if (typeof error === "object" && error !== null) {
64
+ setConfigDependencies(error, dependencies);
65
+ }
66
+
67
+ throw error;
68
+ }
69
+ };
70
+
71
+ const selectedConfigFile = (root: string, configured: string | undefined): string | undefined => {
72
+ if (configured === undefined) {
73
+ return undefined;
74
+ }
75
+
76
+ const path = resolve(root, configured);
77
+
78
+ if (!isPathInside(root, path)) {
79
+ throw new Error(`Configuration file ${configured} must be inside the project root ${root}`);
80
+ }
81
+
82
+ return relative(root, path);
83
+ };
84
+
44
85
  const warnGraduatedFuture = (config: unknown, root: string): void => {
45
86
  const keys = graduatedFutureKeys(config);
46
87
  const signature = keys.join(",");
@@ -65,37 +106,60 @@ const warnGraduatedFuture = (config: unknown, root: string): void => {
65
106
  * @throws When that directory holds no configuration file, or when the configuration fails validation.
66
107
  */
67
108
  const loadConfig = async (cwd: string, options: LoadConfigOptions = {}): Promise<LoadedConfig> => {
109
+ assertSupportedNodeVersion();
68
110
  const searched = resolve(cwd);
69
-
70
- const result = await loadConfigFile<Config>({
71
- name: "gtkx",
72
- cwd: searched,
73
- rcFile: false,
74
- globalRc: false,
75
- packageJson: false,
76
- context: { mode: options.mode },
77
- ...((options.mode !== undefined) && { envName: options.mode }),
78
- });
111
+ const requestedConfigFile = selectedConfigFile(searched, options.configFile);
112
+
113
+ const captured = await captureConfigDependencies(() =>
114
+ loadConfigFile<Config>({
115
+ name: "gtkx",
116
+ cwd: searched,
117
+ rcFile: false,
118
+ globalRc: false,
119
+ packageJson: false,
120
+ omit$Keys: true,
121
+ context: { mode: options.mode },
122
+ jitiOptions: { fsCache: false, transform: transformConfigModule },
123
+ resolve: rejectMissingLocalConfig,
124
+ ...(requestedConfigFile !== undefined && { configFile: requestedConfigFile }),
125
+ ...((options.mode !== undefined) && { envName: options.mode }),
126
+ }));
127
+ const result = captured.value;
79
128
 
80
129
  const configFile = result.configFile;
81
-
82
- if (configFile === undefined || !existsSync(resolve(searched, configFile))) {
83
- throw missingConfigFileError(searched);
84
- }
85
-
86
- const config = result.config;
87
130
  const root = result.cwd ?? searched;
88
- validateConfig(config);
89
- warnGraduatedFuture(config, root);
90
-
91
- return {
92
- config,
93
- configFile,
94
- root,
95
- };
131
+ const layerFiles = (result.layers ?? [])
132
+ .flatMap((layer) => layer.configFile === undefined
133
+ ? []
134
+ : [resolve(layer.cwd ?? root, layer.configFile)]);
135
+ const dependencies = [
136
+ ...(configFile === undefined ? [] : [configFile]),
137
+ ...layerFiles,
138
+ ...captured.dependencies,
139
+ ];
140
+
141
+ return withConfigDependencies(dependencies, () => {
142
+ if (configFile === undefined || !existsSync(resolve(searched, configFile))) {
143
+ throw missingConfigFileError(searched, requestedConfigFile);
144
+ }
145
+
146
+ const config = result.config;
147
+ validateConfig(config, configFile);
148
+ warnGraduatedFuture(config, root);
149
+
150
+ const loaded = {
151
+ config,
152
+ configFile,
153
+ root,
154
+ };
155
+ setConfigDependencies(loaded, dependencies);
156
+
157
+ return loaded;
158
+ });
96
159
  };
97
160
 
98
161
  const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
162
+ assertSupportedNodeVersion();
99
163
  const loaded: Map<string, Promise<LoadedConfig>> = new Map();
100
164
  const resolved: Map<string, Promise<ResolvedConfig>> = new Map();
101
165
 
@@ -114,4 +178,4 @@ const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
114
178
  };
115
179
  };
116
180
 
117
- export { loadConfig, createConfigLoader, type LoadedConfig, type ConfigLoader };
181
+ export { loadConfig, createConfigLoader, type LoadedConfig, type ConfigLoader, type LoadConfigOptions };
@@ -0,0 +1,17 @@
1
+ const MINIMUM_NODE_MAJOR = 26;
2
+ const MINIMUM_NODE_MINOR = 7;
3
+ const MINIMUM_NODE_VERSION = "26.7.0";
4
+ const DEFAULT_NODE_VERSION = "26.8.2";
5
+
6
+ const assertSupportedNodeVersion = (): void => {
7
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
8
+ const isSupported = major > MINIMUM_NODE_MAJOR || (major === MINIMUM_NODE_MAJOR && minor >= MINIMUM_NODE_MINOR);
9
+
10
+ if (!isSupported) {
11
+ throw new Error(
12
+ `GTKX requires Node.js ${MINIMUM_NODE_VERSION} or newer. Current version: ${process.versions.node}.`,
13
+ );
14
+ }
15
+ };
16
+
17
+ export { assertSupportedNodeVersion, DEFAULT_NODE_VERSION, MINIMUM_NODE_VERSION };