@gtkx/config 1.5.0 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts CHANGED
@@ -2,22 +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 { isGirLibrary, text } from "./schema-text.ts";
7
+ import { girLibrary, text } from "./schema-text.ts";
8
8
  import { resolveUserEventSignals } from "./user-event-signals.ts";
9
9
 
10
- /** Accepted `reactCompiler.compilationMode` values, choosing which functions the compiler processes. */
11
- type ReactCompilerCompilationMode = (typeof COMPILATION_MODES)[number];
12
- /** Accepted `reactCompiler.panicThreshold` values, choosing which compiler diagnostics fail the build. */
13
- type ReactCompilerPanicThreshold = (typeof PANIC_THRESHOLDS)[number];
14
-
15
- /** 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`. */
16
11
  type ReactCompilerOptions = {
17
- /** Which functions the compiler processes; left to the compiler's own default when omitted. */
18
- compilationMode?: ReactCompilerCompilationMode;
19
- /** Which compiler diagnostics fail the build; left to the compiler's own default when omitted. */
20
- 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];
21
16
  };
22
17
 
23
18
  /**
@@ -32,13 +27,18 @@ type ResolvedReactCompilerOptions = ReactCompilerOptions & {
32
27
  /**
33
28
  * User-facing configuration for a GTKX project, as authored in `gtkx.config.ts`: the GIR libraries
34
29
  * to bind and where to find them, the GApplication id, per-element configuration, the React
35
- * Compiler, codegen, and user event signal settings, and the `future` block opting into behavior
36
- * that becomes the default in the next major version.
30
+ * Compiler, codegen, and user event signal settings, and the `agents` and `mcp` blocks controlling
31
+ * what coding agents are given.
37
32
  */
38
33
  type Config = z.infer<typeof configSchema>;
39
34
  type ModuleExport = z.infer<typeof moduleExportSchema>;
40
35
  type ElementConfigEntry = z.infer<typeof elementConfigSchema>;
41
36
 
37
+ type McpSettings = {
38
+ tools: string[];
39
+ isReadOnly: boolean;
40
+ };
41
+
42
42
  /** Configuration reduced to the values the app runtime and the build need, with paths already resolved. */
43
43
  type ResolvedConfig = {
44
44
  /** The GApplication identifier the app registers under. */
@@ -56,92 +56,35 @@ type ResolvedConfig = {
56
56
  lazyElements: string[];
57
57
  };
58
58
 
59
- const LIBRARIES_WILDCARD = "*";
60
59
  const APPLICATION_ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)+$/;
61
60
  const APPLICATION_ID_MAX_LENGTH = 255;
61
+ const DEFAULT_LIBRARIES: Set<string> = new Set(["Gtk-4.0", "Adw-1"]);
62
62
  /** Compilation modes `babel-plugin-react-compiler` accepts. */
63
63
  const COMPILATION_MODES = ["infer", "syntax", "annotation", "all"] as const;
64
64
  /** Panic thresholds `babel-plugin-react-compiler` accepts. */
65
65
  const PANIC_THRESHOLDS = ["none", "critical_errors", "all_errors"] as const;
66
66
  const REACT_COMPILER_TARGET = "19";
67
- const COMPILATION_MODE_SET: Set<string> = new Set(COMPILATION_MODES);
68
- const PANIC_THRESHOLD_SET: Set<string> = new Set(PANIC_THRESHOLDS);
69
67
 
