@gtkx/config 2.0.0-beta.10 → 2.0.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +11 -169
  2. package/dist/builtin-elements.d.ts +11 -0
  3. package/dist/builtin-elements.d.ts.map +1 -0
  4. package/dist/builtin-elements.js +329 -0
  5. package/dist/builtin-elements.js.map +1 -0
  6. package/dist/config-dependencies.d.ts +5 -3
  7. package/dist/config-dependencies.d.ts.map +1 -1
  8. package/dist/config-dependencies.js +57 -24
  9. package/dist/config-dependencies.js.map +1 -1
  10. package/dist/config-error.d.ts.map +1 -1
  11. package/dist/config-error.js.map +1 -1
  12. package/dist/config-reloader.d.ts +8 -0
  13. package/dist/config-reloader.d.ts.map +1 -0
  14. package/dist/config-reloader.js +32 -0
  15. package/dist/config-reloader.js.map +1 -0
  16. package/dist/config-source.d.ts +9 -0
  17. package/dist/config-source.d.ts.map +1 -0
  18. package/dist/config-source.js +32 -0
  19. package/dist/config-source.js.map +1 -0
  20. package/dist/config.d.ts +63 -29
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +72 -12
  23. package/dist/config.js.map +1 -1
  24. package/dist/deploy.d.ts +67 -13
  25. package/dist/deploy.d.ts.map +1 -1
  26. package/dist/deploy.js +169 -12
  27. package/dist/deploy.js.map +1 -1
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal.d.ts +5 -3
  32. package/dist/internal.d.ts.map +1 -1
  33. package/dist/internal.js +5 -3
  34. package/dist/internal.js.map +1 -1
  35. package/dist/loader.d.ts +3 -1
  36. package/dist/loader.d.ts.map +1 -1
  37. package/dist/loader.js +18 -35
  38. package/dist/loader.js.map +1 -1
  39. package/dist/node-version.d.ts +2 -1
  40. package/dist/node-version.d.ts.map +1 -1
  41. package/dist/node-version.js +2 -1
  42. package/dist/node-version.js.map +1 -1
  43. package/dist/resource-base-path.d.ts.map +1 -1
  44. package/dist/schema-text.d.ts.map +1 -1
  45. package/dist/user-event-signals.d.ts.map +1 -1
  46. package/dist/virtual.d.ts +1 -1
  47. package/dist/virtual.d.ts.map +1 -1
  48. package/dist/virtual.js +3 -4
  49. package/dist/virtual.js.map +1 -1
  50. package/dist/vite-plugin.d.ts +2 -0
  51. package/dist/vite-plugin.d.ts.map +1 -1
  52. package/dist/vite-plugin.js +4 -3
  53. package/dist/vite-plugin.js.map +1 -1
  54. package/dist/vite-root.d.ts.map +1 -1
  55. package/package.json +15 -16
  56. package/src/builtin-elements.ts +338 -0
  57. package/src/config-dependencies.ts +65 -29
  58. package/src/config-error.ts +5 -3
  59. package/src/config-reloader.ts +45 -0
  60. package/src/config-source.ts +48 -0
  61. package/src/config.ts +87 -18
  62. package/src/deploy.ts +207 -29
  63. package/src/index.ts +9 -2
  64. package/src/internal.ts +5 -2
  65. package/src/loader.ts +27 -52
  66. package/src/node-version.ts +2 -1
  67. package/src/virtual.ts +5 -6
  68. package/src/vite-plugin.ts +9 -4
