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