@gtkx/config 1.6.0 → 2.0.0-beta.10

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 (65) hide show
  1. package/README.md +47 -44
  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 +83 -0
  5. package/dist/config-dependencies.js.map +1 -0
  6. package/dist/config-error.d.ts +4 -15
  7. package/dist/config-error.d.ts.map +1 -1
  8. package/dist/config-error.js +8 -55
  9. package/dist/config-error.js.map +1 -1
  10. package/dist/config.d.ts +35 -61
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +49 -94
  13. package/dist/config.js.map +1 -1
  14. package/dist/deploy.d.ts +26 -16
  15. package/dist/deploy.d.ts.map +1 -1
  16. package/dist/deploy.js +29 -4
  17. package/dist/deploy.js.map +1 -1
  18. package/dist/index.d.ts +1 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/internal.d.ts +5 -2
  23. package/dist/internal.d.ts.map +1 -1
  24. package/dist/internal.js +4 -1
  25. package/dist/internal.js.map +1 -1
  26. package/dist/loader.d.ts +2 -4
  27. package/dist/loader.d.ts.map +1 -1
  28. package/dist/loader.js +96 -22
  29. package/dist/loader.js.map +1 -1
  30. package/dist/node-version.d.ts +4 -0
  31. package/dist/node-version.d.ts.map +1 -0
  32. package/dist/node-version.js +12 -0
  33. package/dist/node-version.js.map +1 -0
  34. package/dist/schema-text.d.ts +1 -2
  35. package/dist/schema-text.d.ts.map +1 -1
  36. package/dist/schema-text.js +1 -1
  37. package/dist/schema-text.js.map +1 -1
  38. package/dist/virtual.d.ts.map +1 -1
  39. package/dist/virtual.js +1 -2
  40. package/dist/virtual.js.map +1 -1
  41. package/dist/vite-plugin.d.ts.map +1 -1
  42. package/dist/vite-plugin.js +14 -5
  43. package/dist/vite-plugin.js.map +1 -1
  44. package/dist/vite-root.d.ts +9 -0
  45. package/dist/vite-root.d.ts.map +1 -0
  46. package/dist/vite-root.js +3 -0
  47. package/dist/vite-root.js.map +1 -0
  48. package/package.json +14 -5
  49. package/src/config-dependencies.ts +114 -0
  50. package/src/config-error.ts +10 -78
  51. package/src/config.ts +60 -166
  52. package/src/deploy.ts +61 -7
  53. package/src/index.ts +1 -2
  54. package/src/internal.ts +4 -3
  55. package/src/loader.ts +138 -31
  56. package/src/node-version.ts +16 -0
  57. package/src/schema-text.ts +1 -1
  58. package/src/virtual.ts +1 -4
  59. package/src/vite-plugin.ts +17 -7
  60. package/src/vite-root.ts +8 -0
  61. package/dist/deprecations.d.ts +0 -5
  62. package/dist/deprecations.d.ts.map +0 -1
  63. package/dist/deprecations.js +0 -97
  64. package/dist/deprecations.js.map +0 -1
  65. package/src/deprecations.ts +0 -130
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAQ3G,MAAM,gBAAgB,GAAG,CAAC,EAAU,EAAiB,EAAE,CACnD,EAAE,KAAK,sBAAsB,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC;AAE3E,MAAM,iBAAiB,GAAG,KAAK,EAC3B,EAAU,EACV,UAAwB,EACxB,KAAkB,EACS,EAAE;IAC7B,IAAI,EAAE,KAAK,+BAA+B,EAAE,CAAC;QACzC,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,OAAO,kBAAkB,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AACrF,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,kBAAkB,GAAG,CAAC,OAO3B,EAAU,EAAE;IACT,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,kBAAkB,EAAE,CAAC;IAC9D,MAAM,KAAK,GAAgB,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAE/C,OAAO;QACH,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,CAAC,MAAkB;YACrB,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;YAEvC,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;QACD,SAAS,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC/C,IAAI,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC;KACjE,CAAC;AACN,CAAC,CAAC;AAEF,eAAe,kBAAkB,CAAC","sourcesContent":["import type { Plugin, UserConfig } from \"vite\";\nimport { type ConfigLoader, createConfigLoader } from \"./loader.ts\";\nimport { GTKX_CONFIG_VIRTUAL_ID, renderConfigModule, RESOLVED_GTKX_CONFIG_VIRTUAL_ID } from \"./virtual.ts\";\n\n/** State the plugin carries from Vite's `config` hook to the virtual module it serves. */\ntype PluginState = {\n /** Project root taken from Vite's `config` hook, undefined when the user config leaves it unset. */\n root: string | undefined;\n};\n\nconst resolveVirtualId = (id: string): string | null =>\n id === GTKX_CONFIG_VIRTUAL_ID ? RESOLVED_GTKX_CONFIG_VIRTUAL_ID : null;\n\nconst loadVirtualModule = async (\n id: string,\n loadConfig: ConfigLoader,\n state: PluginState,\n): Promise<string | undefined> => {\n if (id !== RESOLVED_GTKX_CONFIG_VIRTUAL_ID) {\n return undefined;\n }\n\n return renderConfigModule(await loadConfig.resolve(state.root ?? process.cwd()));\n};\n\n/**\n * Creates a Vite plugin serving `virtual:gtkx-config`, the module carrying the project's resolved\n * `gtkx.config.ts`.\n */\nconst createConfigPlugin = (options: {\n /** Name the plugin is registered under in Vite. */\n name: string;\n /** Loader the configuration is resolved through, defaulting to a fresh caching loader. */\n loadConfig?: ConfigLoader;\n /** Extra Vite configuration contributed from the plugin's `config` hook, given the user's own configuration. */\n config?: (config: UserConfig) => Omit<UserConfig, \"plugins\">;\n}): Plugin => {\n const loadConfig = options.loadConfig ?? createConfigLoader();\n const state: PluginState = { root: undefined };\n\n return {\n name: options.name,\n config(config: UserConfig) {\n state.root = config.root ?? state.root;\n\n return options.config?.(config);\n },\n resolveId: (id: string) => resolveVirtualId(id),\n load: (id: string) => loadVirtualModule(id, loadConfig, state),\n };\n};\n\nexport default createConfigPlugin;\n"]}
1
+ {"version":3,"file":"vite-plugin.js","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAC3G,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAQjD,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,IAAI,sBAAsB,GAAG,CAAC,CAAC;AAChE,MAAM,sBAAsB,GAAG,IAAI,MAAM,CAAC,IAAI,+BAA+B,GAAG,CAAC,CAAC;AAElF,MAAM,gBAAgB,GAAG,CAAC,EAAU,EAAiB,EAAE,CACnD,EAAE,KAAK,sBAAsB,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC;AAE3E,MAAM,iBAAiB,GAAG,KAAK,EAC3B,EAAU,EACV,UAAwB,EACxB,KAAkB,EACS,EAAE;IAC7B,IAAI,EAAE,KAAK,+BAA+B,EAAE,CAAC;QACzC,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,OAAO,kBAAkB,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;AACpE,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,kBAAkB,GAAG,CAAC,OAO3B,EAAU,EAAE;IACT,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,kBAAkB,EAAE,CAAC;IAC9D,MAAM,KAAK,GAAgB,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;IAEnD,OAAO;QACH,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,CAAC,MAAkB;YACrB,KAAK,CAAC,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;YAErC,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QACpC,CAAC;QACD,SAAS,EAAE;YACP,MAAM,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE;YAC7B,OAAO,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,CAAC;SAChD;QACD,IAAI,EAAE;YACF,MAAM,EAAE,EAAE,EAAE,EAAE,sBAAsB,EAAE;YACtC,OAAO,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,CAAC;SACpE;KACJ,CAAC;AACN,CAAC,CAAC;AAEF,eAAe,kBAAkB,CAAC","sourcesContent":["import type { Plugin, UserConfig } from \"vite\";\nimport { type ConfigLoader, createConfigLoader } from \"./loader.ts\";\nimport { GTKX_CONFIG_VIRTUAL_ID, renderConfigModule, RESOLVED_GTKX_CONFIG_VIRTUAL_ID } from \"./virtual.ts\";\nimport { viteProjectRoot } from \"./vite-root.ts\";\n\n/** State the plugin carries from Vite's `config` hook to the virtual module it serves. */\ntype PluginState = {\n /** Project root taken from Vite's `config` hook, falling back to the working directory. */\n root: string;\n};\n\nconst VIRTUAL_ID_RE = new RegExp(`^${GTKX_CONFIG_VIRTUAL_ID}$`);\nconst RESOLVED_VIRTUAL_ID_RE = new RegExp(`^${RESOLVED_GTKX_CONFIG_VIRTUAL_ID}$`);\n\nconst resolveVirtualId = (id: string): string | null =>\n id === GTKX_CONFIG_VIRTUAL_ID ? RESOLVED_GTKX_CONFIG_VIRTUAL_ID : null;\n\nconst loadVirtualModule = async (\n id: string,\n loadConfig: ConfigLoader,\n state: PluginState,\n): Promise<string | undefined> => {\n if (id !== RESOLVED_GTKX_CONFIG_VIRTUAL_ID) {\n return undefined;\n }\n\n return renderConfigModule(await loadConfig.resolve(state.root));\n};\n\n/**\n * Creates a Vite plugin serving `virtual:gtkx-config`, the module carrying the project's resolved\n * `gtkx.config.ts`.\n */\nconst createConfigPlugin = (options: {\n /** Name the plugin is registered under in Vite. */\n name: string;\n /** Loader the configuration is resolved through, defaulting to a fresh caching loader. */\n loadConfig?: ConfigLoader;\n /** Extra Vite configuration contributed from the plugin's `config` hook, given the user's own configuration. */\n config?: (config: UserConfig) => Omit<UserConfig, \"plugins\">;\n}): Plugin => {\n const loadConfig = options.loadConfig ?? createConfigLoader();\n const state: PluginState = { root: process.cwd() };\n\n return {\n name: options.name,\n config(config: UserConfig) {\n state.root = viteProjectRoot(config);\n\n return options.config?.(config);\n },\n resolveId: {\n filter: { id: VIRTUAL_ID_RE },\n handler: (id: string) => resolveVirtualId(id),\n },\n load: {\n filter: { id: RESOLVED_VIRTUAL_ID_RE },\n handler: (id: string) => loadVirtualModule(id, loadConfig, state),\n },\n };\n};\n\nexport default createConfigPlugin;\n"]}
@@ -0,0 +1,9 @@
1
+ type RootConfig = {
2
+ root?: string | undefined;
3
+ test?: {
4
+ root?: string | undefined;
5
+ } | undefined;
6
+ };
7
+ declare const viteProjectRoot: (config: RootConfig) => string;
8
+ export { viteProjectRoot };
9
+ //# sourceMappingURL=vite-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-root.d.ts","sourceRoot":"","sources":["../src/vite-root.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GAAG,SAAS,CAAC;CACpD,CAAC;AAEF,QAAA,MAAM,eAAe,GAAI,QAAQ,UAAU,KAAG,MAA2D,CAAC;AAE1G,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ const viteProjectRoot = (config) => config.test?.root ?? config.root ?? process.cwd();
2
+ export { viteProjectRoot };
3
+ //# sourceMappingURL=vite-root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-root.js","sourceRoot":"","sources":["../src/vite-root.ts"],"names":[],"mappings":"AAKA,MAAM,eAAe,GAAG,CAAC,MAAkB,EAAU,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;AAE1G,OAAO,EAAE,eAAe,EAAE,CAAC","sourcesContent":["type RootConfig = {\n root?: string | undefined;\n test?: { root?: string | undefined } | undefined;\n};\n\nconst viteProjectRoot = (config: RootConfig): string => config.test?.root ?? config.root ?? process.cwd();\n\nexport { viteProjectRoot };\n"]}
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@gtkx/config",
3
- "version": "1.6.0",
4
- "description": "Config schema, loader, and element-prop mapping for the GTK toolchain.",
3
+ "version": "2.0.0-beta.10",
4
+ "description": "Configuration and element-prop mapping for Adwaita-first GTKX apps.",
5
5
  "keywords": [
6
6
  "gtkx",
7
7
  "gtk",
8
8
  "gtk4",
9
+ "adwaita",
10
+ "gnome",
9
11
  "react",
10
12
  "typescript",
11
13
  "config",
@@ -28,14 +30,20 @@
28
30
  "./package.json": "./package.json",
29
31
  ".": {
30
32
  "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js",
34
+ "require": null,
31
35
  "default": "./dist/index.js"
32
36
  },
33
37
  "./internal": {
34
38
  "types": "./dist/internal.d.ts",
39
+ "import": "./dist/internal.js",
40
+ "require": null,
35
41
  "default": "./dist/internal.js"
36
42
  },
37
43
  "./vite-plugin": {
38
44
  "types": "./dist/vite-plugin.d.ts",
45
+ "import": "./dist/vite-plugin.js",
46
+ "require": null,
39
47
  "default": "./dist/vite-plugin.js"
40
48
  }
41
49
  },
