@gtkx/config 2.0.0-beta.10 → 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 (53) hide show
  1. package/README.md +11 -169
  2. package/dist/config-dependencies.d.ts.map +1 -1
  3. package/dist/config-dependencies.js +3 -2
  4. package/dist/config-dependencies.js.map +1 -1
  5. package/dist/config-reloader.d.ts +8 -0
  6. package/dist/config-reloader.d.ts.map +1 -0
  7. package/dist/config-reloader.js +38 -0
  8. package/dist/config-reloader.js.map +1 -0
  9. package/dist/config-source.d.ts +7 -0
  10. package/dist/config-source.d.ts.map +1 -0
  11. package/dist/config-source.js +17 -0
  12. package/dist/config-source.js.map +1 -0
  13. package/dist/config.d.ts +32 -22
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +16 -10
  16. package/dist/config.js.map +1 -1
  17. package/dist/deploy.d.ts +3 -7
  18. package/dist/deploy.d.ts.map +1 -1
  19. package/dist/deploy.js +1 -2
  20. package/dist/deploy.js.map +1 -1
  21. package/dist/internal.d.ts +4 -2
  22. package/dist/internal.d.ts.map +1 -1
  23. package/dist/internal.js +4 -2
  24. package/dist/internal.js.map +1 -1
  25. package/dist/loader.d.ts +1 -1
  26. package/dist/loader.d.ts.map +1 -1
  27. package/dist/loader.js +3 -15
  28. package/dist/loader.js.map +1 -1
  29. package/dist/node-version.d.ts +2 -1
  30. package/dist/node-version.d.ts.map +1 -1
  31. package/dist/node-version.js +2 -1
  32. package/dist/node-version.js.map +1 -1
  33. package/dist/static-dependencies.d.ts +3 -0
  34. package/dist/static-dependencies.d.ts.map +1 -0
  35. package/dist/static-dependencies.js +350 -0
  36. package/dist/static-dependencies.js.map +1 -0
  37. package/dist/virtual.d.ts.map +1 -1
  38. package/dist/virtual.js +0 -2
  39. package/dist/virtual.js.map +1 -1
  40. package/dist/vite-plugin.d.ts.map +1 -1
  41. package/dist/vite-plugin.js.map +1 -1
  42. package/package.json +20 -6
  43. package/src/config-dependencies.ts +4 -2
  44. package/src/config-reloader.ts +54 -0
  45. package/src/config-source.ts +25 -0
  46. package/src/config.ts +22 -14
  47. package/src/deploy.ts +2 -3
  48. package/src/internal.ts +4 -1
  49. package/src/loader.ts +4 -24
  50. package/src/node-version.ts +2 -1
  51. package/src/static-dependencies.ts +464 -0
  52. package/src/virtual.ts +0 -2
  53. package/src/vite-plugin.ts +0 -2
package/src/config.ts CHANGED
@@ -25,13 +25,11 @@ type ResolvedReactCompilerOptions = ReactCompilerOptions & {
25
25
  };
26
26
 
27
27
  /**
28
- * User-facing configuration for a GTKX project, as authored in `gtkx.config.ts`: the GIR libraries
29
- * to bind and where to find them, the GApplication id, per-element configuration, the React
30
- * Compiler, codegen, and user event signal settings, and the `agents` and `mcp` blocks controlling
31
- * what coding agents are given.
28
+ * GTKX project configuration authored in `gtkx.config.ts` and validated when loaded.
32
29
  */
33
30
  type Config = z.infer<typeof configSchema>;
34
31
  type ModuleExport = z.infer<typeof moduleExportSchema>;
32
+ type ElementPropsExport = z.infer<typeof elementPropsSchema>;
35
33
  type ElementConfigEntry = z.infer<typeof elementConfigSchema>;
36
34
 
