@elench/testkit 0.1.56 → 0.1.58
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 +59 -0
- package/lib/cli/commands/browser/serve.mjs +112 -0
- package/lib/cli/commands/discover.mjs +80 -0
- package/lib/cli/entrypoint.mjs +5 -0
- package/lib/cli/presentation/colors.mjs +32 -0
- package/lib/cli/presentation/discovery-reporter.mjs +166 -0
- package/lib/config/discovery.mjs +106 -45
- package/lib/config/discovery.test.mjs +78 -3
- package/lib/config/index.mjs +50 -3
- package/lib/coverage/index.mjs +774 -0
- package/lib/coverage/index.test.mjs +220 -0
- package/lib/discovery/index.d.ts +124 -0
- package/lib/discovery/index.mjs +552 -0
- package/lib/discovery/index.test.mjs +182 -0
- package/lib/history/index.d.ts +46 -0
- package/lib/history/index.mjs +166 -0
- package/lib/history/index.test.mjs +115 -0
- package/lib/package.test.mjs +5 -0
- package/lib/runner/orchestrator.mjs +7 -0
- package/lib/setup/index.d.ts +5 -0
- package/node_modules/@elench/testkit-bridge/package.json +17 -0
- package/node_modules/@elench/testkit-bridge/src/index.mjs +391 -0
- package/node_modules/@elench/testkit-bridge/src/index.test.mjs +183 -0
- package/node_modules/@elench/testkit-protocol/package.json +18 -0
- package/node_modules/@elench/testkit-protocol/src/index.d.ts +204 -0
- package/node_modules/@elench/testkit-protocol/src/index.mjs +245 -0
- package/node_modules/@elench/testkit-protocol/src/index.test.mjs +154 -0
- package/package.json +15 -2
package/lib/config/discovery.mjs
CHANGED
|
@@ -29,13 +29,13 @@ const IGNORED_DIRS = new Set([
|
|
|
29
29
|
"test-results",
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
|
-
export function discoverProject(productDir, explicitServices = {}) {
|
|
32
|
+
export function discoverProject(productDir, explicitServices = {}, options = {}) {
|
|
33
|
+
const strict = options.strict !== false;
|
|
33
34
|
const { suiteFiles, legacyFiles } = discoverFiles(productDir);
|
|
34
35
|
const groupedByService = {};
|
|
35
36
|
const services = {};
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const discoveredSuites = [];
|
|
37
|
+
const diagnostics = buildLegacyFileDiagnostics(legacyFiles);
|
|
38
|
+
const discoveredFiles = [];
|
|
39
39
|
|
|
40
40
|
for (const filePath of suiteFiles) {
|
|
41
41
|
const rule = inferRule(filePath);
|
|
@@ -43,12 +43,20 @@ export function discoverProject(productDir, explicitServices = {}) {
|
|
|
43
43
|
|
|
44
44
|
const owners = inferOwners(filePath, explicitServices);
|
|
45
45
|
if (owners.length === 0) {
|
|
46
|
-
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
code: "unowned_test",
|
|
48
|
+
severity: "error",
|
|
49
|
+
message: `Unowned test file: ${filePath}`,
|
|
50
|
+
path: filePath,
|
|
51
|
+
});
|
|
47
52
|
continue;
|
|
48
53
|
}
|
|
49
54
|
if (owners.length > 1) {
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
diagnostics.push({
|
|
56
|
+
code: "ambiguous_test",
|
|
57
|
+
severity: "error",
|
|
58
|
+
message: `Ambiguous test file: ${filePath} -> ${owners.map((owner) => owner.name).sort((left, right) => left.localeCompare(right)).join(", ")}`,
|
|
59
|
+
path: filePath,
|
|
52
60
|
serviceNames: owners.map((owner) => owner.name).sort((left, right) => left.localeCompare(right)),
|
|
53
61
|
});
|
|
54
62
|
continue;
|
|
@@ -58,7 +66,7 @@ export function discoverProject(productDir, explicitServices = {}) {
|
|
|
58
66
|
services[owner.name] = mergeServiceDiscovery(services[owner.name], owner);
|
|
59
67
|
const relativeToService = relativeToServiceRoot(owner, filePath);
|
|
60
68
|
const suiteRef = deriveSuiteRef(relativeToService, rule.suffix);
|
|
61
|
-
|
|
69
|
+
discoveredFiles.push({
|
|
62
70
|
serviceName: owner.name,
|
|
63
71
|
type: rule.type,
|
|
64
72
|
framework: rule.framework,
|
|
@@ -67,11 +75,11 @@ export function discoverProject(productDir, explicitServices = {}) {
|
|
|
67
75
|
});
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
if (
|
|
71
|
-
throw
|
|
78
|
+
if (strict && hasDiscoveryErrors(diagnostics)) {
|
|
79
|
+
throw buildDiscoveryErrorFromDiagnostics(diagnostics);
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
for (const entry of
|
|
82
|
+
for (const entry of discoveredFiles) {
|
|
75
83
|
const grouped = groupedByService[entry.serviceName] || {};
|
|
76
84
|
const suitesForType = grouped[entry.type] || [];
|
|
77
85
|
const suiteKey = entry.suitePath.join("/");
|
|
@@ -95,12 +103,23 @@ export function discoverProject(productDir, explicitServices = {}) {
|
|
|
95
103
|
suite.files.push(entry.filePath);
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
const fileEntries = [];
|
|
98
107
|
for (const grouped of Object.values(groupedByService)) {
|
|
99
108
|
for (const suites of Object.values(grouped)) {
|
|
100
109
|
const suiteNames = disambiguateSuiteNames(suites);
|
|
101
110
|
for (const suite of suites) {
|
|
102
111
|
suite.name = suiteNames.get(suite._suiteKey);
|
|
103
112
|
suite.files.sort((left, right) => left.localeCompare(right));
|
|
113
|
+
for (const filePath of suite.files) {
|
|
114
|
+
fileEntries.push({
|
|
115
|
+
serviceName: findSuiteServiceName(groupedByService, grouped, suite),
|
|
116
|
+
type: findSuiteType(grouped, suites),
|
|
117
|
+
framework: suite.framework,
|
|
118
|
+
suiteName: suite.name,
|
|
119
|
+
suitePath: [...suite._suitePath],
|
|
120
|
+
filePath,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
104
123
|
delete suite._suiteKey;
|
|
105
124
|
delete suite._suitePath;
|
|
106
125
|
}
|
|
@@ -111,11 +130,59 @@ export function discoverProject(productDir, explicitServices = {}) {
|
|
|
111
130
|
return {
|
|
112
131
|
services,
|
|
113
132
|
suitesByService: groupedByService,
|
|
133
|
+
files: fileEntries.sort(
|
|
134
|
+
(left, right) =>
|
|
135
|
+
left.serviceName.localeCompare(right.serviceName) ||
|
|
136
|
+
left.type.localeCompare(right.type) ||
|
|
137
|
+
left.suiteName.localeCompare(right.suiteName) ||
|
|
138
|
+
left.filePath.localeCompare(right.filePath)
|
|
139
|
+
),
|
|
140
|
+
diagnostics,
|
|
114
141
|
};
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
export function discoverSuites(productDir, explicitServices = {}) {
|
|
118
|
-
return discoverProject(productDir, explicitServices).suitesByService;
|
|
144
|
+
export function discoverSuites(productDir, explicitServices = {}, options = {}) {
|
|
145
|
+
return discoverProject(productDir, explicitServices, options).suitesByService;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function hasDiscoveryErrors(diagnostics = []) {
|
|
149
|
+
return diagnostics.some((entry) => entry?.severity === "error");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function buildDiscoveryErrorFromDiagnostics(diagnostics = []) {
|
|
153
|
+
const lines = ["Filesystem discovery failed for one or more .testkit.ts files."];
|
|
154
|
+
|
|
155
|
+
const legacyFiles = diagnostics.filter((entry) => entry.code === "legacy_path").map((entry) => entry.path);
|
|
156
|
+
const unownedFiles = diagnostics.filter((entry) => entry.code === "unowned_test").map((entry) => entry.path);
|
|
157
|
+
const ambiguousFiles = diagnostics.filter((entry) => entry.code === "ambiguous_test");
|
|
158
|
+
|
|
159
|
+
if (legacyFiles.length > 0) {
|
|
160
|
+
lines.push("");
|
|
161
|
+
lines.push("Legacy test files outside __testkit__:");
|
|
162
|
+
for (const filePath of legacyFiles) {
|
|
163
|
+
lines.push(`- ${filePath}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (unownedFiles.length > 0) {
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push("Unowned test files:");
|
|
170
|
+
for (const filePath of unownedFiles) {
|
|
171
|
+
lines.push(`- ${filePath}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ambiguousFiles.length > 0) {
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push("Ambiguous test files:");
|
|
178
|
+
for (const entry of ambiguousFiles) {
|
|
179
|
+
lines.push(`- ${entry.path} -> ${entry.serviceNames.join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push('Expected test files to live under a "__testkit__" directory within a service root.');
|
|
185
|
+
return new Error(lines.join("\n"));
|
|
119
186
|
}
|
|
120
187
|
|
|
121
188
|
function discoverFiles(productDir) {
|
|
@@ -306,38 +373,6 @@ function mergeServiceDiscovery(existing, owner) {
|
|
|
306
373
|
};
|
|
307
374
|
}
|
|
308
375
|
|
|
309
|
-
function buildDiscoveryError(legacyFiles, unowned, ambiguous) {
|
|
310
|
-
const lines = ["Filesystem discovery failed for one or more .testkit.ts files."];
|
|
311
|
-
|
|
312
|
-
if (legacyFiles.length > 0) {
|
|
313
|
-
lines.push("");
|
|
314
|
-
lines.push("Legacy test files outside __testkit__:");
|
|
315
|
-
for (const filePath of legacyFiles) {
|
|
316
|
-
lines.push(`- ${filePath}`);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (unowned.length > 0) {
|
|
321
|
-
lines.push("");
|
|
322
|
-
lines.push("Unowned test files:");
|
|
323
|
-
for (const filePath of unowned) {
|
|
324
|
-
lines.push(`- ${filePath}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (ambiguous.length > 0) {
|
|
329
|
-
lines.push("");
|
|
330
|
-
lines.push("Ambiguous test files:");
|
|
331
|
-
for (const entry of ambiguous) {
|
|
332
|
-
lines.push(`- ${entry.filePath} -> ${entry.serviceNames.join(", ")}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
lines.push("");
|
|
337
|
-
lines.push('Expected test files to live under a "__testkit__" directory within a service root.');
|
|
338
|
-
return new Error(lines.join("\n"));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
376
|
function inferRule(filePath) {
|
|
342
377
|
return DISCOVERY_RULES.find((rule) => filePath.endsWith(rule.suffix)) || null;
|
|
343
378
|
}
|
|
@@ -351,3 +386,29 @@ function normalizePath(value) {
|
|
|
351
386
|
if (normalized === "." || normalized === "./") return ".";
|
|
352
387
|
return normalized.replace(/^\.\/+/, "").replace(/\/+$/, "") || ".";
|
|
353
388
|
}
|
|
389
|
+
|
|
390
|
+
function buildLegacyFileDiagnostics(legacyFiles) {
|
|
391
|
+
return legacyFiles.map((filePath) => ({
|
|
392
|
+
code: "legacy_path",
|
|
393
|
+
severity: "error",
|
|
394
|
+
message: `Legacy test file outside __testkit__: ${filePath}`,
|
|
395
|
+
path: filePath,
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function findSuiteServiceName(groupedByService, grouped, suite) {
|
|
400
|
+
for (const [serviceName, candidate] of Object.entries(groupedByService)) {
|
|
401
|
+
if (candidate !== grouped) continue;
|
|
402
|
+
for (const suites of Object.values(candidate)) {
|
|
403
|
+
if (suites.includes(suite)) return serviceName;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return "app";
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function findSuiteType(grouped, targetSuites) {
|
|
410
|
+
for (const [type, suites] of Object.entries(grouped)) {
|
|
411
|
+
if (suites === targetSuites) return type;
|
|
412
|
+
}
|
|
413
|
+
return "integration";
|
|
414
|
+
}
|
|
@@ -22,7 +22,7 @@ describe("filesystem-discovery", () => {
|
|
|
22
22
|
writeFile(productDir, "src/api/routes/__testkit__/journeys/smoke.scenario.testkit.ts");
|
|
23
23
|
writeFile(productDir, "frontend/app/__testkit__/homepage/homepage.pw.testkit.ts");
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
const project = discoverProject(productDir, {
|
|
26
26
|
api: {
|
|
27
27
|
local: {
|
|
28
28
|
cwd: ".",
|
|
@@ -35,6 +35,7 @@ describe("filesystem-discovery", () => {
|
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
const suites = project.suitesByService;
|
|
38
39
|
expect(suites.api.integration).toEqual([
|
|
39
40
|
{
|
|
40
41
|
name: "auth",
|
|
@@ -61,6 +62,41 @@ describe("filesystem-discovery", () => {
|
|
|
61
62
|
framework: "playwright",
|
|
62
63
|
},
|
|
63
64
|
]);
|
|
65
|
+
expect(project.files).toEqual([
|
|
66
|
+
{
|
|
67
|
+
serviceName: "api",
|
|
68
|
+
type: "integration",
|
|
69
|
+
framework: "k6",
|
|
70
|
+
suiteName: "auth",
|
|
71
|
+
suitePath: ["src", "api", "routes", "auth"],
|
|
72
|
+
filePath: "src/api/routes/__testkit__/auth/me.int.testkit.ts",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
serviceName: "api",
|
|
76
|
+
type: "integration",
|
|
77
|
+
framework: "k6",
|
|
78
|
+
suiteName: "health",
|
|
79
|
+
suitePath: ["src", "api", "routes", "health"],
|
|
80
|
+
filePath: "src/api/routes/__testkit__/health/ready.int.testkit.ts",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
serviceName: "api",
|
|
84
|
+
type: "scenario",
|
|
85
|
+
framework: "k6",
|
|
86
|
+
suiteName: "journeys",
|
|
87
|
+
suitePath: ["src", "api", "routes", "journeys"],
|
|
88
|
+
filePath: "src/api/routes/__testkit__/journeys/smoke.scenario.testkit.ts",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
serviceName: "frontend",
|
|
92
|
+
type: "e2e",
|
|
93
|
+
framework: "playwright",
|
|
94
|
+
suiteName: "homepage",
|
|
95
|
+
suitePath: ["app", "homepage"],
|
|
96
|
+
filePath: "frontend/app/__testkit__/homepage/homepage.pw.testkit.ts",
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
expect(project.diagnostics).toEqual([]);
|
|
64
100
|
});
|
|
65
101
|
|
|
66
102
|
it("infers the suite from the directory that owns __testkit__", () => {
|
|
@@ -130,10 +166,49 @@ describe("filesystem-discovery", () => {
|
|
|
130
166
|
})
|
|
131
167
|
).toThrow("Legacy test files outside __testkit__");
|
|
132
168
|
});
|
|
169
|
+
|
|
170
|
+
it("reports legacy files in non-strict mode", () => {
|
|
171
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-discovery-"));
|
|
172
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
173
|
+
|
|
174
|
+
writeFile(productDir, "src/api/routes/__testkit__/health/ready.int.testkit.ts");
|
|
175
|
+
writeFile(productDir, "tests/api/integration/health.int.testkit.ts");
|
|
176
|
+
|
|
177
|
+
const project = discoverProject(
|
|
178
|
+
productDir,
|
|
179
|
+
{
|
|
180
|
+
api: {
|
|
181
|
+
local: {
|
|
182
|
+
cwd: ".",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
strict: false,
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(project.suitesByService.api.integration).toEqual([
|
|
192
|
+
{
|
|
193
|
+
name: "health",
|
|
194
|
+
files: ["src/api/routes/__testkit__/health/ready.int.testkit.ts"],
|
|
195
|
+
framework: "k6",
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
expect(project.diagnostics).toEqual([
|
|
199
|
+
{
|
|
200
|
+
code: "legacy_path",
|
|
201
|
+
severity: "error",
|
|
202
|
+
message: "Legacy test file outside __testkit__: tests/api/integration/health.int.testkit.ts",
|
|
203
|
+
path: "tests/api/integration/health.int.testkit.ts",
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
133
208
|
});
|
|
134
209
|
|
|
135
|
-
function writeFile(productDir, relativePath) {
|
|
210
|
+
function writeFile(productDir, relativePath, content = "export {};\n") {
|
|
136
211
|
const absolutePath = path.join(productDir, relativePath);
|
|
137
212
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
138
|
-
fs.writeFileSync(absolutePath,
|
|
213
|
+
fs.writeFileSync(absolutePath, content);
|
|
139
214
|
}
|
package/lib/config/index.mjs
CHANGED
|
@@ -31,14 +31,15 @@ export function parseDotenv(filePath) {
|
|
|
31
31
|
return parseDotenvString(fs.readFileSync(filePath, "utf8"));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export async function
|
|
34
|
+
export async function loadConfigContext(opts = {}) {
|
|
35
35
|
const productDir = resolveProductDir(process.cwd(), opts.dir);
|
|
36
|
-
const
|
|
36
|
+
const setupContext = opts.setupContext || (await loadTestkitSetup(productDir));
|
|
37
|
+
const { setup, setupFile } = setupContext;
|
|
37
38
|
const execution = normalizeRepoExecution(setup.execution);
|
|
38
39
|
const reporting = normalizeReportingConfig(setup.reporting);
|
|
39
40
|
const toolchains = normalizeToolchainRegistry(setup.toolchains);
|
|
40
41
|
const explicitServices = setup.services || {};
|
|
41
|
-
const discovery = discoverProject(productDir, explicitServices);
|
|
42
|
+
const discovery = discoverProject(productDir, explicitServices, opts.discoveryOptions || {});
|
|
42
43
|
const serviceNames = new Set([
|
|
43
44
|
...Object.keys(explicitServices),
|
|
44
45
|
...Object.keys(discovery.suitesByService),
|
|
@@ -63,6 +64,23 @@ export async function loadConfigs(opts = {}) {
|
|
|
63
64
|
|
|
64
65
|
validateConfigCoverage(configs);
|
|
65
66
|
|
|
67
|
+
return {
|
|
68
|
+
productDir,
|
|
69
|
+
setup,
|
|
70
|
+
setupFile,
|
|
71
|
+
execution,
|
|
72
|
+
reporting,
|
|
73
|
+
toolchains,
|
|
74
|
+
explicitServices,
|
|
75
|
+
discovery,
|
|
76
|
+
configs,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function loadConfigs(opts = {}) {
|
|
81
|
+
const context = await loadConfigContext(opts);
|
|
82
|
+
const { configs } = context;
|
|
83
|
+
|
|
66
84
|
const filtered = opts.service
|
|
67
85
|
? configs.filter((config) => config.name === opts.service)
|
|
68
86
|
: configs;
|
|
@@ -126,6 +144,7 @@ function normalizeServiceConfig({
|
|
|
126
144
|
...loadServiceEnv(productDir, envFiles),
|
|
127
145
|
...(explicitService.env || {}),
|
|
128
146
|
};
|
|
147
|
+
const browser = normalizeBrowserServiceConfig(explicitService.browser, name);
|
|
129
148
|
if (explicitService.migrate || explicitService.seed) {
|
|
130
149
|
throw new Error(
|
|
131
150
|
`Service "${name}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`
|
|
@@ -172,6 +191,7 @@ function normalizeServiceConfig({
|
|
|
172
191
|
serviceEnv,
|
|
173
192
|
skip,
|
|
174
193
|
runtime,
|
|
194
|
+
browser,
|
|
175
195
|
local,
|
|
176
196
|
},
|
|
177
197
|
};
|
|
@@ -684,6 +704,33 @@ function normalizeSkipReason(reason, label) {
|
|
|
684
704
|
return normalized;
|
|
685
705
|
}
|
|
686
706
|
|
|
707
|
+
function normalizeBrowserServiceConfig(value, serviceName) {
|
|
708
|
+
if (!value) return undefined;
|
|
709
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
710
|
+
throw new Error(`Service "${serviceName}" browser config must be an object`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const origins = Array.isArray(value.origins)
|
|
714
|
+
? value.origins
|
|
715
|
+
.map((origin) => normalizeOptionalString(origin))
|
|
716
|
+
.filter(Boolean)
|
|
717
|
+
: [];
|
|
718
|
+
|
|
719
|
+
for (const origin of origins) {
|
|
720
|
+
try {
|
|
721
|
+
const parsed = new URL(origin);
|
|
722
|
+
if (!parsed.origin) {
|
|
723
|
+
throw new Error("missing origin");
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
throw new Error(`Service "${serviceName}" browser.origins contains an invalid URL: ${origin}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (origins.length === 0) return undefined;
|
|
731
|
+
return { origins };
|
|
732
|
+
}
|
|
733
|
+
|
|
687
734
|
function loadServiceEnv(productDir, envFiles) {
|
|
688
735
|
const env = {};
|
|
689
736
|
for (const envFile of envFiles) {
|