@@ -0,0 +1,338 @@
1
+ import type { ElementConfigOptions, ModuleExport } from "./config.ts";
2
+
3
+ const forTypes = (types: string[], config: ElementConfigOptions): Record<string, ElementConfigOptions> =>
4
+ Object.fromEntries(types.map((type) => [type, config]));
5
+
6
+ const internal = (name: string): ModuleExport => ({ module: "@gtkx/react/internal", export: name });
7
+
8
+ const SINGLE_CHILD_TYPES: string[] = [
9
+ "AdwBin",
10
+ "AdwClamp",
11
+ "AdwNavigationPage",
12
+ "AdwSplitButton",
13
+ "AdwStatusPage",
14
+ "AdwTabOverview",
15
+ "AdwToastOverlay",
16
+ "AdwToggle",
17
+ "GtkAspectFrame",
18
+ "GtkButton",
19
+ "GtkCheckButton",
20
+ "GtkDragIcon",
21
+ "GtkExpander",
22
+ "GtkFlowBoxChild",
23
+ "GtkFrame",
24
+ "GtkGraphicsOffload",
25
+ "GtkListBoxRow",
26
+ "GtkListHeader",
27
+ "GtkListItem",
28
+ "GtkMenuButton",
29
+ "GtkPopover",
30
+ "GtkPopoverBin",
31
+ "GtkRevealer",
32
+ "GtkScrolledWindow",
33
+ "GtkSearchBar",
34
+ "GtkTreeExpander",
35
+ "GtkViewport",
36
+ "GtkWindowHandle",
37
+ ];
38
+
39
+ const CONTENT_SETTER_TYPES: string[] = ["AdwBottomSheet", "AdwOverlaySplitView"];
40
+
41
+ /**
42
+ * Built-in GTK and Adwaita element metadata: base props, component wrappers, omitted properties,
43
+ * and parent-created objects. Contains no behaviors and can be imported without loading GI bindings.
44
+ */
45
+ const BUILTIN_ELEMENTS: Record<string, ElementConfigOptions> = {
46
+ ...forTypes(SINGLE_CHILD_TYPES, {
47
+ props: internal("ChildrenProps"),
48
+ omittedProps: ["child"],
49
+ }),
50
+ ...forTypes(["GtkHeaderBar", "GtkActionBar"], {
51
+ props: internal("GtkHeaderBarProps"),
52
+ }),
53
+ GtkWindow: {
54
+ props: internal("ChildrenProps"),
55
+ component: internal("createWindowComponent"),
56
+ omittedProps: ["child"],
57
+ },
58
+ GtkApplicationWindow: {
59
+ component: internal("createApplicationWindowComponent"),
60
+ },
61
+ GtkLabel: {
62
+ props: internal("ChildrenProps"),
63
+ },
64
+ GtkTextBuffer: {
65
+ props: internal("ChildrenProps"),
66
+ },
67
+ GtkTextTag: {
68
+ props: internal("ChildrenProps"),
69
+ },
70
+ GtkTextChildAnchor: {
71
+ props: { ...internal("GtkTextChildAnchorProps"), constructOnly: ["replacement"] },
72
+ },
73
+ GtkGridLayoutChild: {
74
+ isLazy: true,
75
+ },
76
+ GtkFixedLayoutChild: {
77
+ isLazy: true,
78
+ },
79
+ GtkOverlayLayoutChild: {
80
+ isLazy: true,
81
+ },
82
+ GtkStackPage: {
83
+ isLazy: true,
84
+ },
85
+ GtkNotebookPage: {
86
+ isLazy: true,
87
+ },
88
+ GActionGroup: {
89
+ props: internal("ActionGroupProps"),
90
+ },
91
+ GtkAccessible: {
92
+ props: { module: "@gtkx/react", export: "AccessibleProps" },
93
+ },
94
+ GtkWidget: {
95
+ props: { ...internal("GtkWidgetProps"), constructOnly: ["textChildAnchor"] },
96
+ },
97
+ GtkBox: {
98
+ props: internal("ChildrenProps"),
99
+ },
100
+ GtkListBox: {
101
+ props: internal("GtkListBoxProps"),
102
+ component: internal("createListBoxComponent"),
103
+ },
104
+ GtkFlowBox: {
105
+ props: internal("ChildrenProps"),
106
+ },
107
+ GtkOverlay: {
108
+ props: internal("GtkOverlayProps"),
109
+ omittedProps: ["child"],
110
+ },
111
+ GtkShortcutController: {
112
+ props: internal("GtkShortcutControllerProps"),
113
+ },
114
+ GtkCallbackAction: {
115
+ component: internal("createCallbackActionComponent"),
116
+ props: {
117
+ ...internal("GtkCallbackActionElementProps"),
118
+ composition: "factory",
119
+ constructOnly: ["callback"],
120
+ },
121
+ },
122
+ GtkCustomLayout: {
123
+ component: internal("createCustomLayoutComponent"),
124
+ props: {
125
+ ...internal("GtkCustomLayoutElementProps"),
126
+ composition: "factory",
127
+ constructOnly: ["requestMode", "measure", "allocate"],
128
+ },
129
+ },
130
+ GtkShortcutTrigger: {
131
+ component: internal("createShortcutTriggerComponent"),
132
+ props: {
133
+ ...internal("GtkShortcutTriggerElementProps"),
134
+ composition: "factory",
135
+ constructOnly: ["accelerator"],
136
+ },
137
+ },
138
+ GtkTextView: {
139
+ acceptedChildTypes: ["GtkTextBuffer", "GtkWidget"],
140
+ props: internal("ChildrenProps"),
141
+ },
142
+ GActionMap: {
143
+ props: internal("ActionMapProps"),
144
+ },
145
+ GMenu: {
146
+ acceptedChildTypes: ["GMenuItem"],
147
+ props: internal("MenuProps"),
148
+ component: internal("createMenuComponent"),
149
+ },
150
+ GMenuItem: {
151
+ component: internal("createMenuItemComponent"),
152
+ props: { ...internal("MenuItemProps"), constructOnly: ["label", "action"] },
153
+ },
154
+ GtkColumnView: {
155
+ acceptedChildTypes: ["GtkColumnViewColumn"],
156
+ props: internal("ChildrenProps"),
157
+ },
158
+ GtkGrid: {
159
+ props: internal("ChildrenProps"),
160
+ },
161
+ GtkFixed: {
162
+ props: internal("ChildrenProps"),
163
+ },
164
+ GtkSizeGroup: {
165
+ props: internal("GtkSizeGroupProps"),
166
+ component: internal("createPortaledComponent"),
167
+ },
168
+ GtkConstraintLayout: {
169
+ component: internal("createConstraintLayoutComponent"),
170
+ props: internal("GtkConstraintLayoutProps"),
171
+ },
172
+ GtkStack: {
173
+ props: internal("ChildrenProps"),
174
+ component: internal("createStackComponent"),
175
+ },
176
+ GtkNotebook: {
177
+ props: internal("ChildrenProps"),
178
+ },
179
+ GtkApplication: {
180
+ acceptedChildTypes: ["GtkWindow"],
181
+ props: { ...internal("GtkApplicationProps"), constructOnly: ["mainOptions"] },
182
+ component: internal("createApplicationComponent"),
183
+ },
184
+ GtkAboutDialog: {
185
+ props: { ...internal("GtkAboutDialogProps"), constructOnly: ["creditSections"] },
186
+ },
187
+ GtkScale: {
188
+ props: internal("GtkScaleProps"),
189
+ },
190
+ GtkCalendar: {
191
+ props: internal("GtkCalendarProps"),
192
+ },
193
+ GtkLevelBar: {
194
+ props: internal("GtkLevelBarProps"),
195
+ },
196
+ GtkDropTarget: {
197
+ props: internal("GtkDropTargetProps"),
198
+ },
199
+ GtkDrawingArea: {
200
+ props: internal("GtkDrawingAreaProps"),
201
+ },
202
+ GtkDragSource: {
203
+ props: internal("GtkDragSourceProps"),
204
+ },
205
+ ...forTypes(CONTENT_SETTER_TYPES, {
206
+ props: internal("ChildrenProps"),
207
+ omittedProps: ["content"],
208
+ }),
209
+ AdwClampScrollable: {
210
+ props: internal("ChildrenProps"),
211
+ omittedProps: ["child"],
212
+ },
213
+ AdwViewStackPage: {
214
+ isLazy: true,
215
+ },
216
+ AdwTabPage: {
217
+ isLazy: true,
218
+ props: { ...internal("AdwTabPageProps"), constructOnly: ["parent"] },
219
+ },
220
+ AdwTabBar: {
221
+ props: internal("AdwTabDropProps"),
222
+ },
223
+ AdwTabOverview: {
224
+ props: internal("AdwTabDropProps"),
225
+ omittedProps: ["child"],
226
+ },
227
+ AdwAboutDialog: {
228
+ props: {
229
+ ...internal("AdwAboutDialogProps"),
230
+ constructOnly: ["links", "creditSections", "acknowledgementSections", "legalSections", "otherApps"],
231
+ },
232
+ },
233
+ AdwLayout: {
234
+ isLazy: true,
235
+ },
236
+ AdwSidebar: {
237
+ acceptedChildTypes: ["AdwSidebarSection"],
238
+ props: internal("ChildrenProps"),
239
+ },
240
+ AdwSidebarSection: {
241
+ acceptedChildTypes: ["AdwSidebarItem"],
242
+ props: internal("ChildrenProps"),
243
+ },
244
+ AdwMultiLayoutView: {
245
+ props: internal("AdwMultiLayoutViewProps"),
246
+ component: internal("createMultiLayoutViewComponent"),
247
+ },
248
+ AdwPreferencesRow: {
249
+ props: internal("AdwPreferencesRowProps"),
250
+ },
251
+ AdwDialog: {
252
+ props: internal("AdwBreakpointsProps"),
253
+ component: internal("createDialogComponent"),
254
+ omittedProps: ["child"],
255
+ },
256
+ AdwApplicationWindow: {
257
+ props: internal("AdwBreakpointsProps"),
258
+ omittedProps: ["content"],
259
+ },
260
+ AdwWindow: {
261
+ props: internal("AdwBreakpointsProps"),
262
+ omittedProps: ["content"],
263
+ },
264
+ AdwBreakpointBin: {
265
+ props: internal("AdwBreakpointsProps"),
266
+ omittedProps: ["child"],
267
+ },
268
+ AdwActionRow: {
269
+ props: internal("AdwPreferencesRowProps"),
270
+ },
271
+ AdwEntryRow: {
272
+ props: internal("AdwPreferencesRowProps"),
273
+ },
274
+ AdwExpanderRow: {
275
+ props: internal("AdwExpanderRowProps"),
276
+ },
277
+ AdwNavigationSplitView: {
278
+ acceptedChildTypes: ["AdwNavigationPage"],
279
+ props: internal("ChildrenProps"),
280
+ omittedProps: ["content"],
281
+ },
282
+ AdwWrapBox: {
283
+ props: internal("ChildrenProps"),
284
+ },
285
+ AdwCarousel: {
286
+ props: internal("ChildrenProps"),
287
+ },
288
+ AdwPreferencesPage: {
289
+ acceptedChildTypes: ["AdwPreferencesGroup"],
290
+ props: internal("ChildrenProps"),
291
+ },
292
+ AdwPreferencesDialog: {
293
+ acceptedChildTypes: ["AdwPreferencesPage"],
294
+ props: internal("ChildrenProps"),
295
+ },
296
+ AdwPreferencesGroup: {
297
+ props: internal("ChildrenProps"),
298
+ },
299
+ AdwTabView: {
300
+ props: internal("ChildrenProps"),
301
+ },
302
+ AdwNavigationView: {
303
+ acceptedChildTypes: ["AdwNavigationPage"],
304
+ props: internal("ChildrenProps"),
305
+ },
306
+ AdwViewStack: {
307
+ props: internal("ChildrenProps"),
308
+ component: internal("createViewStackComponent"),
309
+ },
310
+ AdwToolbarView: {
311
+ props: internal("AdwToolbarViewProps"),
312
+ omittedProps: ["content"],
313
+ },
314
+ AdwHeaderBar: {
315
+ props: internal("GtkHeaderBarProps"),
316
+ },
317
+ AdwShortcutsDialog: {
318
+ acceptedChildTypes: ["AdwShortcutsSection"],
319
+ props: internal("ChildrenProps"),
320
+ },
321
+ AdwShortcutsSection: {
322
+ acceptedChildTypes: ["AdwShortcutsItem"],
323
+ props: internal("ChildrenProps"),
324
+ },
325
+ AdwToggleGroup: {
326
+ props: { ...internal("AdwToggleGroupProps"), composition: "intersection" },
327
+ acceptedChildTypes: ["AdwToggle"],
328
+ component: internal("createToggleGroupComponent"),
329
+ },
330
+ AdwAlertDialog: {
331
+ props: internal("AdwAlertDialogProps"),
332
+ omittedProps: ["extraChild"],
333
+ },
334
+ };
335
+
336
+ export { SINGLE_CHILD_TYPES, CONTENT_SETTER_TYPES, BUILTIN_ELEMENTS };
337
+
338
+ export type { ElementConfigOptions, ElementPropsExport, ModuleExport } from "./config.ts";
@@ -1,15 +1,17 @@
1
- import { createJiti, type Jiti, type TransformOptions, type TransformResult } from "jiti";
1
+ import { createJiti, type Jiti } from "jiti";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { createRequire, registerHooks } from "node:module";
3
+ import { registerHooks } from "node:module";
4
4
  import { extname, sep } from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { isRecord } from "./config-error.ts";