70
- const librariesSchema = z.custom<typeof LIBRARIES_WILDCARD | string[]>().check((ctx) => {
71
- const value = ctx.value;
68
+ const librarySchema = girLibrary('must be of the form "Name-Version", such as "Gtk-4.0"')
69
+ .refine((library) => !DEFAULT_LIBRARIES.has(library), { error: "is bound by default; remove it" });
72
70
 
73
- if (value === LIBRARIES_WILDCARD) {
74
- return;
75
- }
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" });
76
74
 
77
- if (!Array.isArray(value) || value.length === 0) {
78
- ctx.issues.push(rawIssue(value, [], `must be "${LIBRARIES_WILDCARD}", a non-empty string array, or omitted`));
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
+ });
79
80
 
80
- return;
81
- }
82
-
83
- for (const [index, entry] of value.entries()) {
84
- ctx.issues.push(...libraryEntryIssues(value, index, entry));
85
- }
86
- });
87
-
88
- const applicationIdSchema = z.custom<string>().check((ctx) => {
89
- const value = ctx.value;
90
-
91
- if (typeof value !== "string" || !isValidApplicationId(value)) {
92
- ctx.issues.push(
93
- rawIssue(
94
- value,
95
- [],
96
- `invalid \`applicationId\` "${value}", must satisfy g_application_id_is_valid ` +
97
- '(e.g. "org.example.MyApp")',
98
- true,
99
- ),
100
- );
101
- }
102
- });
103
-
104
- const reactCompilerSchema = z.custom<boolean | ReactCompilerOptions>().check((ctx) => {
105
- const value = ctx.value;
106
-
107
- if (typeof value === "boolean") {
108
- return;
109
- }
110
-
111
- if (!isRecord(value)) {
112
- ctx.issues.push(rawIssue(value, [], "must be a boolean or an options object"));
113
-
114
- return;
115
- }
116
-
117
- const compilationMode = value.compilationMode;
118
-
119
- if (!isValidReactCompilerOption(compilationMode, COMPILATION_MODE_SET)) {
120
- ctx.issues.push(
121
- rawIssue(
122
- value,
123
- [],
124
- `invalid \`reactCompiler.compilationMode\` "${String(compilationMode)}", ` +
125
- `must be one of ${COMPILATION_MODES.join(", ")}`,
126
- true,
127
- ),
128
- );
129
- }
130
-
131
- const panicThreshold = value.panicThreshold;
132
-
133
- if (!isValidReactCompilerOption(panicThreshold, PANIC_THRESHOLD_SET)) {
134
- ctx.issues.push(
135
- rawIssue(
136
- value,
137
- [],
138
- `invalid \`reactCompiler.panicThreshold\` "${String(panicThreshold)}", ` +
139
- `must be one of ${PANIC_THRESHOLDS.join(", ")}`,
140
- true,
141
- ),
142
- );
143
- }
144
- });
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
+ ]);
145
88
 