37
35
  type McpSettings = {
@@ -80,7 +78,7 @@ const applicationIdSchema = z
80
78
 
81
79
  const reactCompilerSchema = z.union([
82
80
  z.boolean(),
83
- z.object({
81
+ z.strictObject({
84
82
  compilationMode: z.enum(COMPILATION_MODES).optional(),
85
83
  panicThreshold: z.enum(PANIC_THRESHOLDS).optional(),
86
84
  }),
@@ -97,7 +95,7 @@ const userEventSignalsSchema = z.record(
97
95
  { error: "must be a record of GLib type names to signal name arrays" },
98
96
  );
99
97
 
100
- const moduleExportSchema = z.object(
98
+ const moduleExportSchema = z.strictObject(
101
99
  {
102
100
  module: z.string({ error: "must be a module specifier" }).min(1, { error: "must be a module specifier" }),
103
101
  export: z.string({ error: "must be an export name" }).min(1, { error: "must be an export name" }),
@@ -105,10 +103,16 @@ const moduleExportSchema = z.object(
105
103
  { error: "must be a { module, export } object" },
106
104
  );
107
105
 
108
- const elementConfigSchema = z.object({
106
+ const elementPropsSchema = moduleExportSchema.extend({
107
+ composition: z.enum(["factory", "intersection"]).optional(),
108
+ constructOnly: z.array(z.string()).optional(),
109
+ });
110
+
111
+ const elementConfigSchema = z.strictObject({
109
112
  component: moduleExportSchema.optional(),
110
- props: moduleExportSchema.optional(),
113
+ props: elementPropsSchema.optional(),
111
114
  isLazy: z.boolean({ error: "must be a boolean" }).optional(),
115
+ acceptedChildTypes: z.array(z.string()).optional(),
112
116
  omittedProps: z
113
117
  .array(
114
118
  z.string({ error: "must be a non-empty property name" }).min(1, {
@@ -119,7 +123,7 @@ const elementConfigSchema = z.object({
119
123
  .optional(),
120
124
  });
121
125
 
122
- const elementsSchema = z.object({
126
+ const elementsSchema = z.strictObject({
123
127
  behaviors: z
124
128
  .string({ error: "must be a path to a module exporting element behaviors" })
125
129
  .min(1, { error: "must be a path to a module exporting element behaviors" })
@@ -127,12 +131,12 @@ const elementsSchema = z.object({
127
131
  config: z.record(z.string(), elementConfigSchema).optional(),
128
132
  });
129
133
 
130
- const agentsSchema = z.object({
134
+ const agentsSchema = z.strictObject({
131
135
  rules: z.boolean({ error: "must be a boolean" }).optional(),
132
136
  reference: z.boolean({ error: "must be a boolean" }).optional(),
133
137
  });
134
138
 
135
- const mcpSchema = z.object({
139
+ const mcpSchema = z.strictObject({
136
140
  tools: z
137
141
  .array(z.string({ error: "must be a tool name pattern" }).min(1, { error: "must be a tool name pattern" }), {
138
142
  error: "must be an array of tool name patterns",
@@ -153,7 +157,7 @@ const graduatedFutureSchema = z
153
157
  })
154
158
  .strict();
155
159
 
156
- const deprecationsSchema = z.object({
160
+ const deprecationsSchema = z.strictObject({
157
161
  silence: z
158
162
  .array(z.never({ error: "does not name a current deprecation" }), {
159
163
  error: "must be an array of current deprecation ids",
@@ -162,7 +166,7 @@ const deprecationsSchema = z.object({
162
166
  });
163
167
 
164
168
  /** Schema every `gtkx.config.ts` is validated against, and the source of the {@link Config} type. */
165
- const configSchema = z.object({
169
+ const configSchema = z.strictObject({
166
170
  libraries: librariesSchema.optional(),
167
171
  girPath: z.array(z.string(), { error: "must be an array of strings if provided" }).optional(),
168
172
  applicationId: applicationIdSchema,
@@ -251,9 +255,12 @@ const elementEntryValues = <T>(
251
255
  const resolveElementComponents = (elements: Config["elements"]): Record<string, ModuleExport> =>
252
256
  elementEntryValues(elements, (entry) => entry.component);
253
257
 
254
- const resolveElementProps = (elements: Config["elements"]): Record<string, ModuleExport> =>
258
+ const resolveElementProps = (elements: Config["elements"]): Record<string, ElementPropsExport> =>
255
259
  elementEntryValues(elements, (entry) => entry.props);
256
260
 
261
+ const resolveAcceptedChildTypes = (elements: Config["elements"]): Record<string, string[]> =>
262
+ elementEntryValues(elements, (entry) => entry.acceptedChildTypes);
263
+
257
264
  const resolveOmittedProps = (elements: Config["elements"]): Record<string, string[]> =>
258
265
  elementEntryValues(elements, (entry) => entry.omittedProps);
259
266
 
@@ -285,6 +292,7 @@ export {
285
292
  resolveLazyElements,
286
293
  resolveElementComponents,
287
294
  resolveElementProps,
295
+ resolveAcceptedChildTypes,
288
296
  resolveMcpSettings,
289
297
  resolveOmittedProps,
290
298
  resolveConfig,
package/src/deploy.ts CHANGED
@@ -10,7 +10,6 @@ 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;
@@ -160,7 +159,7 @@ type DeployNodeOptions = Partial<
160
159
  Record<"path", string | undefined> &
161
160
  Record<"shouldStrip" | "shouldUseCompileCache", boolean | undefined>
162
161
  > & {
163
- /** Expected Node.js version. Downloaded runtimes default to exactly 26.7.0 when omitted. */
162
+ /** Expected Node.js version. Downloaded runtimes default to exactly 26.8.2 when omitted. */
164
163
  version?: string | undefined;
165
164
  };
166
165
  const nodeRuntimeSchema: z.ZodType<DeployNodeOptions> = z.strictObject({
@@ -196,7 +195,7 @@ const rpmSchema = z.strictObject({
196
195
 
197
196
  const appimageSchema = z.strictObject({
198
197
  fileName: text("must be an output file name").optional(),
199
- compression: z.enum(APPIMAGE_COMPRESSIONS, { error: "must be one of gzip, xz, zstd" }).optional(),
198
+ compression: z.literal("zstd", { error: "must be zstd" }).optional(),
200
199
  updateInformation: text("must be an AppImage update information string").optional(),
201
200
  runtimeFile: text("must be a path to an AppImage runtime file").optional(),
202
201
  });
package/src/internal.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export { configDependenciesFor } from "./config-dependencies.ts";
2
+ export { createConfigReloader } from "./config-reloader.ts";
2
3
  export type { McpSettings, ResolvedReactCompilerOptions } from "./config.ts";
3
4
  export {
4
5
  APPLICATION_ID_MAX_LENGTH,
5
6
  isAgentReferenceEnabled,
6
7
  isAgentRulesEnabled,
7
8
  isValidApplicationId,
9
+ resolveAcceptedChildTypes,
8
10
  resolveElementComponents,
9
11
  resolveElementProps,
10
12
  resolveLazyElements,
@@ -12,6 +14,7 @@ export {
12
14
  resolveOmittedProps,
13
15
  } from "./config.ts";
14
16
  export { createConfigLoader } from "./loader.ts";
15
- export { assertSupportedNodeVersion, MINIMUM_NODE_VERSION } from "./node-version.ts";
17
+ export { assertSupportedNodeVersion, DEFAULT_NODE_VERSION, MINIMUM_NODE_VERSION } from "./node-version.ts";
16
18
  export { resourceBasePath } from "./resource-base-path.ts";
17
19
  export { viteProjectRoot } from "./vite-root.ts";
20
+ export { SUPPORTED_EXTENSIONS as CONFIG_EXTENSIONS } from "c12";
package/src/loader.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { isPathInside, warn } from "@gtkx/utils";
2
2
  import { loadConfig as loadConfigFile } from "c12";
3
3
  import { existsSync } from "node:fs";
4
- import { basename, extname, isAbsolute, relative, resolve } from "node:path";
4
+ import { relative, resolve } from "node:path";
5
5
  import {
6
6
  captureConfigDependencies,
7
7
  setConfigDependencies,
8
8
  transformConfigModule,
9
9
  } from "./config-dependencies.ts";
10
10
  import { missingConfigFileError } from "./config-error.ts";
11
+ import { type ConfigResolutionOptions, localConfigSourcePath } from "./config-source.ts";
11
12
  import {
12
13
  type Config,
13
14
  graduatedFutureKeys,
@@ -47,28 +48,6 @@ type ConfigLoader = {
47
48
  const GRADUATED_FUTURE_ENV = "GTKX_GRADUATED_FUTURE_SHOWN";
48
49
  const graduatedFutureWarnings: Map<string, string> = new Map();
49
50
 
50
- type ConfigResolutionOptions = { configFile?: string | undefined; cwd?: string | undefined };
51
-
52
- const isLocalConfigSource = (source: string): boolean => source.startsWith(".") || isAbsolute(source);
53
-
54
- const isDirectoryConfigSource = (source: string): boolean => {
55
- const extension = extname(source);
56
-
57
- return extension.length === 0 || extension === basename(source);
58
- };
59
-
60
- const localConfigSourcePath = (source: string, options: ConfigResolutionOptions): string | undefined => {
61
- if (source === "." || !isLocalConfigSource(source)) {
62
- return undefined;
63
- }
64
-
65
- const cwd = options.cwd ?? process.cwd();
66
-
67
- return isDirectoryConfigSource(source)
68
- ? resolve(cwd, source, options.configFile ?? "gtkx.config")
69
- : resolve(cwd, source);
70
- };
71
-
72
51
  const rejectMissingLocalConfig = (source: string, options: ConfigResolutionOptions): undefined => {
73
52
  const path = localConfigSourcePath(source, options);
74
53
 
@@ -138,6 +117,7 @@ const loadConfig = async (cwd: string, options: LoadConfigOptions = {}): Promise
138
117
  rcFile: false,
139
118
  globalRc: false,
140
119
  packageJson: false,
120
+ omit$Keys: true,
141
121
  context: { mode: options.mode },
142
122
  jitiOptions: { fsCache: false, transform: transformConfigModule },
143
123
  resolve: rejectMissingLocalConfig,
@@ -198,4 +178,4 @@ const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
198
178
  };
199
179
  };
200
180
 
201
- export { loadConfig, createConfigLoader, type LoadedConfig, type ConfigLoader };
181
+ export { loadConfig, createConfigLoader, type LoadedConfig, type ConfigLoader, type LoadConfigOptions };
@@ -1,6 +1,7 @@
1
1
  const MINIMUM_NODE_MAJOR = 26;
2
2
  const MINIMUM_NODE_MINOR = 7;
3
3
  const MINIMUM_NODE_VERSION = "26.7.0";
4
+ const DEFAULT_NODE_VERSION = "26.8.2";
4
5
 
5
6
  const assertSupportedNodeVersion = (): void => {
6
7
  const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
@@ -13,4 +14,4 @@ const assertSupportedNodeVersion = (): void => {
13
14
  }
14
15
  };
15
16
 
16
- export { assertSupportedNodeVersion, MINIMUM_NODE_VERSION };
17
+ export { assertSupportedNodeVersion, DEFAULT_NODE_VERSION, MINIMUM_NODE_VERSION };
@@ -0,0 +1,464 @@
1
+ import { parseSync, traverse, types } from "@babel/core";
2
+ import { parseJSON, parseJSON5, parseJSONC, parseTOML, parseYAML } from "confbox";
3
+ import { createJiti, type Jiti } from "jiti";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { localConfigSourcePath } from "./config-source.ts";
8
+
9
+ const MODULE_EXTENSIONS: ReadonlySet<string> = new Set([
10
+ ".cjs",
11
+ ".cts",
12
+ ".js",
13
+ ".mjs",
14
+ ".mts",
15
+ ".ts",
16
+ ]);
17
+ const RESOLVABLE_EXTENSIONS = [
18
+ ".js",
19
+ ".ts",
20
+ ".mjs",
21
+ ".cjs",
22
+ ".mts",
23
+ ".cts",
24
+ ".json",
25
+ ".jsonc",
26
+ ".json5",
27
+ ".yaml",
28
+ ".yml",
29
+ ".toml",
30
+ ];
31
+ const JAVASCRIPT_FALLBACKS: Readonly<Record<string, string[]>> = {
32
+ ".cjs": [".cts"],
33
+ ".js": [".ts"],
34
+ ".jsx": [".tsx"],
35
+ ".mjs": [".mts"],
36
+ };
37
+ const DATA_PARSERS: Readonly<Record<string, (source: string) => unknown>> = {
38
+ ".json": parseJSON,
39
+ ".json5": parseJSON5,
40
+ ".jsonc": parseJSONC,
41
+ ".toml": parseTOML,
42
+ ".yaml": parseYAML,
43
+ ".yml": parseYAML,
44
+ };
45
+
46
+ type PackageImports = { file: string; imports: Record<string, unknown>; root: string };
47
+ type ModuleSources = { all: string[]; extended: string[] };
48
+
49
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
50
+ typeof value === "object" && value !== null && !Array.isArray(value);
51
+
52
+ const parserPlugins = (path: string): ("decorators" | "jsx" | "typescript")[] => {
53
+ const extension = extname(path).toLowerCase();
54
+
55
+ return [
56
+ "decorators",
57
+ ...([".cts", ".mts", ".ts"].includes(extension) ? ["typescript" as const] : []),
58
+ ];
59
+ };
60
+
61
+ function expressionSources(value: types.Expression): string[] {
62
+ if (types.isStringLiteral(value)) {
63
+ return [value.value];
64
+ }
65
+
66
+ if (types.isArrayExpression(value)) {
67
+ return value.elements.flatMap((element) =>
68
+ types.isExpression(element) ? expressionSources(element) : []);
69
+ }
70
+
71
+ return types.isObjectExpression(value)
72
+ ? value.properties.flatMap((property) => sourcePropertySources(property))
73
+ : [];
74
+ }
75
+
76
+ const isNamedProperty = (property: types.ObjectProperty, name: string): boolean =>
77
+ (types.isIdentifier(property.key) && property.key.name === name) ||
78
+ (types.isStringLiteral(property.key) && property.key.value === name);
79
+
80
+ function sourcePropertySources(
81
+ property: types.ObjectMethod | types.ObjectProperty | types.SpreadElement,
82
+ ): string[] {
83
+ return types.isObjectProperty(property) &&
84
+ isNamedProperty(property, "source") &&
85
+ types.isExpression(property.value)
86
+ ? expressionSources(property.value)
87
+ : [];
88
+ }
89
+
90
+ const extendsSources = (value: types.Expression): string[] => expressionSources(value);
91
+
92
+ const isExtendsProperty = (property: types.ObjectProperty): boolean =>
93
+ isNamedProperty(property, "extends");
94
+
95
+ type ParsedModule = NonNullable<ReturnType<typeof parseSync>>;
96
+
97
+ const parseModule = (path: string): ParsedModule | undefined => {
98
+ try {
99
+ return parseSync(readFileSync(path, "utf8"), {
100
+ ast: true,
101
+ babelrc: false,
102
+ configFile: false,
103
+ filename: path,
104
+ parserOpts: { allowReturnOutsideFunction: true, plugins: parserPlugins(path) },
105
+ sourceType: "unambiguous",
106
+ }) ?? undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ };
111
+
112
+ const addSource = (sources: Set<string>, source: types.StringLiteral | null | undefined): void => {
113
+ if (source !== null && source !== undefined) {
114
+ sources.add(source.value);
115
+ }
116
+ };
117
+
118
+ const requireSource = (call: types.CallExpression): types.StringLiteral | undefined =>
119
+ types.isIdentifier(call.callee, { name: "require" }) && types.isStringLiteral(call.arguments[0])
120
+ ? call.arguments[0]
121
+ : undefined;
122
+
123
+ const importEqualsSource = (declaration: types.TSImportEqualsDeclaration): types.StringLiteral | undefined => {
124
+ const { moduleReference } = declaration;
125
+
126
+ return types.isTSExternalModuleReference(moduleReference) ? moduleReference.expression : undefined;
127
+ };
128
+
129
+ const propertySources = (property: types.ObjectProperty): string[] =>
130
+ isExtendsProperty(property) && types.isExpression(property.value)
131
+ ? extendsSources(property.value)
132
+ : [];
133
+
134
+ const collectModuleSources = (ast: ParsedModule): ModuleSources => {
135
+ const sources: Set<string> = new Set();
136
+ const extended: Set<string> = new Set();
137
+
138
+ traverse(ast, {
139
+ CallExpression: (nodePath) => {
140
+ addSource(sources, requireSource(nodePath.node));
141
+ },
142
+ ExportAllDeclaration: (nodePath) => {
143
+ addSource(sources, nodePath.node.source);
144
+ },
145
+ ExportNamedDeclaration: (nodePath) => {
146
+ addSource(sources, nodePath.node.source);
147
+ },
148
+ ImportDeclaration: (nodePath) => {
149
+ addSource(sources, nodePath.node.source);
150
+ },
151
+ ImportExpression: (nodePath) => {
152
+ const { source } = nodePath.node;
153
+ addSource(sources, types.isStringLiteral(source) ? source : undefined);
154
+ },
155
+ ObjectProperty: (nodePath) => {
156
+ for (const source of propertySources(nodePath.node)) {
157
+ sources.add(source);
158
+ extended.add(source);
159
+ }
160
+ },
161
+ TSImportEqualsDeclaration: (nodePath) => {
162
+ addSource(sources, importEqualsSource(nodePath.node));
163
+ },
164
+ });
165
+
166
+ return { all: [...sources], extended: [...extended] };
167
+ };
168
+
169
+ const moduleSources = (path: string): ModuleSources => {
170
+ const ast = parseModule(path);
171
+
172
+ return ast === undefined ? { all: [], extended: [] } : collectModuleSources(ast);
173
+ };
174
+
175
+ function parsedExtendsSources(value: unknown): string[] {
176
+ if (typeof value === "string") {
177
+ return [value];
178
+ }
179
+
180
+ if (Array.isArray(value)) {
181
+ return value.flatMap((item) => parsedExtendsSources(item));
182
+ }
183
+
184
+ return objectExtendsSources(value);
185
+ }
186
+
187
+ const objectExtendsSources = (value: unknown): string[] => {
188
+ if (typeof value !== "object" || value === null) {
189
+ return [];
190
+ }
191
+
192
+ return "source" in value ? parsedExtendsSources(value.source) : [];
193
+ };
194
+
195
+ const configExtendsSources = (value: unknown): string[] => {
196
+ if (typeof value !== "object" || value === null) {
197
+ return [];
198
+ }
199
+
200
+ return "extends" in value ? parsedExtendsSources(value.extends) : [];
201
+ };
202
+
203
+ const dataSources = (path: string): string[] => {
204
+ const parser = DATA_PARSERS[extname(path).toLowerCase()];
205
+
206
+ if (parser === undefined) {
207
+ return [];
208
+ }
209
+
210
+ try {
211
+ return configExtendsSources(parser(readFileSync(path, "utf8")));
212
+ } catch {
213
+ return [];
214
+ }
215
+ };
216
+
217
+ const extensionCandidates = (path: string): string[] => {
218
+ const extension = extname(path).toLowerCase();
219
+
220
+ if (extension.length === 0) {
221
+ return RESOLVABLE_EXTENSIONS.map((candidate) => `${path}${candidate}`);
222
+ }
223
+
224
+ const stem = path.slice(0, -extension.length);
225
+
226
+ return (JAVASCRIPT_FALLBACKS[extension] ?? []).map((candidate) => `${stem}${candidate}`);
227
+ };
228
+
229
+ const targetCandidates = (target: string, configName: string): string[] => {
230
+ const extension = extname(target);
231
+
232
+ if (extension.length > 0) {
233
+ return [target, ...extensionCandidates(target)];
234
+ }
235
+
236
+ return [
237
+ target,
238
+ ...extensionCandidates(target),
239
+ join(target, configName),
240
+ ...RESOLVABLE_EXTENSIONS.map((candidate) => join(target, `index${candidate}`)),
241
+ ];
242
+ };
243
+
244
+ const localSourceTarget = (source: string, importer: string): string | undefined => {
245
+ const clean = source.split(/[?#]/u, 1)[0] ?? "";
246
+
247
+ if (clean.startsWith("file:")) {
248
+ return fileURLToPath(clean);
249
+ }
250
+
251
+ if (!isAbsolute(clean) && !clean.startsWith(".")) {
252
+ return undefined;
253
+ }
254
+
255
+ return resolve(dirname(importer), clean);
256
+ };
257
+
258
+ const unresolvedCandidates = (source: string, importer: string, configName: string): string[] => {
259
+ const target = localSourceTarget(source, importer);
260
+
261
+ if (target === undefined) {
262
+ return [];
263
+ }
264
+
265
+ return targetCandidates(target, configName);
266
+ };
267
+
268
+ const nearestPackageFile = (importer: string): string | undefined => {
269
+ let directory = dirname(importer);
270
+ let previous = "";
271
+
272
+ while (directory !== previous) {
273
+ const candidate = join(directory, "package.json");
274
+
275
+ if (existsSync(candidate)) {
276
+ return candidate;
277
+ }
278
+
279
+ previous = directory;
280
+ directory = dirname(directory);
281
+ }
282
+
283
+ return undefined;
284
+ };
285
+
286
+ const readPackageImports = (importer: string): PackageImports | undefined => {
287
+ const file = nearestPackageFile(importer);
288
+
289
+ if (file === undefined) {
290
+ return undefined;
291
+ }
292
+
293
+ try {
294
+ const manifest = parseJSON(readFileSync(file, "utf8"));
295
+
296
+ return isRecord(manifest) && isRecord(manifest.imports)
297
+ ? { file, imports: manifest.imports, root: dirname(file) }
298
+ : undefined;
299
+ } catch {
300
+ return undefined;
301
+ }
302
+ };
303
+
304
+ function stringTargets(value: unknown): string[] {
305
+ if (typeof value === "string") {
306
+ return [value];
307
+ }
308
+
309
+ if (Array.isArray(value)) {
310
+ return value.flatMap((item) => stringTargets(item));
311
+ }
312
+
313
+ return isRecord(value)
314
+ ? Object.values(value).flatMap((item) => stringTargets(item))
315
+ : [];
316
+ }
317
+
318
+ const importPatternMatch = (pattern: string, source: string): string | undefined => {
319
+ if (pattern === source) {
320
+ return "";
321
+ }
322
+
323
+ const wildcard = pattern.indexOf("*");
324
+
325
+ if (wildcard === -1) {
326
+ return undefined;
327
+ }
328
+
329
+ const prefix = pattern.slice(0, wildcard);
330
+ const suffix = pattern.slice(wildcard + 1);
331
+
332
+ return source.startsWith(prefix) && source.endsWith(suffix)
333
+ ? source.slice(prefix.length, source.length - suffix.length)
334
+ : undefined;
335
+ };
336
+
337
+ const packageImportTargets = (source: string, imports: Record<string, unknown>): string[] =>
338
+ Object.entries(imports).flatMap(([pattern, value]) => {
339
+ const match = importPatternMatch(pattern, source);
340
+
341
+ return match === undefined
342
+ ? []
343
+ : stringTargets(value).map((target) => target.split("*").join(match));
344
+ });
345
+
346
+ const packageImportDependencies = (source: string, importer: string, configName: string): string[] => {
347
+ if (!source.startsWith("#")) {
348
+ return [];
349
+ }
350
+
351
+ const packageImports = readPackageImports(importer);
352
+
353
+ if (packageImports === undefined) {
354
+ return [];
355
+ }
356
+
357
+ const targets = packageImportTargets(source, packageImports.imports)
358
+ .filter((target) => target.startsWith("."))
359
+ .flatMap((target) => targetCandidates(resolve(packageImports.root, target), configName));
360
+
361
+ return [packageImports.file, ...targets];
362
+ };
363
+
364
+ const normalizedResolvedPath = (path: string): string =>
365
+ path.startsWith("file:") ? fileURLToPath(path) : path;
366
+
367
+ const resolvedSourcePath = (source: string, importer: string, jiti: Jiti): string | undefined => {
368
+ try {
369
+ const path = jiti.esmResolve(source, {
370
+ parentURL: pathToFileURL(importer).href,
371
+ try: true,
372
+ });
373
+
374
+ return path === undefined ? undefined : normalizedResolvedPath(path);
375
+ } catch {
376
+ return undefined;
377
+ }
378
+ };
379
+
380
+ const sourceDependencies = (
381
+ source: string,
382
+ importer: string,
383
+ configName: string,
384
+ jiti: Jiti,
385
+ ): string[] => {
386
+ const dependency = resolvedSourcePath(source, importer, jiti);
387
+ const packageDependencies = packageImportDependencies(source, importer, configName);
388
+
389
+ return dependency === undefined
390
+ ? [...unresolvedCandidates(source, importer, configName), ...packageDependencies]
391
+ : [dependency, ...packageDependencies];
392
+ };
393
+
394
+ const configLayerDirectory = (path: string, configName: string, configRoot: string): string => {
395
+ const segments = configName.split(/[\\/]/u).filter((segment) => segment.length > 0);
396
+ let directory = resolve(path);
397
+ let remaining = segments.length;
398
+
399
+ while (remaining > 0) {
400
+ directory = dirname(directory);
401
+ remaining -= 1;
402
+ }
403
+
404
+ return resolve(directory, ...segments) === resolve(path) ? directory : resolve(configRoot);
405
+ };
406
+
407
+ const c12SourceDependencies = (
408
+ source: string,
409
+ importer: string,
410
+ configName: string,
411
+ configRoot: string,
412
+ ): string[] => {
413
+ const clean = source.split(/[?#]/u, 1)[0] ?? "";
414
+
415
+ const cwd = configLayerDirectory(importer, configName, configRoot);
416
+ const target = localConfigSourcePath(clean, { cwd, configFile: configName });
417
+
418
+ return target === undefined ? [] : targetCandidates(target, configName);
419
+ };
420
+
421
+ const directDependencies = (path: string, configName: string, configRoot: string): string[] => {
422
+ const module = MODULE_EXTENSIONS.has(extname(path).toLowerCase()) ? moduleSources(path) : undefined;
423
+ const sources = module?.all ?? dataSources(path);
424
+ const extended = module?.extended ?? sources;
425
+ const jiti = createJiti(path, {
426
+ extensions: RESOLVABLE_EXTENSIONS,
427
+ fsCache: false,
428
+ moduleCache: false,
429
+ });
430
+
431
+ return [
432
+ ...sources.flatMap((source) => sourceDependencies(source, path, configName, jiti)),
433
+ ...extended.flatMap((source) => c12SourceDependencies(source, path, configName, configRoot)),
434
+ ]
435
+ .filter((dependency) => !dependency.includes(`${sep}node_modules${sep}`));
436
+ };
437
+
438
+ const resolveConfigDependencies = (
439
+ configFile: string,
440
+ configName = basename(configFile),
441
+ configRoot = dirname(configFile),
442
+ ): string[] => {
443
+ const dependencies: Set<string> = new Set();
444
+ const pending = [resolve(configFile)];
445
+
446
+ while (pending.length > 0) {
447
+ const path = pending.pop();
448
+
449
+ if (path === undefined) {
450
+ break;
451
+ }
452
+
453
+ if (dependencies.has(path)) {
454
+ continue;
455
+ }
456
+
457
+ dependencies.add(path);
458
+ pending.push(...directDependencies(path, configName, configRoot));
459
+ }
460
+
461
+ return [...dependencies];
462
+ };
463
+
464
+ export { resolveConfigDependencies };