@@ -55,14 +63,15 @@
55
63
  }
56
64
  },
57
65
  "engines": {
58
- "node": ">=24"
66
+ "node": ">=26.7.0"
59
67
  },
60
68
  "dependencies": {
69
+ "@gtkx/utils": "2.0.0-beta.10",
61
70
  "c12": "^3.3.4",
62
71
  "defu": "^6.1.7",
72
+ "jiti": "^2.7.0",
63
73
  "vite": "^8.2.2",
64
- "zod": "^4.4.3",
65
- "@gtkx/utils": "1.6.0"
74
+ "zod": "^4.5.4"
66
75
  },
67
76
  "scripts": {
68
77
  "release": "tsx ../../scripts/release-package.ts"
@@ -0,0 +1,114 @@
1
+ import { createJiti, type Jiti, type TransformOptions, type TransformResult } from "jiti";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { createRequire, registerHooks } from "node:module";
4
+ import { extname, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ type CaptureState = { cacheKey: string; dependencies: Set<string>; transformer: Jiti };
8
+ type CapturedConfig<T> = { dependencies: string[]; value: T };
9
+
10
+ const captureStorage: AsyncLocalStorage<CaptureState> = new AsyncLocalStorage();
11
+ const dependenciesByValue: WeakMap<object, string[]> = new WeakMap();
12
+ const CACHE_BUSTED_EXTENSIONS: ReadonlySet<string> = new Set([".cjs", ".js", ".json", ".mjs"]);
13
+
14
+ const isTrackedPath = (path: string): boolean => !path.includes(`${sep}node_modules${sep}`);
15
+
16
+ const addPath = (path: string): void => {
17
+ if (isTrackedPath(path)) {
18
+ captureStorage.getStore()?.dependencies.add(path);
19
+ }
20
+ };
21
+
22
+ const addUrl = (url: string): void => {
23
+ if (url.startsWith("file:")) {
24
+ addPath(fileURLToPath(url));
25
+ }
26
+ };
27
+
28
+ const cacheBustedUrl = (url: string): string => {
29
+ if (!url.startsWith("file:")) {
30
+ return url;
31
+ }
32
+
33
+ const path = fileURLToPath(url);
34
+
35
+ if (!isTrackedPath(path) || !CACHE_BUSTED_EXTENSIONS.has(extname(path))) {
36
+ return url;
37
+ }
38
+
39
+ const parsed = new URL(url);
40
+ parsed.searchParams.set("gtkx-config-load", captureStorage.getStore()?.cacheKey ?? "uncaptured");
41
+
42
+ return parsed.href;
43
+ };
44
+
45
+ const registerResolutionHook = (): ReturnType<typeof registerHooks> =>
46
+ registerHooks({
47
+ resolve(specifier, context, nextResolve) {
48
+ const result = nextResolve(specifier, context);
49
+ addUrl(result.url);
50
+
51
+ return { ...result, url: cacheBustedUrl(result.url) };
52
+ },
53
+ });
54
+
55
+ const clearNativeModuleCache = (dependencies: Iterable<string>): void => {
56
+ const cache = createRequire(import.meta.url).cache;
57
+
58
+ for (const path of dependencies) {
59
+ Reflect.deleteProperty(cache, path);
60
+ }
61
+ };
62
+
63
+ const transformConfigModule = (options: TransformOptions): TransformResult => {
64
+ const state = captureStorage.getStore();
65
+
66
+ if (state === undefined) {
67
+ throw new Error("Configuration transformation started outside a dependency capture");
68
+ }
69
+
70
+ if (options.filename !== undefined) {
71
+ addPath(options.filename);
72
+ }
73
+
74
+ return { code: state.transformer.transform(options) };
75
+ };
76
+
77
+ const setConfigDependencies = (value: object, dependencies: Iterable<string>): void => {
78
+ const paths = [...new Set(dependencies)];
79
+ dependenciesByValue.set(value, paths);
80
+ clearNativeModuleCache(paths);
81
+ };
82
+
83
+ const configDependenciesFor = (value: unknown): string[] =>
84
+ typeof value === "object" && value !== null ? dependenciesByValue.get(value) ?? [] : [];
85
+
86
+ const captureConfigDependencies = async <T>(operation: () => Promise<T>): Promise<CapturedConfig<T>> => {
87
+ const state: CaptureState = {
88
+ cacheKey: process.hrtime.bigint().toString(),
89
+ dependencies: new Set(),
90
+ transformer: createJiti(import.meta.url, { fsCache: false, moduleCache: false }),
91
+ };
92
+ const hook = registerResolutionHook();
93
+
94
+ try {
95
+ const value = await captureStorage.run(state, operation);
96
+
97
+ return { dependencies: [...state.dependencies], value };
98
+ } catch (error) {
99
+ if (typeof error === "object" && error !== null) {
100
+ setConfigDependencies(error, state.dependencies);
101
+ }
102
+
103
+ throw error;
104
+ } finally {
105
+ hook.deregister();
106
+ }
107
+ };
108
+
109
+ export {
110
+ captureConfigDependencies,
111
+ configDependenciesFor,
112
+ setConfigDependencies,
113
+ transformConfigModule,
114
+ };
@@ -1,86 +1,18 @@
1
- import type { z } from "zod";
1
+ import { z } from "zod";
2
2
 
3
- type IssuePath = (string | number)[];
4
-
5
- const CONFIG_PREFIX = "gtkx.config.ts:";
6
- const UNRECOGNIZED_KEY_REASON = "is not a recognized key";
3
+ const DEFAULT_CONFIG_FILE = "gtkx.config.*";
7
4
 
8
5
  const isRecord = (value: unknown): value is Record<string, unknown> =>
9
6
  typeof value === "object" && value !== null && !Array.isArray(value);
10
7
 
11
- const rawIssue = (input: unknown, path: IssuePath, message: string, isStandalone = false) => ({
12
- code: "custom" as const,
13
- input,
14
- path,
15
- message,
16
- continue: true as const,
17
- ...(isStandalone && { params: { standalone: true } }),
18
- });
19
-
20
- const appendSegment = (path: string, segment: PropertyKey): string => {
21
- if (typeof segment === "number") {
22
- return `${path}[${String(segment)}]`;
23
- }
24
-
25
- return path === "" ? String(segment) : `${path}.${String(segment)}`;
26
- };
27
-
28
- const dottedPath = (segments: PropertyKey[]): string => {
29
- let path = "";
30
-
31
- for (const segment of segments) {
32
- path = appendSegment(path, segment);
33
- }
34
-
35
- return path;
36
- };
37
-
38
- const isStandaloneIssue = (issue: z.core.$ZodIssue): boolean =>
39
- "params" in issue && isRecord(issue.params) && issue.params.standalone === true;
40
-
41
- const unrecognizedKeyPath = (issue: z.core.$ZodIssue, fullPath: PropertyKey[]): string | undefined => {
42
- if (issue.code === "unrecognized_keys") {
43
- const [key] = issue.keys;
44
-
45
- return dottedPath(key === undefined ? fullPath : [...fullPath, key]);
46
- }
47
-
48
- return issue.code === "invalid_key" ? dottedPath(fullPath) : undefined;
49
- };
50
-
51
- const keyRejectionReason = (issue: z.core.$ZodIssue): string => {
52
- const nested = issue.code === "invalid_key" ? issue.issues[0] : undefined;
53
-
54
- return nested?.code === "custom" ? nested.message : UNRECOGNIZED_KEY_REASON;
55
- };
56
-
57
- const formatIssue = (issue: z.core.$ZodIssue, fullPath: PropertyKey[]): string => {
58
- const unrecognized = unrecognizedKeyPath(issue, fullPath);
59
-
60
- if (unrecognized !== undefined) {
61
- return `${CONFIG_PREFIX} \`${unrecognized}\` ${keyRejectionReason(issue)}`;
62
- }
63
-
64
- if (isStandaloneIssue(issue)) {
65
- return `${CONFIG_PREFIX} ${issue.message}`;
66
- }
67
-
68
- const path = dottedPath(fullPath);
69
-
70
- return path === "" ? `${CONFIG_PREFIX} ${issue.message}` : `${CONFIG_PREFIX} \`${path}\` ${issue.message}`;
71
- };
72
-
73
- const missingConfigFileError = (cwd: string): Error =>
74
- new Error(`${CONFIG_PREFIX} no configuration file found in ${cwd}`);
75
-
76
- const configError = (error: z.ZodError): Error => {
77
- const issue = error.issues[0];
8
+ const configPrefix = (configFile: string): string => `${configFile}:`;
78
9
 
79
- if (issue === undefined) {
80
- return new Error(`${CONFIG_PREFIX} invalid configuration`);
81
- }
10
+ const missingConfigFileError = (cwd: string, configFile?: string): Error =>
11
+ new Error(configFile === undefined
12
+ ? `${configPrefix(DEFAULT_CONFIG_FILE)} no configuration file found in ${cwd}`
13
+ : `${configPrefix(configFile)} no configuration file found in ${cwd}`);
82
14
 
83
- return new Error(formatIssue(issue, issue.path));
84
- };
15
+ const configError = (error: z.ZodError, configFile = DEFAULT_CONFIG_FILE): Error =>
16
+ new Error(`${configPrefix(configFile)}\n${z.prettifyError(error)}`);
85
17
 
86
- export { isRecord, rawIssue, missingConfigFileError, configError };
18
+ export { isRecord, missingConfigFileError, configError };
package/src/config.ts CHANGED
@@ -2,23 +2,17 @@ import { createDefineConfig, type DefineConfig } from "c12";
2
2
  import { defu } from "defu";
3
3
  import { resolve } from "node:path";
4
4
  import { z } from "zod";
5
- import { configError, isRecord, rawIssue } from "./config-error.ts";
5
+ import { configError, isRecord } from "./config-error.ts";
6
6
  import { deploySchema } from "./deploy.ts";
7
- import { DEPRECATION_IDS } from "./deprecations.ts";
8
- import { isGirLibrary, text } from "./schema-text.ts";
7
+ import { girLibrary, text } from "./schema-text.ts";
9
8
  import { resolveUserEventSignals } from "./user-event-signals.ts";
10
9
 
11
- /** Accepted `reactCompiler.compilationMode` values, choosing which functions the compiler processes. */
12
- type ReactCompilerCompilationMode = (typeof COMPILATION_MODES)[number];
13
- /** Accepted `reactCompiler.panicThreshold` values, choosing which compiler diagnostics fail the build. */
14
- type ReactCompilerPanicThreshold = (typeof PANIC_THRESHOLDS)[number];
15
-
16
- /** Object form of the `reactCompiler` config key, forwarded as-is to `babel-plugin-react-compiler`. */
10
+ /** Object form of the `reactCompiler` config key, forwarded to `babel-plugin-react-compiler`. */
17
11
  type ReactCompilerOptions = {
18
- /** Which functions the compiler processes; left to the compiler's own default when omitted. */
19
- compilationMode?: ReactCompilerCompilationMode;
20
- /** Which compiler diagnostics fail the build; left to the compiler's own default when omitted. */
21
- panicThreshold?: ReactCompilerPanicThreshold;
12
+ /** Which functions the compiler processes. */
13
+ compilationMode?: (typeof COMPILATION_MODES)[number];
14
+ /** Which compiler diagnostics fail the build. */
15
+ panicThreshold?: (typeof PANIC_THRESHOLDS)[number];
22
16
  };
23
17
 
24
18
  /**
@@ -33,9 +27,8 @@ type ResolvedReactCompilerOptions = ReactCompilerOptions & {
33
27
  /**
34
28
  * User-facing configuration for a GTKX project, as authored in `gtkx.config.ts`: the GIR libraries
35
29
  * to bind and where to find them, the GApplication id, per-element configuration, the React
36
- * Compiler, codegen, and user event signal settings, the `agents` and `mcp` blocks controlling what
37
- * coding agents are given, the `future` block opting into behavior that becomes the default in the
38
- * next major version, and the `deprecations` block silencing the warnings about flags left unset.
30
+ * Compiler, codegen, and user event signal settings, and the `agents` and `mcp` blocks controlling
31
+ * what coding agents are given.
39
32
  */
40
33
  type Config = z.infer<typeof configSchema>;
41
34
  type ModuleExport = z.infer<typeof moduleExportSchema>;
@@ -46,16 +39,6 @@ type McpSettings = {
46
39
  isReadOnly: boolean;
47
40
  };
48
41
 
49
- type ResolvedFuture = {
50
- isByteArrayTyped: boolean;
51
- isValueUnwrapped: boolean;
52
- isFinishTrimmed: boolean;
53
- isInoutInPlace: boolean;
54
- isResourceImported: boolean;
55
- isAdwaitaDefault: boolean;
56
- isTreeShaken: boolean;
57
- };
58
-
59
42
  /** Configuration reduced to the values the app runtime and the build need, with paths already resolved. */
60
43
  type ResolvedConfig = {
61
44
  /** The GApplication identifier the app registers under. */
@@ -73,92 +56,35 @@ type ResolvedConfig = {
73
56
  lazyElements: string[];
74
57
  };
75
58
 
76
- const LIBRARIES_WILDCARD = "*";
77
59
  const APPLICATION_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)+$/;
78
60
  const APPLICATION_ID_MAX_LENGTH = 255;
61
+ const IMPLICIT_LIBRARIES: Set<string> = new Set(["Adw-1", "Gtk-4.0"]);
79
62
  /** Compilation modes `babel-plugin-react-compiler` accepts. */
80
63
  const COMPILATION_MODES = ["infer", "syntax", "annotation", "all"] as const;
81
64
  /** Panic thresholds `babel-plugin-react-compiler` accepts. */
82
65
  const PANIC_THRESHOLDS = ["none", "critical_errors", "all_errors"] as const;
83
66
  const REACT_COMPILER_TARGET = "19";
84
- const COMPILATION_MODE_SET: Set<string> = new Set(COMPILATION_MODES);
85
- const PANIC_THRESHOLD_SET: Set<string> = new Set(PANIC_THRESHOLDS);
86
-
87
- const librariesSchema = z.custom<typeof LIBRARIES_WILDCARD | string[]>().check((ctx) => {
88
- const value = ctx.value;
89
-
90
- if (value === LIBRARIES_WILDCARD) {
91
- return;
92
- }
93
67
 
94
- if (!Array.isArray(value) || value.length === 0) {
95
- ctx.issues.push(rawIssue(value, [], `must be "${LIBRARIES_WILDCARD}", a non-empty string array, or omitted`));
68
+ const librarySchema = girLibrary('must be of the form "Name-Version", such as "Adw-1"')
69
+ .refine((library) => !IMPLICIT_LIBRARIES.has(library), { error: "is bound implicitly; remove it" });
96
70
 
97
- return;
98
- }
99
-
100
- for (const [index, entry] of value.entries()) {
101
- ctx.issues.push(...libraryEntryIssues(value, index, entry));
102
- }
103
- });
71
+ const librariesSchema = z
72
+ .array(librarySchema, { error: "must be a non-empty string array or omitted" })
73
+ .min(1, { error: "must be a non-empty string array or omitted" });
104
74
 
105
- const applicationIdSchema = z.custom<string>().check((ctx) => {
106
- const value = ctx.value;
107
-
108
- if (typeof value !== "string" || !isValidApplicationId(value)) {
109
- ctx.issues.push(
110
- rawIssue(
111
- value,
112
- [],
113
- `invalid \`applicationId\` "${value}", must satisfy g_application_id_is_valid ` +
114
- '(e.g. "org.example.MyApp")',
115
- true,
116
- ),
117
- );
118
- }
119
- });
75
+ const applicationIdSchema = z
76
+ .string({ error: "must satisfy g_application_id_is_valid" })
77
+ .refine((value) => isValidApplicationId(value), {
78
+ error: 'must satisfy g_application_id_is_valid, such as "org.example.MyApp"',
79
+ });
120
80
 
121
- const reactCompilerSchema = z.custom<boolean | ReactCompilerOptions>().check((ctx) => {
122
- const value = ctx.value;
123
-
124
- if (typeof value === "boolean") {
125
- return;
126
- }
127
-
128
- if (!isRecord(value)) {
129
- ctx.issues.push(rawIssue(value, [], "must be a boolean or an options object"));
130
-
131
- return;
132
- }
133
-
134
- const compilationMode = value.compilationMode;
135
-
136
- if (!isValidReactCompilerOption(compilationMode, COMPILATION_MODE_SET)) {
137
- ctx.issues.push(
138
- rawIssue(
139
- value,
140
- [],
141
- `invalid \`reactCompiler.compilationMode\` "${String(compilationMode)}", ` +
142
- `must be one of ${COMPILATION_MODES.join(", ")}`,
143
- true,
144
- ),
145
- );
146
- }
147
-
148
- const panicThreshold = value.panicThreshold;
149
-
150
- if (!isValidReactCompilerOption(panicThreshold, PANIC_THRESHOLD_SET)) {
151
- ctx.issues.push(
152
- rawIssue(
153
- value,
154
- [],
155
- `invalid \`reactCompiler.panicThreshold\` "${String(panicThreshold)}", ` +
156
- `must be one of ${PANIC_THRESHOLDS.join(", ")}`,
157
- true,
158
- ),
159
- );
160
- }
161
- });
81
+ const reactCompilerSchema = z.union([
82
+ z.boolean(),
83
+ z.object({
84
+ compilationMode: z.enum(COMPILATION_MODES).optional(),
85
+ panicThreshold: z.enum(PANIC_THRESHOLDS).optional(),
86
+ }),
87
+ ]);
162
88
 
163
89
  const userEventSignalsSchema = z.record(
164
90
  z.string(),
@@ -215,30 +141,28 @@ const mcpSchema = z.object({
215
141
  readOnly: z.boolean({ error: "must be a boolean" }).optional(),
216
142
  });
217
143
 
218
- const futureSchema = z.object({
219
- v2ByteArrays: z.boolean({ error: "must be a boolean" }).optional(),
220
- v2ValueReturns: z.boolean({ error: "must be a boolean" }).optional(),
221
- v2FinishResults: z.boolean({ error: "must be a boolean" }).optional(),
222
- v2InoutReturns: z.boolean({ error: "must be a boolean" }).optional(),
223
- v2ResourceImports: z.boolean({ error: "must be a boolean" }).optional(),
224
- v2DefaultLibraries: z.boolean({ error: "must be a boolean" }).optional(),
225
- v2TreeShaking: z.boolean({ error: "must be a boolean" }).optional(),
226
- });
227
-
228
- const FUTURE_KEYS: Set<string> = new Set(Object.keys(futureSchema.shape));
229
- const DEPRECATION_ID_ERROR = `must be one of ${DEPRECATION_IDS.join(", ")}`;
144
+ const graduatedFutureSchema = z
145
+ .object({
146
+ v2ByteArrays: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
147
+ v2ValueReturns: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
148
+ v2FinishResults: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
149
+ v2InoutReturns: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
150
+ v2ResourceImports: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
151
+ v2DefaultLibraries: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
152
+ v2TreeShaking: z.literal(true, { error: "can only be true; remove the flag" }).optional(),
153
+ })
154
+ .strict();
230
155
 
231
156
  const deprecationsSchema = z.object({
232
157
  silence: z
233
- .array(z.enum(DEPRECATION_IDS, { error: DEPRECATION_ID_ERROR }), {
234
- error: "must be an array of deprecation ids",
158
+ .array(z.never({ error: "does not name a current deprecation" }), {
159
+ error: "must be an array of current deprecation ids",
235
160
  })
236
161
  .optional(),
237
162
  });
238
163
 
239
164
  /** Schema every `gtkx.config.ts` is validated against, and the source of the {@link Config} type. */
240
165
  const configSchema = z.object({
241
- future: futureSchema.optional(),
242
166
  libraries: librariesSchema.optional(),
243
167
  girPath: z.array(z.string(), { error: "must be an array of strings if provided" }).optional(),
244
168
  applicationId: applicationIdSchema,
@@ -258,34 +182,10 @@ const configSchema = z.object({
258
182
  * autocompletion and type checking.
259
183
  */
260
184
  const defineConfig: DefineConfig<Config> = createDefineConfig<Config>();
185
+ const validationSchema = configSchema.extend({ future: graduatedFutureSchema.optional() });
261
186
 
262
- const libraryEntryIssues = (value: unknown[], index: number, entry: unknown): ReturnType<typeof rawIssue>[] => {
263
- if (typeof entry === "string" && isGirLibrary(entry)) {
264
- return [];
265
- }
266
-
267
- if (entry === LIBRARIES_WILDCARD) {
268
- const message =
269
- `to generate every library, set \`libraries: "${LIBRARIES_WILDCARD}"\` as a bare string, ` +
270
- "not an array entry";
271
-
272
- return [rawIssue(value, [index], message, true)];
273
- }
274
-
275
- const message =
276
- `invalid library identifier "${String(entry)}", must be of the form "Name-Version" ` +
277
- '(e.g. "Gtk-4.0")';
278
-
279
- return [rawIssue(value, [index], message, true)];
280
- };
281
-
282
- const isValidApplicationId = (applicationId: string): boolean => {
283
- if (applicationId.length === 0 || applicationId.length > APPLICATION_ID_MAX_LENGTH) {
284
- return false;
285
- }
286
-
287
- return APPLICATION_ID_PATTERN.test(applicationId);
288
- };
187
+ const isValidApplicationId = (applicationId: string): boolean =>
188
+ applicationId.length <= APPLICATION_ID_MAX_LENGTH && APPLICATION_ID_PATTERN.test(applicationId);
289
189
 
290
190
  const resolveReactCompilerOptions = (setting: Config["reactCompiler"]): ResolvedReactCompilerOptions | null => {
291
191
  if (setting === false) {
@@ -294,20 +194,29 @@ const resolveReactCompilerOptions = (setting: Config["reactCompiler"]): Resolved
294
194
 
295
195
  const overrides = setting === undefined || setting === true ? {} : setting;
296
196
 
297
- return { ...overrides, target: REACT_COMPILER_TARGET };
197
+ return {
198
+ ...(overrides.compilationMode !== undefined && { compilationMode: overrides.compilationMode }),
199
+ ...(overrides.panicThreshold !== undefined && { panicThreshold: overrides.panicThreshold }),
200
+ target: REACT_COMPILER_TARGET,
201
+ };
298
202
  };
299
203
 
300
- const isValidReactCompilerOption = (value: unknown, allowed: Set<string>): boolean =>
301
- value === undefined || (typeof value === "string" && allowed.has(value));
302
-
303
- const validateConfig = (config: Config): void => {
304
- const result = configSchema.safeParse(config);
204
+ const validateConfig = (config: unknown, configFile?: string): void => {
205
+ const result = validationSchema.safeParse(config);
305
206
 
306
207
  if (!result.success) {
307
- throw configError(result.error);
208
+ throw configError(result.error, configFile);
308
209
  }
309
210
  };
310
211
 
212
+ const graduatedFutureKeys = (config: unknown): string[] => {
213
+ if (!isRecord(config) || !isRecord(config.future)) {
214
+ return [];
215
+ }
216
+
217
+ return Object.keys(config.future).toSorted((first, second) => first.localeCompare(second));
218
+ };
219
+
311
220
  /**
312
221
  * Deep-merges a configuration over a base. `override` wins over `base` on conflicting scalar and object keys, while
313
222
  * arrays are concatenated with the `override` entries first.
@@ -356,19 +265,6 @@ const resolveMcpSettings = (config: Config): McpSettings => ({
356
265
  isReadOnly: config.mcp?.readOnly === true,
357
266
  });
358
267
 
359
- const resolveFuture = (future: Config["future"]): ResolvedFuture => ({
360
- isByteArrayTyped: future?.v2ByteArrays === true,
361
- isValueUnwrapped: future?.v2ValueReturns === true,
362
- isFinishTrimmed: future?.v2FinishResults === true,
363
- isInoutInPlace: future?.v2InoutReturns === true,
364
- isResourceImported: future?.v2ResourceImports === true,
365
- isAdwaitaDefault: future?.v2DefaultLibraries === true,
366
- isTreeShaken: future?.v2TreeShaking === true,
367
- });
368
-
369
- const unknownFutureKeys = (future: Config["future"]): string[] =>
370
- Object.keys(future ?? {}).filter((key) => !FUTURE_KEYS.has(key));
371
-
372
268
  const resolveConfig = (config: Config, root?: string): ResolvedConfig => ({
373
269
  applicationId: config.applicationId,
374
270
  reactCompiler: resolveReactCompilerOptions(config.reactCompiler),
@@ -383,6 +279,7 @@ export {
383
279
  isAgentReferenceEnabled,
384
280
  isAgentRulesEnabled,
385
281
  isValidApplicationId,
282
+ graduatedFutureKeys,
386
283
  validateConfig,
387
284
  mergeConfig,
388
285
  resolveLazyElements,
@@ -391,11 +288,8 @@ export {
391
288
  resolveMcpSettings,
392
289
  resolveOmittedProps,
393
290
  resolveConfig,
394
- resolveFuture,
395
- unknownFutureKeys,
396
291
  type McpSettings,
397
292
  type ResolvedReactCompilerOptions,
398
293
  type Config,
399
294
  type ResolvedConfig,
400
- type ResolvedFuture,
401
295
  };