@gtkx/config 1.6.0 → 2.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +47 -44
  2. package/dist/config-dependencies.d.ts +11 -0
  3. package/dist/config-dependencies.d.ts.map +1 -0
  4. package/dist/config-dependencies.js +83 -0
  5. package/dist/config-dependencies.js.map +1 -0
  6. package/dist/config-error.d.ts +4 -15
  7. package/dist/config-error.d.ts.map +1 -1
  8. package/dist/config-error.js +8 -55
  9. package/dist/config-error.js.map +1 -1
  10. package/dist/config.d.ts +35 -61
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +49 -94
  13. package/dist/config.js.map +1 -1
  14. package/dist/deploy.d.ts +26 -16
  15. package/dist/deploy.d.ts.map +1 -1
  16. package/dist/deploy.js +29 -4
  17. package/dist/deploy.js.map +1 -1
  18. package/dist/index.d.ts +1 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/internal.d.ts +5 -2
  23. package/dist/internal.d.ts.map +1 -1
  24. package/dist/internal.js +4 -1
  25. package/dist/internal.js.map +1 -1
  26. package/dist/loader.d.ts +2 -4
  27. package/dist/loader.d.ts.map +1 -1
  28. package/dist/loader.js +96 -22
  29. package/dist/loader.js.map +1 -1
  30. package/dist/node-version.d.ts +4 -0
  31. package/dist/node-version.d.ts.map +1 -0
  32. package/dist/node-version.js +12 -0
  33. package/dist/node-version.js.map +1 -0
  34. package/dist/schema-text.d.ts +1 -2
  35. package/dist/schema-text.d.ts.map +1 -1
  36. package/dist/schema-text.js +1 -1
  37. package/dist/schema-text.js.map +1 -1
  38. package/dist/virtual.d.ts.map +1 -1
  39. package/dist/virtual.js +1 -2
  40. package/dist/virtual.js.map +1 -1
  41. package/dist/vite-plugin.d.ts.map +1 -1
  42. package/dist/vite-plugin.js +14 -5
  43. package/dist/vite-plugin.js.map +1 -1
  44. package/dist/vite-root.d.ts +9 -0
  45. package/dist/vite-root.d.ts.map +1 -0
  46. package/dist/vite-root.js +3 -0
  47. package/dist/vite-root.js.map +1 -0
  48. package/package.json +14 -5
  49. package/src/config-dependencies.ts +114 -0
  50. package/src/config-error.ts +10 -78
  51. package/src/config.ts +60 -166
  52. package/src/deploy.ts +61 -7
  53. package/src/index.ts +1 -2
  54. package/src/internal.ts +4 -3
  55. package/src/loader.ts +138 -31
  56. package/src/node-version.ts +16 -0
  57. package/src/schema-text.ts +1 -1
  58. package/src/virtual.ts +1 -4
  59. package/src/vite-plugin.ts +17 -7
  60. package/src/vite-root.ts +8 -0
  61. package/dist/deprecations.d.ts +0 -5
  62. package/dist/deprecations.d.ts.map +0 -1
  63. package/dist/deprecations.js +0 -97
  64. package/dist/deprecations.js.map +0 -1
  65. package/src/deprecations.ts +0 -130
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 LIBRARY_ID_ERROR = 'must be a GIR library identifier of the form "Name-Version", such as "Gtk-4.0"';
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,16 +117,55 @@ const brandingSchema = z.strictObject({
110
117
  dark: hexColorSchema,
111
118
  });
112
119
 
