@gtkx/config 2.0.0-beta.1 → 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 (70) hide show
  1. package/README.md +11 -166
  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 +84 -0
  5. package/dist/config-dependencies.js.map +1 -0
  6. package/dist/config-error.d.ts +2 -2
  7. package/dist/config-error.d.ts.map +1 -1
  8. package/dist/config-error.js +6 -3
  9. package/dist/config-error.js.map +1 -1
  10. package/dist/config-reloader.d.ts +8 -0
  11. package/dist/config-reloader.d.ts.map +1 -0
  12. package/dist/config-reloader.js +38 -0
  13. package/dist/config-reloader.js.map +1 -0
  14. package/dist/config-source.d.ts +7 -0
  15. package/dist/config-source.d.ts.map +1 -0
  16. package/dist/config-source.js +17 -0
  17. package/dist/config-source.js.map +1 -0
  18. package/dist/config.d.ts +42 -38
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +21 -15
  21. package/dist/config.js.map +1 -1
  22. package/dist/deploy.d.ts +28 -22
  23. package/dist/deploy.d.ts.map +1 -1
  24. package/dist/deploy.js +29 -5
  25. package/dist/deploy.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/internal.d.ts +6 -1
  31. package/dist/internal.d.ts.map +1 -1
  32. package/dist/internal.js +6 -1
  33. package/dist/internal.js.map +1 -1
  34. package/dist/loader.d.ts +2 -2
  35. package/dist/loader.d.ts.map +1 -1
  36. package/dist/loader.js +66 -15
  37. package/dist/loader.js.map +1 -1
  38. package/dist/node-version.d.ts +5 -0
  39. package/dist/node-version.d.ts.map +1 -0
  40. package/dist/node-version.js +13 -0
  41. package/dist/node-version.js.map +1 -0
  42. package/dist/static-dependencies.d.ts +3 -0
  43. package/dist/static-dependencies.d.ts.map +1 -0
  44. package/dist/static-dependencies.js +350 -0
  45. package/dist/static-dependencies.js.map +1 -0
  46. package/dist/virtual.d.ts.map +1 -1
  47. package/dist/virtual.js +0 -2
  48. package/dist/virtual.js.map +1 -1
  49. package/dist/vite-plugin.d.ts.map +1 -1
  50. package/dist/vite-plugin.js +14 -5
  51. package/dist/vite-plugin.js.map +1 -1
  52. package/dist/vite-root.d.ts +9 -0
  53. package/dist/vite-root.d.ts.map +1 -0
  54. package/dist/vite-root.js +3 -0
  55. package/dist/vite-root.js.map +1 -0
  56. package/package.json +23 -6
  57. package/src/config-dependencies.ts +116 -0
  58. package/src/config-error.ts +9 -4
  59. package/src/config-reloader.ts +54 -0
  60. package/src/config-source.ts +25 -0
  61. package/src/config.ts +27 -19
  62. package/src/deploy.ts +61 -8
  63. package/src/index.ts +1 -0
  64. package/src/internal.ts +6 -0
  65. package/src/loader.ts +92 -28
  66. package/src/node-version.ts +17 -0
  67. package/src/static-dependencies.ts +464 -0
  68. package/src/virtual.ts +0 -2
  69. package/src/vite-plugin.ts +16 -8
  70. package/src/vite-root.ts +8 -0
@@ -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 };
package/src/virtual.ts CHANGED
@@ -3,7 +3,6 @@ import { resourceBasePath } from "./resource-base-path.ts";
3
3
 
4
4
  const GTKX_CONFIG_VIRTUAL_ID = "virtual:gtkx-config";
5
5
  const RESOLVED_GTKX_CONFIG_VIRTUAL_ID = `\0${GTKX_CONFIG_VIRTUAL_ID}`;
6
- const METADATA_SPECIFIER = "@gtkx/jsx/metadata";
7
6
 
8
7
  const renderConfigModule = (config: ResolvedConfig): string => {
9
8
  const lazyJson = JSON.stringify(Object.fromEntries(config.lazyElements.map((type) => [type, { isLazy: true }])));
@@ -18,7 +17,6 @@ const renderConfigModule = (config: ResolvedConfig): string => {
18
17
 
19
18
  return [
20
19
  ...behaviorImports,
21
- `export * from ${JSON.stringify(METADATA_SPECIFIER)};`,
22
20
  `export const applicationId = ${JSON.stringify(config.applicationId)};`,
23
21
  `export const resourceBasePath = ${JSON.stringify(resourceBasePath(config.applicationId))};`,
24
22
  `export const userEventSignals = ${JSON.stringify(config.userEventSignals)};`,
@@ -1,13 +1,15 @@
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
- /** State the plugin carries from Vite's `config` hook to the virtual module it serves. */
6
6
  type PluginState = {
7
- /** Project root taken from Vite's `config` hook, undefined when the user config leaves it unset. */
8
- root: string | undefined;
7
+ root: string;
9
8
  };
10
9
 
10
+ const VIRTUAL_ID_RE = new RegExp(`^${GTKX_CONFIG_VIRTUAL_ID}$`);
11
+ const RESOLVED_VIRTUAL_ID_RE = new RegExp(`^${RESOLVED_GTKX_CONFIG_VIRTUAL_ID}$`);
12
+
11
13
  const resolveVirtualId = (id: string): string | null =>
12
14
  id === GTKX_CONFIG_VIRTUAL_ID ? RESOLVED_GTKX_CONFIG_VIRTUAL_ID : null;
13
15
 
@@ -20,7 +22,7 @@ const loadVirtualModule = async (
20
22
  return undefined;
21
23
  }
22
24
 
23
- return renderConfigModule(await loadConfig.resolve(state.root ?? process.cwd()));
25
+ return renderConfigModule(await loadConfig.resolve(state.root));
24
26
  };
25
27
 
26
28
  /**
@@ -36,17 +38,23 @@ const createConfigPlugin = (options: {
36
38
  config?: (config: UserConfig) => Omit<UserConfig, "plugins">;
37
39
  }): Plugin => {
38
40
  const loadConfig = options.loadConfig ?? createConfigLoader();
39
- const state: PluginState = { root: undefined };
41
+ const state: PluginState = { root: process.cwd() };
40
42
 
41
43
  return {
42
44
  name: options.name,
43
45
  config(config: UserConfig) {
44
- state.root = config.root ?? state.root;
46
+ state.root = viteProjectRoot(config);
45
47
 
46
48
  return options.config?.(config);
47
49
  },
48
- resolveId: (id: string) => resolveVirtualId(id),
49
- load: (id: string) => loadVirtualModule(id, loadConfig, state),
50
+ resolveId: {
51
+ filter: { id: VIRTUAL_ID_RE },
52
+ handler: (id: string) => resolveVirtualId(id),
53
+ },
54
+ load: {
55
+ filter: { id: RESOLVED_VIRTUAL_ID_RE },
56
+ handler: (id: string) => loadVirtualModule(id, loadConfig, state),
57
+ },
50
58
  };
51
59
  };
52
60
 
@@ -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 };