146
89
  const userEventSignalsSchema = z.record(
147
90
  z.string(),
@@ -184,17 +127,42 @@ const elementsSchema = z.object({
184
127
  config: z.record(z.string(), elementConfigSchema).optional(),
185
128
  });
186
129
 
187
- const futureSchema = z.object({
188
- v2ByteArrays: z.boolean({ error: "must be a boolean" }).optional(),
189
- v2ValueReturns: z.boolean({ error: "must be a boolean" }).optional(),
190
- v2FinishResults: z.boolean({ error: "must be a boolean" }).optional(),
191
- v2InoutReturns: z.boolean({ error: "must be a boolean" }).optional(),
192
- v2ResourceImports: z.boolean({ error: "must be a boolean" }).optional(),
130
+ const agentsSchema = z.object({
131
+ rules: z.boolean({ error: "must be a boolean" }).optional(),
132
+ reference: z.boolean({ error: "must be a boolean" }).optional(),
133
+ });
134
+
135
+ const mcpSchema = z.object({
136
+ tools: z
137
+ .array(z.string({ error: "must be a tool name pattern" }).min(1, { error: "must be a tool name pattern" }), {
138
+ error: "must be an array of tool name patterns",
139
+ })
140
+ .optional(),
141
+ readOnly: z.boolean({ error: "must be a boolean" }).optional(),
142
+ });
143
+
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();
155
+
156
+ const deprecationsSchema = z.object({
157
+ silence: z
158
+ .array(z.never({ error: "does not name a current deprecation" }), {
159
+ error: "must be an array of current deprecation ids",
160
+ })
161
+ .optional(),
193
162
  });
194
163
 
195
164
  /** Schema every `gtkx.config.ts` is validated against, and the source of the {@link Config} type. */
196
165
  const configSchema = z.object({
197
- future: futureSchema.optional(),
198
166
  libraries: librariesSchema.optional(),
199
167
  girPath: z.array(z.string(), { error: "must be an array of strings if provided" }).optional(),
200
168
  applicationId: applicationIdSchema,
@@ -204,6 +172,9 @@ const configSchema = z.object({
204
172
  elements: elementsSchema.optional(),
205
173
  applicationIcon: text("must be a path to an icon theme directory or a single icon file").optional(),
206
174
  deploy: deploySchema.optional(),
175
+ agents: agentsSchema.optional(),
176
+ mcp: mcpSchema.optional(),
177
+ deprecations: deprecationsSchema.optional(),
207
178
  });
208
179
 
209
180
  /**
@@ -211,34 +182,10 @@ const configSchema = z.object({
211
182
  * autocompletion and type checking.
212
183
  */
213
184
  const defineConfig: DefineConfig<Config> = createDefineConfig<Config>();
185
+ const validationSchema = configSchema.extend({ future: graduatedFutureSchema.optional() });
214
186
 
215
- const libraryEntryIssues = (value: unknown[], index: number, entry: unknown): ReturnType<typeof rawIssue>[] => {
216
- if (typeof entry === "string" && isGirLibrary(entry)) {
217
- return [];
218
- }
219
-
220
- if (entry === LIBRARIES_WILDCARD) {
221
- const message =
222
- `to generate every library, set \`libraries: "${LIBRARIES_WILDCARD}"\` as a bare string, ` +
223
- "not an array entry";
224
-
225
- return [rawIssue(value, [index], message, true)];
226
- }
227
-
228
- const message =
229
- `invalid library identifier "${String(entry)}", must be of the form "Name-Version" ` +
230
- '(e.g. "Gtk-4.0")';
231
-
232
- return [rawIssue(value, [index], message, true)];
233
- };
234
-
235
- const isValidApplicationId = (applicationId: string): boolean => {
236
- if (applicationId.length === 0 || applicationId.length > APPLICATION_ID_MAX_LENGTH) {
237
- return false;
238
- }
239
-
240
- return APPLICATION_ID_PATTERN.test(applicationId);
241
- };
187
+ const isValidApplicationId = (applicationId: string): boolean =>
188
+ applicationId.length <= APPLICATION_ID_MAX_LENGTH && APPLICATION_ID_PATTERN.test(applicationId);
242
189
 
243
190
  const resolveReactCompilerOptions = (setting: Config["reactCompiler"]): ResolvedReactCompilerOptions | null => {
244
191
  if (setting === false) {
@@ -247,22 +194,31 @@ const resolveReactCompilerOptions = (setting: Config["reactCompiler"]): Resolved
247
194
 
248
195
  const overrides = setting === undefined || setting === true ? {} : setting;
249
196
 
250
- 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
+ };
251
202
  };
252
203
 
253
- const isValidReactCompilerOption = (value: unknown, allowed: Set<string>): boolean =>
254
- value === undefined || (typeof value === "string" && allowed.has(value));
255
-
256
- const validateConfig = (config: Config): void => {
257
- const result = configSchema.safeParse(config);
204
+ const validateConfig = (config: unknown): void => {
205
+ const result = validationSchema.safeParse(config);
258
206
 
259
207
  if (!result.success) {
260
208
  throw configError(result.error);
261
209
  }
262
210
  };
263
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
+
264
220
  /**
265
- * Deep-merges two configurations. `override` wins over `base` on conflicting scalar and object keys, while
221
+ * Deep-merges a configuration over a base. `override` wins over `base` on conflicting scalar and object keys, while
266
222
  * arrays are concatenated with the `override` entries first.
267
223
  */
268
224
  const mergeConfig = (base: Config, override: Config): Config => defu(override, base);
@@ -301,6 +257,14 @@ const resolveElementProps = (elements: Config["elements"]): Record<string, Modul
301
257
  const resolveOmittedProps = (elements: Config["elements"]): Record<string, string[]> =>
302
258
  elementEntryValues(elements, (entry) => entry.omittedProps);
303
259
 
260
+ const isAgentRulesEnabled = (config: Config): boolean => config.agents?.rules !== false;
261
+ const isAgentReferenceEnabled = (config: Config): boolean => config.agents?.reference !== false;
262
+
263
+ const resolveMcpSettings = (config: Config): McpSettings => ({
264
+ tools: config.mcp?.tools ?? [],
265
+ isReadOnly: config.mcp?.readOnly === true,
266
+ });
267
+
304
268
  const resolveConfig = (config: Config, root?: string): ResolvedConfig => ({
305
269
  applicationId: config.applicationId,
306
270
  reactCompiler: resolveReactCompilerOptions(config.reactCompiler),
@@ -312,14 +276,19 @@ const resolveConfig = (config: Config, root?: string): ResolvedConfig => ({
312
276
  export {
313
277
  APPLICATION_ID_MAX_LENGTH,
314
278
  defineConfig,
279
+ isAgentReferenceEnabled,
280
+ isAgentRulesEnabled,
315
281
  isValidApplicationId,
282
+ graduatedFutureKeys,
316
283
  validateConfig,
317
284
  mergeConfig,
318
285
  resolveLazyElements,
319
286
  resolveElementComponents,
320
287
  resolveElementProps,
288
+ resolveMcpSettings,
321
289
  resolveOmittedProps,
322
290
  resolveConfig,
291
+ type McpSettings,
323
292
  type ResolvedReactCompilerOptions,
324
293
  type Config,
325
294
  type ResolvedConfig,
package/src/deploy.ts CHANGED
@@ -119,7 +119,7 @@ const extraFileEntrySchema = z.union([text(SOURCE_PATH_ERROR), extraFileSchema],
119
119
 
120
120
  const nodeRuntimeSchema = z.strictObject({
121
121
  source: z.enum(NODE_SOURCES, { error: "must be one of download, host, path" }).optional(),
122
- version: text("must be a Node.js version such as 24.19.0").optional(),
122
+ version: text("must be a Node.js version such as 26.7.0").optional(),
123
123
  path: text("must be a path to a node binary").optional(),
124
124
  shouldStrip: flag(BOOLEAN_ERROR).optional(),
125
125
  shouldUseCompileCache: flag(BOOLEAN_ERROR).optional(),
package/src/index.ts CHANGED
@@ -1,4 +1,2 @@
1
- /** @public */
2
1
  export { type Config, defineConfig, mergeConfig, type ResolvedConfig } from "./config.ts";
3
- /** @public */
4
2
  export { type ConfigLoader, type LoadedConfig, loadConfig } from "./loader.ts";
package/src/internal.ts CHANGED
@@ -1,10 +1,13 @@
1
- export type { ResolvedReactCompilerOptions } from "./config.ts";
1
+ export type { McpSettings, ResolvedReactCompilerOptions } from "./config.ts";
2
2
  export {
3
3
  APPLICATION_ID_MAX_LENGTH,
4
+ isAgentReferenceEnabled,
5
+ isAgentRulesEnabled,
4
6
  isValidApplicationId,
5
7
  resolveElementComponents,
6
8
  resolveElementProps,
7
9
  resolveLazyElements,
10
+ resolveMcpSettings,
8
11
  resolveOmittedProps,
9
12
  } from "./config.ts";
10
13
  export { createConfigLoader } from "./loader.ts";
package/src/loader.ts CHANGED
@@ -1,9 +1,15 @@
1
- import { getOrInsert } from "@gtkx/utils";
1
+ import { warn } from "@gtkx/utils";
2
2
  import { loadConfig as loadConfigFile } from "c12";
3
3
  import { existsSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
  import { missingConfigFileError } from "./config-error.ts";
6
- import { type Config, resolveConfig, type ResolvedConfig, validateConfig } from "./config.ts";
6
+ import {
7
+ type Config,
8
+ graduatedFutureKeys,
9
+ resolveConfig,
10
+ type ResolvedConfig,
11
+ validateConfig,
12
+ } from "./config.ts";
7
13
 
8
14
  /** Result of loading a project's `gtkx.config.ts` file. */
9
15
  type LoadedConfig = {
@@ -32,6 +38,26 @@ type ConfigLoader = {
32
38
  resolve: (cwd: string) => Promise<ResolvedConfig>;
33
39
  };
34
40
 
41
+ const GRADUATED_FUTURE_ENV = "GTKX_GRADUATED_FUTURE_SHOWN";
42
+ const graduatedFutureWarnings: Map<string, string> = new Map();
43
+
44
+ const warnGraduatedFuture = (config: unknown, root: string): void => {
45
+ const keys = graduatedFutureKeys(config);
46
+ const signature = keys.join(",");
47
+
48
+ if (
49
+ keys.length === 0 ||
50
+ graduatedFutureWarnings.get(root) === signature ||
51
+ process.env[GRADUATED_FUTURE_ENV] === signature
52
+ ) {
53
+ return;
54
+ }
55
+
56
+ graduatedFutureWarnings.set(root, signature);
57
+ process.env[GRADUATED_FUTURE_ENV] = signature;
58
+ warn(`GTKX 2 ignores graduated future flags: ${keys.join(", ")}. Remove them from gtkx.config.ts.`);
59
+ };
60
+
35
61
  /**
36
62
  * Loads and validates the `gtkx.config.ts` file for a project.
37
63
  * @param cwd Directory the configuration file is looked up in; parent directories are not searched.
@@ -58,12 +84,14 @@ const loadConfig = async (cwd: string, options: LoadConfigOptions = {}): Promise
58
84
  }
59
85
 
60
86
  const config = result.config;
87
+ const root = result.cwd ?? searched;
61
88
  validateConfig(config);
89
+ warnGraduatedFuture(config, root);
62
90
 
63
91
  return {
64
92
  config,
65
93
  configFile,
66
- root: result.cwd ?? searched,
94
+ root,
67
95
  };
68
96
  };
69
97
 
@@ -72,7 +100,7 @@ const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
72
100
  const resolved: Map<string, Promise<ResolvedConfig>> = new Map();
73
101
 
74
102
  const load = (cwd: string): Promise<LoadedConfig> =>
75
- getOrInsert(loaded, resolve(cwd), (root) => loadConfig(root, options));
103
+ loaded.getOrInsertComputed(resolve(cwd), (root) => loadConfig(root, options));
76
104
 
77
105
  const resolveAt = async (root: string): Promise<ResolvedConfig> => {
78
106
  const { config, root: configRoot } = await load(root);
@@ -82,7 +110,7 @@ const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
82
110
 
83
111
  return {
84
112
  load,
85
- resolve: (cwd: string): Promise<ResolvedConfig> => getOrInsert(resolved, resolve(cwd), resolveAt),
113
+ resolve: (cwd: string): Promise<ResolvedConfig> => resolved.getOrInsertComputed(resolve(cwd), resolveAt),
86
114
  };
87
115
  };
88
116
 
@@ -40,4 +40,4 @@ const relativePathRecord = <Value extends z.ZodType>(
40
40
  recordMessage: string,
41
41
  ): z.ZodRecord<z.ZodString, Value> => z.record(relativePath(keyMessage), value, { error: recordMessage });
42
42
 
43
- export { fileExtension, flag, girLibrary, isGirLibrary, relativePathRecord, text, textList, textRecord, url };
43
+ export { fileExtension, flag, girLibrary, relativePathRecord, text, textList, textRecord, url };
package/src/virtual.ts CHANGED
@@ -5,11 +5,8 @@ const GTKX_CONFIG_VIRTUAL_ID = "virtual:gtkx-config";
5
5
  const RESOLVED_GTKX_CONFIG_VIRTUAL_ID = `\0${GTKX_CONFIG_VIRTUAL_ID}`;
6
6
  const METADATA_SPECIFIER = "@gtkx/jsx/metadata";
7
7
 
8
- const lazyElementConfig = (lazyElements: string[]): Record<string, { isLazy: boolean }> =>
9
- Object.fromEntries(lazyElements.map((type) => [type, { isLazy: true }]));
10
-
11
8
  const renderConfigModule = (config: ResolvedConfig): string => {
12
- const lazyJson = JSON.stringify(lazyElementConfig(config.lazyElements));
9
+ const lazyJson = JSON.stringify(Object.fromEntries(config.lazyElements.map((type) => [type, { isLazy: true }])));
13
10
 
14
11
  const behaviorImports =
15
12
  config.elements === null