7
+ import { configSourceCandidates } from "./config-source.ts";
6
8
 
7
- type CaptureState = { cacheKey: string; dependencies: Set<string>; transformer: Jiti };
9
+ type CaptureState = { cacheKey: string; dependencies: Set<string>; resolver: Jiti };
8
10
  type CapturedConfig<T> = { dependencies: string[]; value: T };
9
11
 
10
12
  const captureStorage: AsyncLocalStorage<CaptureState> = new AsyncLocalStorage();
11
13
  const dependenciesByValue: WeakMap<object, string[]> = new WeakMap();
12
- const CACHE_BUSTED_EXTENSIONS: ReadonlySet<string> = new Set([".cjs", ".js", ".json", ".mjs"]);
14
+ const CACHE_BUSTED_EXTENSIONS: ReadonlySet<string> = new Set([".js", ".json", ".mjs", ".mts", ".ts"]);
13
15
 
14
16
  const isTrackedPath = (path: string): boolean => !path.includes(`${sep}node_modules${sep}`);
15
17
 
@@ -25,8 +27,22 @@ const addUrl = (url: string): void => {
25
27
  }
26
28
  };
27
29
 
30
+ const addConfigSource = (path: string): void => {
31
+ for (const candidate of configSourceCandidates(path)) {
32
+ addPath(candidate);
33
+ }
34
+ };
35
+
36
+ const addFailedSource = (error: unknown): void => {
37
+ if (isRecord(error) && typeof error.url === "string" && error.url.startsWith("file:")) {
38
+ addConfigSource(fileURLToPath(error.url));
39
+ }
40
+ };
41
+
28
42
  const cacheBustedUrl = (url: string): string => {
29
- if (!url.startsWith("file:")) {
43
+ const state = captureStorage.getStore();
44
+
45
+ if (state === undefined || !url.startsWith("file:")) {
30
46
  return url;
31
47
  }
32
48
 
@@ -37,7 +53,7 @@ const cacheBustedUrl = (url: string): string => {
37
53
  }
38
54
 
39
55
  const parsed = new URL(url);
40
- parsed.searchParams.set("gtkx-config-load", captureStorage.getStore()?.cacheKey ?? "uncaptured");
56
+ parsed.searchParams.set("gtkx-config-load", state.cacheKey);
41
57
 
42
58
  return parsed.href;
43
59
  };
@@ -45,49 +61,61 @@ const cacheBustedUrl = (url: string): string => {
45
61
  const registerResolutionHook = (): ReturnType<typeof registerHooks> =>
46
62
  registerHooks({
47
63
  resolve(specifier, context, nextResolve) {
48
- const result = nextResolve(specifier, context);
64
+ let result;
65
+
66
+ try {
67
+ result = nextResolve(specifier, context);
68
+ } catch (error) {
69
+ const state = captureStorage.getStore();
70
+ const fallback = captureStorage.exit(() =>
71
+ state?.resolver.esmResolve(specifier, {
72
+ ...(context.parentURL !== undefined && { parentURL: context.parentURL }),
73
+ try: true,
74
+ }),
75
+ );
76
+
77
+ if (fallback === undefined) {
78
+ addFailedSource(error);
79
+ throw error;
80
+ }
81
+
82
+ result = nextResolve(fallback, context);
83
+ }
84
+
49
85
  addUrl(result.url);
50
86
 
51
87
  return { ...result, url: cacheBustedUrl(result.url) };
52
88
  },
53
89
  });
54
90
 
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");
91
+ const importConfigModule: Jiti["import"] = async <T>(path: string, options?: { default?: true }): Promise<T> => {
92
+ if ([".cjs", ".cts"].includes(extname(path))) {
93
+ throw new Error(`Configuration modules must use ESM: ${path}`);
68
94
  }
69
95
 
70
- if (options.filename !== undefined) {
71
- addPath(options.filename);
72
- }
96
+ const value = (await import(pathToFileURL(path).href, {
97
+ ...(extname(path) === ".json" && { with: { type: "json" } }),
98
+ })) as Record<string, unknown>;
73
99
 
74
- return { code: state.transformer.transform(options) };
100
+ return (options?.default ? (value.default ?? { ...value }) : value) as T;
75
101
  };
76
102
 
103
+ const createConfigImporter = (): Jiti =>
104
+ Object.assign(createJiti(import.meta.url, { fsCache: false, moduleCache: false }), { import: importConfigModule });
105
+
77
106
  const setConfigDependencies = (value: object, dependencies: Iterable<string>): void => {
78
107
  const paths = [...new Set(dependencies)];
79
108
  dependenciesByValue.set(value, paths);
80
- clearNativeModuleCache(paths);
81
109
  };
82
110
 
83
111
  const configDependenciesFor = (value: unknown): string[] =>
84
- typeof value === "object" && value !== null ? dependenciesByValue.get(value) ?? [] : [];
112
+ typeof value === "object" && value !== null ? (dependenciesByValue.get(value) ?? []) : [];
85
113
 
86
114
  const captureConfigDependencies = async <T>(operation: () => Promise<T>): Promise<CapturedConfig<T>> => {
87
115
  const state: CaptureState = {
88
116
  cacheKey: process.hrtime.bigint().toString(),
89
117
  dependencies: new Set(),
90
- transformer: createJiti(import.meta.url, { fsCache: false, moduleCache: false }),
118
+ resolver: createJiti(import.meta.url, { fsCache: false, moduleCache: false }),
91
119
  };
92
120
  const hook = registerResolutionHook();
93
121
 
@@ -106,9 +134,17 @@ const captureConfigDependencies = async <T>(operation: () => Promise<T>): Promis
106
134
  }
107
135
  };
108
136
 
137
+ const loadConfigModule = async (path: string): Promise<unknown> => {
138
+ const captured = await captureConfigDependencies(() => importConfigModule(path, { default: true }));
139
+
140
+ return captured.value;
141
+ };
142
+
109
143
  export {
144
+ addConfigSource,
110
145
  captureConfigDependencies,
111
146
  configDependenciesFor,
147
+ createConfigImporter,
148
+ loadConfigModule,
112
149
  setConfigDependencies,
113
- transformConfigModule,
114
150
  };
@@ -8,9 +8,11 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
8
8
  const configPrefix = (configFile: string): string => `${configFile}:`;
9
9
 
10
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}`);
11
+ new Error(
12
+ configFile === undefined
13
+ ? `${configPrefix(DEFAULT_CONFIG_FILE)} no configuration file found in ${cwd}`
14
+ : `${configPrefix(configFile)} no configuration file found in ${cwd}`,
15
+ );
14
16
 
15
17
  const configError = (error: z.ZodError, configFile = DEFAULT_CONFIG_FILE): Error =>
16
18
  new Error(`${configPrefix(configFile)}\n${z.prettifyError(error)}`);
@@ -0,0 +1,45 @@
1
+ import { resolve } from "node:path";
2
+ import { configDependenciesFor } from "./config-dependencies.ts";
3
+ import { loadConfig, type LoadConfigOptions, type LoadedConfig } from "./loader.ts";
4
+
5
+ type ConfigReloader = {
6
+ reload: () => Promise<LoadedConfig>;
7
+ resolvePaths: () => string[];
8
+ };
9
+
10
+ const createConfigReloader = async (
11
+ cwd: string,
12
+ options: LoadConfigOptions = {},
13
+ initialDependencies: string[] = [],
14
+ ): Promise<ConfigReloader> => {
15
+ let selectedConfigFile: string;
16
+ let knownDependencies = initialDependencies;
17
+
18
+ if (options.configFile === undefined) {
19
+ const loaded = await loadConfig(cwd, options);
20
+ selectedConfigFile = loaded.configFile;
21
+ knownDependencies = [...knownDependencies, ...configDependenciesFor(loaded)];
22
+ } else {
23
+ selectedConfigFile = resolve(cwd, options.configFile);
24
+ }
25
+
26
+ return {
27
+ resolvePaths: () => [...new Set([selectedConfigFile, ...knownDependencies])],
28
+ reload: async () => {
29
+ let loaded: LoadedConfig;
30
+
31
+ try {
32
+ loaded = await loadConfig(cwd, { ...options, configFile: selectedConfigFile });
33
+ } catch (error) {
34
+ knownDependencies = [...new Set([...knownDependencies, ...configDependenciesFor(error)])];
35
+ throw error;
36
+ }
37
+
38
+ knownDependencies = configDependenciesFor(loaded);
39
+
40
+ return loaded;
41
+ },
42
+ };
43
+ };
44
+
45
+ export { createConfigReloader };
@@ -0,0 +1,48 @@
1
+ import { SUPPORTED_EXTENSIONS } from "c12";
2
+ import { basename, extname, isAbsolute, join, resolve } from "node:path";
3
+
4
+ type ConfigResolutionOptions = { configFile?: string | undefined; cwd?: string | undefined };
5
+
6
+ const CONFIG_LOCATIONS = ["gtkx.config", ".config/gtkx", ".config/gtkx.config"];
7
+ const CONFIG_EXTENSIONS = SUPPORTED_EXTENSIONS.filter((extension) => extension !== ".cjs" && extension !== ".cts");
8
+
9
+ const configCandidatePaths = (directory: string): string[] =>
10
+ CONFIG_LOCATIONS.flatMap((location) =>
11
+ CONFIG_EXTENSIONS.map((extension) => join(directory, `${location}${extension}`)),
12
+ );
13
+
14
+ const configSourceCandidates = (path: string): string[] => {
15
+ const extension = extname(path);
16
+ const fallback = extension === ".js" ? ".ts" : extension === ".mjs" ? ".mts" : undefined;
17
+
18
+ if (extension.length > 0) {
19
+ return fallback === undefined ? [path] : [path, `${path.slice(0, -extension.length)}${fallback}`];
20
+ }
21
+
22
+ return [
23
+ path,
24
+ ...CONFIG_EXTENSIONS.flatMap((candidate) => [`${path}${candidate}`, join(path, `index${candidate}`)]),
25
+ ];
26
+ };
27
+
28
+ const isLocalConfigSource = (source: string): boolean => source.startsWith(".") || isAbsolute(source);
29
+
30
+ const isDirectoryConfigSource = (source: string): boolean => {
31
+ const extension = extname(source);
32
+
33
+ return extension.length === 0 || extension === basename(source);
34
+ };
35
+
36
+ const localConfigSourcePath = (source: string, options: ConfigResolutionOptions): string | undefined => {
37
+ if (source === "." || !isLocalConfigSource(source)) {
38
+ return undefined;
39
+ }
40
+
41
+ const cwd = options.cwd ?? process.cwd();
42
+
43
+ return isDirectoryConfigSource(source)
44
+ ? resolve(cwd, source, options.configFile ?? "gtkx.config")
45
+ : resolve(cwd, source);
46
+ };
47
+
48
+ export { configCandidatePaths, configSourceCandidates, localConfigSourcePath, type ConfigResolutionOptions };