113
- const extraFileSchema = z.strictObject({
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.string({ error: FILE_MODE_ERROR }).regex(FILE_MODE_PATTERN, { error: FILE_MODE_ERROR }).optional(),
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 nodeRuntimeSchema = z.strictObject({
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
- version: text("must be a Node.js version such as 24.19.0").optional(),
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(),
124
170
  shouldStrip: flag(BOOLEAN_ERROR).optional(),
125
171
  shouldUseCompileCache: flag(BOOLEAN_ERROR).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
@@ -1,4 +1,3 @@
1
- /** @public */
2
1
  export { type Config, defineConfig, mergeConfig, type ResolvedConfig } from "./config.ts";
3
- /** @public */
2
+ export { type DeployExtraFileOptions, type DeployNodeOptions } from "./deploy.ts";
4
3
  export { type ConfigLoader, type LoadedConfig, loadConfig } from "./loader.ts";
package/src/internal.ts CHANGED
@@ -1,4 +1,5 @@
1
- export type { McpSettings, ResolvedFuture, ResolvedReactCompilerOptions } from "./config.ts";
1
+ export { configDependenciesFor } from "./config-dependencies.ts";
2
+ export type { McpSettings, ResolvedReactCompilerOptions } from "./config.ts";
2
3
  export {
3
4
  APPLICATION_ID_MAX_LENGTH,
4
5
  isAgentReferenceEnabled,
@@ -6,11 +7,11 @@ export {
6
7
  isValidApplicationId,
7
8
  resolveElementComponents,
8
9
  resolveElementProps,
9
- resolveFuture,
10
10
  resolveLazyElements,
11
11
  resolveMcpSettings,
12
12
  resolveOmittedProps,
13
- unknownFutureKeys,
14
13
  } from "./config.ts";
15
14
  export { createConfigLoader } from "./loader.ts";
15
+ export { assertSupportedNodeVersion, MINIMUM_NODE_VERSION } from "./node-version.ts";
16
16
  export { resourceBasePath } from "./resource-base-path.ts";
17
+ export { viteProjectRoot } from "./vite-root.ts";
package/src/loader.ts CHANGED
@@ -1,10 +1,21 @@
1
- import { getOrInsert } 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
- import { type Config, resolveConfig, type ResolvedConfig, validateConfig } from "./config.ts";
7
- import { warnDeprecations } from "./deprecations.ts";
11
+ import {
12
+ type Config,
13
+ graduatedFutureKeys,
14
+ resolveConfig,
15
+ type ResolvedConfig,
16
+ validateConfig,
17
+ } from "./config.ts";
18
+ import { assertSupportedNodeVersion } from "./node-version.ts";
8
19
 
9
20
  /** Result of loading a project's `gtkx.config.ts` file. */
10
21
  type LoadedConfig = {
@@ -23,7 +34,7 @@ type LoadConfigOptions = {
23
34
  * top-level values, and the name is passed to a config authored as a function.
24
35
  */
25
36
  mode?: string | undefined;
26
- };
37
+ } & Partial<Record<"configFile", string | undefined>>;
27
38
 
28
39
  /** Reads a project's configuration once per directory, caching what it loads and what it resolves. */
29
40
  type ConfigLoader = {
@@ -33,51 +44,147 @@ type ConfigLoader = {
33
44
  resolve: (cwd: string) => Promise<ResolvedConfig>;
34
45
  };
35
46
 
47
+ const GRADUATED_FUTURE_ENV = "GTKX_GRADUATED_FUTURE_SHOWN";
48
+ const graduatedFutureWarnings: Map<string, string> = new Map();
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
+
106
+ const warnGraduatedFuture = (config: unknown, root: string): void => {
107
+ const keys = graduatedFutureKeys(config);
108
+ const signature = keys.join(",");
109
+
110
+ if (
111
+ keys.length === 0 ||
112
+ graduatedFutureWarnings.get(root) === signature ||
113
+ process.env[GRADUATED_FUTURE_ENV] === signature
114
+ ) {
115
+ return;
116
+ }
117
+
118
+ graduatedFutureWarnings.set(root, signature);
119
+ process.env[GRADUATED_FUTURE_ENV] = signature;
120
+ warn(`GTKX 2 ignores graduated future flags: ${keys.join(", ")}. Remove them from gtkx.config.ts.`);
121
+ };
122
+
36
123
  /**
37
- * Loads and validates the `gtkx.config.ts` file for a project. Writes one deprecation notice to stderr the
38
- * first time a configuration leaves a `future` flag unset, recording what it reported in the environment so
39
- * a child process does not repeat it.
124
+ * Loads and validates the `gtkx.config.ts` file for a project.
40
125
  * @param cwd Directory the configuration file is looked up in; parent directories are not searched.
41
126
  * @param options Loading options, such as the environment mode whose overrides are applied.
42
127
  * @throws When that directory holds no configuration file, or when the configuration fails validation.
43
128
  */
44
129
  const loadConfig = async (cwd: string, options: LoadConfigOptions = {}): Promise<LoadedConfig> => {
130
+ assertSupportedNodeVersion();
45
131
  const searched = resolve(cwd);
132
+ const requestedConfigFile = selectedConfigFile(searched, options.configFile);
46
133
 
47
- const result = await loadConfigFile<Config>({
48
- name: "gtkx",
49
- cwd: searched,
50
- rcFile: false,
51
- globalRc: false,
52
- packageJson: false,
53
- context: { mode: options.mode },
54
- ...((options.mode !== undefined) && { envName: options.mode }),
55
- });
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;
56
148
 
57
149
  const configFile = result.configFile;
150
+ const root = result.cwd ?? searched;
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
+ ];
58
160
 
59
- if (configFile === undefined || !existsSync(resolve(searched, configFile))) {
60
- throw missingConfigFileError(searched);
61
- }
161
+ return withConfigDependencies(dependencies, () => {
162
+ if (configFile === undefined || !existsSync(resolve(searched, configFile))) {
163
+ throw missingConfigFileError(searched, requestedConfigFile);
164
+ }
62
165
 
63
- const config = result.config;
64
- const root = result.cwd ?? searched;
65
- validateConfig(config);
66
- warnDeprecations(config, root);
166
+ const config = result.config;
167
+ validateConfig(config, configFile);
168
+ warnGraduatedFuture(config, root);
67
169
 
68
- return {
69
- config,
70
- configFile,
71
- root,
72
- };
170
+ const loaded = {
171
+ config,
172
+ configFile,
173
+ root,
174
+ };
175
+ setConfigDependencies(loaded, dependencies);
176
+
177
+ return loaded;
178
+ });
73
179
  };
74
180
 
75
181
  const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
182
+ assertSupportedNodeVersion();
76
183
  const loaded: Map<string, Promise<LoadedConfig>> = new Map();
77
184
  const resolved: Map<string, Promise<ResolvedConfig>> = new Map();
78
185
 
79
186
  const load = (cwd: string): Promise<LoadedConfig> =>
80
- getOrInsert(loaded, resolve(cwd), (root) => loadConfig(root, options));
187
+ loaded.getOrInsertComputed(resolve(cwd), (root) => loadConfig(root, options));
81
188
 
82
189
  const resolveAt = async (root: string): Promise<ResolvedConfig> => {
83
190
  const { config, root: configRoot } = await load(root);
@@ -87,7 +194,7 @@ const createConfigLoader = (options: LoadConfigOptions = {}): ConfigLoader => {
87
194
 
88
195
  return {
89
196
  load,
90
- resolve: (cwd: string): Promise<ResolvedConfig> => getOrInsert(resolved, resolve(cwd), resolveAt),
197
+ resolve: (cwd: string): Promise<ResolvedConfig> => resolved.getOrInsertComputed(resolve(cwd), resolveAt),
91
198
  };
92
199
  };
93
200
 
@@ -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 };
@@ -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
@@ -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, undefined when the user config leaves it unset. */
8
- root: string | undefined;
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 ?? process.cwd()));
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: undefined };
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.root ?? state.root;
48
+ state.root = viteProjectRoot(config);
45
49
 
46
50
  return options.config?.(config);
47
51
  },
48
- resolveId: (id: string) => resolveVirtualId(id),
49
- load: (id: string) => loadVirtualModule(id, loadConfig, state),
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
 
@@ -0,0 +1,8 @@
1
+ type RootConfig = {
2
+ root?: string | undefined;
3
+ test?: { root?: string | undefined } | undefined;
4
+ };
5
+
6
+ const viteProjectRoot = (config: RootConfig): string => config.test?.root ?? config.root ?? process.cwd();
7
+
8
+ export { viteProjectRoot };
@@ -1,5 +0,0 @@
1
- import type { Config } from "./config.ts";
2
- declare const DEPRECATION_IDS: readonly ["gtkx-v2-byte-arrays", "gtkx-v2-value-returns", "gtkx-v2-finish-results", "gtkx-v2-inout-returns", "gtkx-v2-resource-imports", "gtkx-v2-default-libraries", "gtkx-v2-tree-shaking"];
3
- declare const warnDeprecations: (config: Config, root: string) => void;
4
- export { DEPRECATION_IDS, warnDeprecations };
5
- //# sourceMappingURL=deprecations.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"deprecations.d.ts","sourceRoot":"","sources":["../src/deprecations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAkB1C,QAAA,MAAM,eAAe,+LAQX,CAAC;AAuFX,QAAA,MAAM,gBAAgB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,IAaxD,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC"}
@@ -1,97 +0,0 @@
1
- import { warn } from "@gtkx/utils";
2
- const SHOWN_ENV = "GTKX_DEPRECATIONS_SHOWN";
3
- const GUIDE_URL = "https://gtkx.dev/guide/upgrading-to-2";
4
- const ENTRY_COLUMN = 30;
5
- const BUILD_REPORTS = " The build, not tsc, reports the specifiers still to change.";
6
- const NOTHING_REPORTS = " Nothing reports this one; check the app yourself.";
7
- const SILENT_SITES = " `Array.isArray` and `JSON.stringify` change silently; grep for them.";
8
- const DEPRECATION_IDS = [
9
- "gtkx-v2-byte-arrays",
10
- "gtkx-v2-value-returns",
11
- "gtkx-v2-finish-results",
12
- "gtkx-v2-inout-returns",
13
- "gtkx-v2-resource-imports",
14
- "gtkx-v2-default-libraries",
15
- "gtkx-v2-tree-shaking",
16
- ];
17
- const FUTURE_DEPRECATIONS = [
18
- {
19
- id: "gtkx-v2-byte-arrays",
20
- flag: "v2ByteArrays",
21
- change: "Byte sequences come back as number[]. In 2.0 they come back as Uint8Array.",
22
- unchecked: SILENT_SITES,
23
- },
24
- {
25
- id: "gtkx-v2-value-returns",
26
- flag: "v2ValueReturns",
27
- change: "Bindings that return a GValue hand back the box. In 2.0 they hand back its contents, as unknown.",
28
- },
29
- {
30
- id: "gtkx-v2-finish-results",
31
- flag: "v2FinishResults",
32
- change: "Async pairs with out parameters resolve with a leading success boolean. In 2.0 it is dropped.",
33
- },
34
- {
35
- id: "gtkx-v2-inout-returns",
36
- flag: "v2InoutReturns",
37
- change: "Inout records repeat in the return value. In 2.0 the repeated entry is dropped.",
38
- },
39
- {
40
- id: "gtkx-v2-resource-imports",
41
- flag: "v2ResourceImports",
42
- change: "Assets resolve through the #data/ import map. In 2.0 they resolve through ?resource imports.",
43
- unchecked: BUILD_REPORTS,
44
- },
45
- {
46
- id: "gtkx-v2-default-libraries",
47
- flag: "v2DefaultLibraries",
48
- change: "Only Gtk-4.0 is bound by default. In 2.0 Adw-1 is bound alongside it.",
49
- unchecked: NOTHING_REPORTS,
50
- },
51
- {
52
- id: "gtkx-v2-tree-shaking",
53
- flag: "v2TreeShaking",
54
- change: "The stores register every class eagerly. " +
55
- "In 2.0 each class registers itself and unused ones drop from bundles.",
56
- },
57
- ];
58
- const shownRoots = new Map();
59
- const isSilenced = (config, id) => (config.deprecations?.silence ?? []).includes(id);
60
- const unsetDeprecations = (config) => FUTURE_DEPRECATIONS.filter((deprecation) => config.future?.[deprecation.flag] !== true);
61
- const formatSummary = (unset, silenced) => {
62
- const note = silenced === 0 ? "" : ` ${String(silenced)} of them silenced here.`;
63
- return (`${String(unset)} of ${String(FUTURE_DEPRECATIONS.length)} future flags are unset. ` +
64
- `Their behavior becomes the default in GTKX 2.0.${note}`);
65
- };
66
- const formatDeprecation = (deprecation) => ` [${deprecation.id}]`.padEnd(ENTRY_COLUMN) +
67
- `future: { ${deprecation.flag}: true }\n ${deprecation.change}` +
68
- (deprecation.unchecked ?? "");
69
- const formatAdvice = (pending) => {
70
- if (pending.every((deprecation) => deprecation.unchecked === undefined)) {
71
- return " Set one flag at a time and run tsc: every affected call site is a type error.";
72
- }
73
- return " Set one flag at a time and run tsc: it reports every affected call site except where noted above.";
74
- };
75
- const formatBlock = (unset, pending, first) => [
76
- formatSummary(unset.length, unset.length - pending.length),
77
- "",
78
- ...pending.flatMap((deprecation) => [formatDeprecation(deprecation), ""]),
79
- formatAdvice(pending),
80
- ` Guide ${GUIDE_URL}`,
81
- ` Silence deprecations: { silence: [${JSON.stringify(first.id)}] }`,
82
- ].join("\n");
83
- const hasShown = (root, signature) => shownRoots.get(root) === signature || process.env[SHOWN_ENV] === signature;
84
- const warnDeprecations = (config, root) => {
85
- const unset = unsetDeprecations(config);
86
- const pending = unset.filter((deprecation) => !isSilenced(config, deprecation.id));
87
- const [first] = pending;
88
- const signature = pending.map((deprecation) => deprecation.id).join(",");
89
- if (first === undefined || hasShown(root, signature)) {
90
- return;
91
- }
92
- shownRoots.set(root, signature);
93
- process.env[SHOWN_ENV] = signature;
94
- warn(formatBlock(unset, pending, first));
95
- };
96
- export { DEPRECATION_IDS, warnDeprecations };
97
- //# sourceMappingURL=deprecations.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"deprecations.js","sourceRoot":"","sources":["../src/deprecations.ts"],"names":[],"mappings":"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","sourcesContent":["import { warn } from \"@gtkx/utils\";\nimport type { Config } from \"./config.ts\";\n\ntype DeprecationId = (typeof DEPRECATION_IDS)[number];\n\ntype FutureDeprecation = {\n id: DeprecationId;\n flag: keyof NonNullable<Config[\"future\"]>;\n change: string;\n unchecked?: string;\n};\n\nconst SHOWN_ENV = \"GTKX_DEPRECATIONS_SHOWN\";\nconst GUIDE_URL = \"https://gtkx.dev/guide/upgrading-to-2\";\nconst ENTRY_COLUMN = 30;\nconst BUILD_REPORTS = \" The build, not tsc, reports the specifiers still to change.\";\nconst NOTHING_REPORTS = \" Nothing reports this one; check the app yourself.\";\nconst SILENT_SITES = \" `Array.isArray` and `JSON.stringify` change silently; grep for them.\";\n\nconst DEPRECATION_IDS = [\n \"gtkx-v2-byte-arrays\",\n \"gtkx-v2-value-returns\",\n \"gtkx-v2-finish-results\",\n \"gtkx-v2-inout-returns\",\n \"gtkx-v2-resource-imports\",\n \"gtkx-v2-default-libraries\",\n \"gtkx-v2-tree-shaking\",\n] as const;\n\nconst FUTURE_DEPRECATIONS: FutureDeprecation[] = [\n {\n id: \"gtkx-v2-byte-arrays\",\n flag: \"v2ByteArrays\",\n change: \"Byte sequences come back as number[]. In 2.0 they come back as Uint8Array.\",\n unchecked: SILENT_SITES,\n },\n {\n id: \"gtkx-v2-value-returns\",\n flag: \"v2ValueReturns\",\n change: \"Bindings that return a GValue hand back the box. In 2.0 they hand back its contents, as unknown.\",\n },\n {\n id: \"gtkx-v2-finish-results\",\n flag: \"v2FinishResults\",\n change: \"Async pairs with out parameters resolve with a leading success boolean. In 2.0 it is dropped.\",\n },\n {\n id: \"gtkx-v2-inout-returns\",\n flag: \"v2InoutReturns\",\n change: \"Inout records repeat in the return value. In 2.0 the repeated entry is dropped.\",\n },\n {\n id: \"gtkx-v2-resource-imports\",\n flag: \"v2ResourceImports\",\n change: \"Assets resolve through the #data/ import map. In 2.0 they resolve through ?resource imports.\",\n unchecked: BUILD_REPORTS,\n },\n {\n id: \"gtkx-v2-default-libraries\",\n flag: \"v2DefaultLibraries\",\n change: \"Only Gtk-4.0 is bound by default. In 2.0 Adw-1 is bound alongside it.\",\n unchecked: NOTHING_REPORTS,\n },\n {\n id: \"gtkx-v2-tree-shaking\",\n flag: \"v2TreeShaking\",\n change:\n \"The stores register every class eagerly. \" +\n \"In 2.0 each class registers itself and unused ones drop from bundles.\",\n },\n];\n\nconst shownRoots: Map<string, string> = new Map();\n\nconst isSilenced = (config: Config, id: DeprecationId): boolean => (config.deprecations?.silence ?? []).includes(id);\n\nconst unsetDeprecations = (config: Config): FutureDeprecation[] =>\n FUTURE_DEPRECATIONS.filter((deprecation) => config.future?.[deprecation.flag] !== true);\n\nconst formatSummary = (unset: number, silenced: number): string => {\n const note = silenced === 0 ? \"\" : ` ${String(silenced)} of them silenced here.`;\n\n return (\n `${String(unset)} of ${String(FUTURE_DEPRECATIONS.length)} future flags are unset. ` +\n `Their behavior becomes the default in GTKX 2.0.${note}`\n );\n};\n\nconst formatDeprecation = (deprecation: FutureDeprecation): string =>\n ` [${deprecation.id}]`.padEnd(ENTRY_COLUMN) +\n `future: { ${deprecation.flag}: true }\\n ${deprecation.change}` +\n (deprecation.unchecked ?? \"\");\n\nconst formatAdvice = (pending: FutureDeprecation[]): string => {\n if (pending.every((deprecation) => deprecation.unchecked === undefined)) {\n return \" Set one flag at a time and run tsc: every affected call site is a type error.\";\n }\n\n return \" Set one flag at a time and run tsc: it reports every affected call site except where noted above.\";\n};\n\nconst formatBlock = (unset: FutureDeprecation[], pending: FutureDeprecation[], first: FutureDeprecation): string =>\n [\n formatSummary(unset.length, unset.length - pending.length),\n \"\",\n ...pending.flatMap((deprecation) => [formatDeprecation(deprecation), \"\"]),\n formatAdvice(pending),\n ` Guide ${GUIDE_URL}`,\n ` Silence deprecations: { silence: [${JSON.stringify(first.id)}] }`,\n ].join(\"\\n\");\n\nconst hasShown = (root: string, signature: string): boolean =>\n shownRoots.get(root) === signature || process.env[SHOWN_ENV] === signature;\n\nconst warnDeprecations = (config: Config, root: string): void => {\n const unset = unsetDeprecations(config);\n const pending = unset.filter((deprecation) => !isSilenced(config, deprecation.id));\n const [first] = pending;\n const signature = pending.map((deprecation) => deprecation.id).join(\",\");\n\n if (first === undefined || hasShown(root, signature)) {\n return;\n }\n\n shownRoots.set(root, signature);\n process.env[SHOWN_ENV] = signature;\n warn(formatBlock(unset, pending, first));\n};\n\nexport { DEPRECATION_IDS, warnDeprecations };\n"]}