@elench/testkit 0.1.57 → 0.1.59
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/lib/cli/commands/browser/serve.mjs +112 -0
- package/lib/cli/entrypoint.mjs +4 -0
- package/lib/config/discovery.mjs +61 -32
- package/lib/config/discovery.test.mjs +65 -2
- package/lib/config/index.mjs +58 -1
- package/lib/coverage/index.mjs +776 -0
- package/lib/coverage/index.test.mjs +241 -0
- package/lib/discovery/index.d.ts +3 -0
- package/lib/discovery/index.mjs +17 -2
- package/lib/discovery/path-policy.mjs +106 -0
- package/lib/discovery/path-policy.test.mjs +65 -0
- package/lib/setup/index.d.ts +12 -3
- 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 +11 -2
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import { startBrowserBridgeServer } from "@elench/testkit-bridge";
|
|
3
|
+
import { loadConfigContext, resolveProductDir } from "../../../config/index.mjs";
|
|
4
|
+
import { discoverTests } from "../../../discovery/index.mjs";
|
|
5
|
+
import { loadCurrentRunArtifact } from "../../viewer.mjs";
|
|
6
|
+
|
|
7
|
+
export default class BrowserServeCommand extends Command {
|
|
8
|
+
static summary = "Serve the local browser bridge for the current testkit product";
|
|
9
|
+
|
|
10
|
+
static enableJsonFlag = true;
|
|
11
|
+
|
|
12
|
+
static flags = {
|
|
13
|
+
dir: Flags.string({
|
|
14
|
+
description: "Product directory",
|
|
15
|
+
}),
|
|
16
|
+
host: Flags.string({
|
|
17
|
+
description: "Host to bind the browser bridge server",
|
|
18
|
+
default: "127.0.0.1",
|
|
19
|
+
}),
|
|
20
|
+
port: Flags.integer({
|
|
21
|
+
description: "Port to bind the browser bridge server",
|
|
22
|
+
default: 3847,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(BrowserServeCommand);
|
|
28
|
+
const productDir = resolveProductDir(process.cwd(), flags.dir);
|
|
29
|
+
|
|
30
|
+
const adapter = {
|
|
31
|
+
loadProductContext: async () => {
|
|
32
|
+
const [configContext, discovery] = await Promise.all([
|
|
33
|
+
loadConfigContext({
|
|
34
|
+
dir: productDir,
|
|
35
|
+
discoveryOptions: { strict: false },
|
|
36
|
+
}),
|
|
37
|
+
discoverTests({
|
|
38
|
+
dir: productDir,
|
|
39
|
+
diagnostics: "report",
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
product: {
|
|
45
|
+
name: discovery.product.name,
|
|
46
|
+
directory: discovery.product.directory,
|
|
47
|
+
},
|
|
48
|
+
services: configContext.configs
|
|
49
|
+
.map((config) => {
|
|
50
|
+
const baseUrl = config.testkit.local?.baseUrl || null;
|
|
51
|
+
const browserOrigins = config.testkit.browser?.origins || [];
|
|
52
|
+
if (!baseUrl && browserOrigins.length === 0) return null;
|
|
53
|
+
const serviceEntries = [];
|
|
54
|
+
if (baseUrl && !baseUrl.includes("{port}")) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(baseUrl);
|
|
57
|
+
serviceEntries.push({
|
|
58
|
+
name: config.name,
|
|
59
|
+
baseUrl,
|
|
60
|
+
origin: parsed.origin,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore invalid local.baseUrl templates here; explicit browser origins still work.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const origin of browserOrigins) {
|
|
67
|
+
serviceEntries.push({
|
|
68
|
+
name: config.name,
|
|
69
|
+
baseUrl: origin,
|
|
70
|
+
origin,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return serviceEntries;
|
|
74
|
+
})
|
|
75
|
+
.flat()
|
|
76
|
+
.filter(Boolean),
|
|
77
|
+
discovery,
|
|
78
|
+
runArtifact: loadRunArtifactIfPresent(productDir),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const serverRef = await startBrowserBridgeServer(adapter, {
|
|
84
|
+
host: flags.host,
|
|
85
|
+
port: flags.port,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const payload = {
|
|
89
|
+
ok: true,
|
|
90
|
+
productDir,
|
|
91
|
+
host: serverRef.host,
|
|
92
|
+
port: serverRef.port,
|
|
93
|
+
url: serverRef.url,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (!this.jsonEnabled()) {
|
|
97
|
+
this.log(`testkit browser bridge serving ${productDir}`);
|
|
98
|
+
this.log(`Listening on ${serverRef.url}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await new Promise(() => {});
|
|
102
|
+
return payload;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function loadRunArtifactIfPresent(productDir) {
|
|
107
|
+
try {
|
|
108
|
+
return loadCurrentRunArtifact(productDir);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export function normalizeCliArgs(argv) {
|
|
|
9
9
|
"artifacts",
|
|
10
10
|
"watch",
|
|
11
11
|
"discover",
|
|
12
|
+
"browser",
|
|
12
13
|
"known-failures",
|
|
13
14
|
"db",
|
|
14
15
|
"help",
|
|
@@ -77,6 +78,9 @@ function reorderCommandArgs(args, positionals) {
|
|
|
77
78
|
if (positionals[0]?.value === "db" && positionals[1] && positionals[2]) {
|
|
78
79
|
commandTokens.push(positionals[1], positionals[2]);
|
|
79
80
|
}
|
|
81
|
+
if (positionals[0]?.value === "browser" && positionals[1]) {
|
|
82
|
+
commandTokens.push(positionals[1]);
|
|
83
|
+
}
|
|
80
84
|
const commandIndexes = new Set(commandTokens.map((token) => token.index));
|
|
81
85
|
return [
|
|
82
86
|
...commandTokens.map((token) => token.value),
|
package/lib/config/discovery.mjs
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_DISCOVERY_EXCLUDES,
|
|
5
|
+
normalizeDiscoveryConfig,
|
|
6
|
+
normalizeDiscoveryPath,
|
|
7
|
+
resolveDiscoveryRoots,
|
|
8
|
+
shouldExcludeDiscoveryPath,
|
|
9
|
+
} from "../discovery/path-policy.mjs";
|
|
3
10
|
|
|
4
11
|
const TESTKIT_DIRNAME = "__testkit__";
|
|
5
12
|
const DISCOVERY_RULES = [
|
|
@@ -11,27 +18,10 @@ const DISCOVERY_RULES = [
|
|
|
11
18
|
{ suffix: ".pw.testkit.ts", type: "e2e", framework: "playwright" },
|
|
12
19
|
];
|
|
13
20
|
|
|
14
|
-
const IGNORED_DIRS = new Set([
|
|
15
|
-
".cache",
|
|
16
|
-
".git",
|
|
17
|
-
".hg",
|
|
18
|
-
".next",
|
|
19
|
-
".nuxt",
|
|
20
|
-
".playwright-browsers",
|
|
21
|
-
".svn",
|
|
22
|
-
".testkit",
|
|
23
|
-
".turbo",
|
|
24
|
-
"build",
|
|
25
|
-
"coverage",
|
|
26
|
-
"dist",
|
|
27
|
-
"node_modules",
|
|
28
|
-
"playwright-report",
|
|
29
|
-
"test-results",
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
21
|
export function discoverProject(productDir, explicitServices = {}, options = {}) {
|
|
33
22
|
const strict = options.strict !== false;
|
|
34
|
-
const
|
|
23
|
+
const repoDiscovery = normalizeDiscoveryConfig(options.discovery, { allowRoots: true });
|
|
24
|
+
const { suiteFiles, legacyFiles } = discoverFiles(productDir, repoDiscovery);
|
|
35
25
|
const groupedByService = {};
|
|
36
26
|
const services = {};
|
|
37
27
|
const diagnostics = buildLegacyFileDiagnostics(legacyFiles);
|
|
@@ -41,7 +31,8 @@ export function discoverProject(productDir, explicitServices = {}, options = {})
|
|
|
41
31
|
const rule = inferRule(filePath);
|
|
42
32
|
if (!rule) continue;
|
|
43
33
|
|
|
44
|
-
const owners = inferOwners(filePath, explicitServices);
|
|
34
|
+
const owners = inferOwners(filePath, explicitServices, repoDiscovery);
|
|
35
|
+
if (owners === null) continue;
|
|
45
36
|
if (owners.length === 0) {
|
|
46
37
|
diagnostics.push({
|
|
47
38
|
code: "unowned_test",
|
|
@@ -185,10 +176,16 @@ export function buildDiscoveryErrorFromDiagnostics(diagnostics = []) {
|
|
|
185
176
|
return new Error(lines.join("\n"));
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
function discoverFiles(productDir) {
|
|
179
|
+
function discoverFiles(productDir, repoDiscovery = {}) {
|
|
189
180
|
const suiteFiles = [];
|
|
190
181
|
const legacyFiles = [];
|
|
191
|
-
const
|
|
182
|
+
const exclude = [
|
|
183
|
+
...new Set([
|
|
184
|
+
...DEFAULT_DISCOVERY_EXCLUDES,
|
|
185
|
+
...((repoDiscovery.exclude || []).map(normalizePath)),
|
|
186
|
+
]),
|
|
187
|
+
];
|
|
188
|
+
const queue = resolveDiscoveryRoots(productDir, repoDiscovery.roots, ".");
|
|
192
189
|
|
|
193
190
|
while (queue.length > 0) {
|
|
194
191
|
const current = queue.pop();
|
|
@@ -198,7 +195,8 @@ function discoverFiles(productDir) {
|
|
|
198
195
|
if (entry.isSymbolicLink()) continue;
|
|
199
196
|
|
|
200
197
|
if (entry.isDirectory()) {
|
|
201
|
-
|
|
198
|
+
const relativeDirPath = normalizeDiscoveryPath(path.relative(productDir, absolutePath));
|
|
199
|
+
if (shouldExcludeDiscoveryPath(relativeDirPath, exclude)) continue;
|
|
202
200
|
queue.push(absolutePath);
|
|
203
201
|
continue;
|
|
204
202
|
}
|
|
@@ -221,9 +219,9 @@ function discoverFiles(productDir) {
|
|
|
221
219
|
};
|
|
222
220
|
}
|
|
223
221
|
|
|
224
|
-
function inferOwners(filePath, explicitServices) {
|
|
222
|
+
function inferOwners(filePath, explicitServices, repoDiscovery = {}) {
|
|
225
223
|
const serviceRules = Object.entries(explicitServices).flatMap(([name, config]) =>
|
|
226
|
-
buildServiceRules(name, config)
|
|
224
|
+
buildServiceRules(name, config, repoDiscovery)
|
|
227
225
|
);
|
|
228
226
|
if (serviceRules.length === 0) {
|
|
229
227
|
return [
|
|
@@ -235,8 +233,14 @@ function inferOwners(filePath, explicitServices) {
|
|
|
235
233
|
];
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
const
|
|
236
|
+
const relevantRules = serviceRules.filter(
|
|
237
|
+
(rule) => rule.depth === 0 || (rule.matchPrefix && hasPrefix(filePath, rule.matchPrefix))
|
|
238
|
+
);
|
|
239
|
+
const owningRules = relevantRules.filter((rule) => ownsFile(rule, filePath));
|
|
239
240
|
if (owningRules.length === 0) {
|
|
241
|
+
if (relevantRules.length > 0 && relevantRules.every((rule) => isExcludedForServiceRule(rule, filePath))) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
240
244
|
return [];
|
|
241
245
|
}
|
|
242
246
|
|
|
@@ -291,37 +295,62 @@ function disambiguateSuiteNames(suites) {
|
|
|
291
295
|
return resolved;
|
|
292
296
|
}
|
|
293
297
|
|
|
294
|
-
function buildServiceRules(serviceName, serviceConfig) {
|
|
298
|
+
function buildServiceRules(serviceName, serviceConfig, repoDiscovery = {}) {
|
|
295
299
|
const cwd = normalizePath(serviceConfig?.local?.cwd || ".");
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
: [
|
|
300
|
+
const serviceDiscovery = normalizeDiscoveryConfig(serviceConfig?.discovery, { allowRoots: true });
|
|
301
|
+
const discoveryRoots =
|
|
302
|
+
Array.isArray(serviceDiscovery.roots) && serviceDiscovery.roots.length > 0 ? serviceDiscovery.roots : ["."];
|
|
303
|
+
const serviceRootRelativeExcludes = (serviceDiscovery.exclude || []).map((entry) =>
|
|
304
|
+
cwd === "." ? normalizePath(entry) : normalizePath(path.posix.join(cwd, entry))
|
|
305
|
+
);
|
|
306
|
+
const exclude = [
|
|
307
|
+
...new Set([
|
|
308
|
+
...DEFAULT_DISCOVERY_EXCLUDES,
|
|
309
|
+
...((repoDiscovery.exclude || []).map(normalizePath)),
|
|
310
|
+
...serviceRootRelativeExcludes,
|
|
311
|
+
]),
|
|
312
|
+
];
|
|
299
313
|
|
|
300
314
|
return discoveryRoots.map((root) => {
|
|
301
315
|
const normalizedRoot = normalizePath(root || ".");
|
|
316
|
+
const matchPrefix =
|
|
317
|
+
cwd === "."
|
|
318
|
+
? normalizedRoot
|
|
319
|
+
: normalizedRoot === "."
|
|
320
|
+
? cwd
|
|
321
|
+
: normalizePath(path.posix.join(cwd, normalizedRoot));
|
|
302
322
|
return {
|
|
303
323
|
name: serviceName,
|
|
304
324
|
source: "cwd",
|
|
305
325
|
cwdPrefix: cwd === "." ? null : cwd,
|
|
306
|
-
matchPrefix:
|
|
307
|
-
depth:
|
|
326
|
+
matchPrefix: matchPrefix === "." ? null : matchPrefix,
|
|
327
|
+
depth: matchPrefix === "." ? 0 : matchPrefix.split("/").filter(Boolean).length,
|
|
328
|
+
exclude,
|
|
308
329
|
};
|
|
309
330
|
});
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
function ownsFile(serviceRule, filePath) {
|
|
334
|
+
if (isExcludedForServiceRule(serviceRule, filePath)) return false;
|
|
313
335
|
if (serviceRule.depth === 0) return true;
|
|
314
336
|
if (serviceRule.matchPrefix && hasPrefix(filePath, serviceRule.matchPrefix)) return true;
|
|
315
337
|
return false;
|
|
316
338
|
}
|
|
317
339
|
|
|
318
340
|
function relativeToServiceRoot(serviceRule, filePath) {
|
|
341
|
+
if (serviceRule.matchPrefix && hasPrefix(filePath, serviceRule.matchPrefix)) {
|
|
342
|
+
return path.posix.relative(serviceRule.matchPrefix, filePath);
|
|
343
|
+
}
|
|
319
344
|
if (serviceRule.cwdPrefix && hasPrefix(filePath, serviceRule.cwdPrefix)) {
|
|
320
345
|
return path.posix.relative(serviceRule.cwdPrefix, filePath);
|
|
321
346
|
}
|
|
322
347
|
return filePath;
|
|
323
348
|
}
|
|
324
349
|
|
|
350
|
+
function isExcludedForServiceRule(serviceRule, filePath) {
|
|
351
|
+
return shouldExcludeDiscoveryPath(normalizePath(filePath), serviceRule.exclude || []);
|
|
352
|
+
}
|
|
353
|
+
|
|
325
354
|
function deriveSuiteRef(relativePath, suffix) {
|
|
326
355
|
const parts = relativePath.split("/").filter(Boolean);
|
|
327
356
|
const testkitIndex = parts.indexOf(TESTKIT_DIRNAME);
|
|
@@ -204,10 +204,73 @@ describe("filesystem-discovery", () => {
|
|
|
204
204
|
},
|
|
205
205
|
]);
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
it("does not treat nested coverage source directories as generated output", () => {
|
|
209
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-discovery-"));
|
|
210
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
211
|
+
|
|
212
|
+
writeFile(productDir, "app/coverage/__testkit__/coverage-map.pw.testkit.ts");
|
|
213
|
+
|
|
214
|
+
const project = discoverProject(productDir, {
|
|
215
|
+
web: {
|
|
216
|
+
local: {
|
|
217
|
+
cwd: ".",
|
|
218
|
+
},
|
|
219
|
+
discovery: {
|
|
220
|
+
roots: ["app"],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(project.files).toEqual([
|
|
226
|
+
{
|
|
227
|
+
serviceName: "web",
|
|
228
|
+
type: "e2e",
|
|
229
|
+
framework: "playwright",
|
|
230
|
+
suiteName: "coverage",
|
|
231
|
+
suitePath: ["coverage"],
|
|
232
|
+
filePath: "app/coverage/__testkit__/coverage-map.pw.testkit.ts",
|
|
233
|
+
},
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("supports repo-level and service-level discovery excludes", () => {
|
|
238
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-discovery-"));
|
|
239
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
240
|
+
|
|
241
|
+
writeFile(productDir, "coverage/__testkit__/noise/noise.pw.testkit.ts");
|
|
242
|
+
writeFile(productDir, "app/generated/__testkit__/noise/noise.pw.testkit.ts");
|
|
243
|
+
writeFile(productDir, "app/coverage/__testkit__/coverage-map.pw.testkit.ts");
|
|
244
|
+
|
|
245
|
+
const project = discoverProject(
|
|
246
|
+
productDir,
|
|
247
|
+
{
|
|
248
|
+
web: {
|
|
249
|
+
local: {
|
|
250
|
+
cwd: ".",
|
|
251
|
+
},
|
|
252
|
+
discovery: {
|
|
253
|
+
roots: ["app"],
|
|
254
|
+
exclude: ["app/generated"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
discovery: {
|
|
260
|
+
exclude: ["coverage"],
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(project.files.map((entry) => entry.filePath)).toEqual([
|
|
266
|
+
"app/coverage/__testkit__/coverage-map.pw.testkit.ts",
|
|
267
|
+
]);
|
|
268
|
+
});
|
|
269
|
+
|
|
207
270
|
});
|
|
208
271
|
|
|
209
|
-
function writeFile(productDir, relativePath) {
|
|
272
|
+
function writeFile(productDir, relativePath, content = "export {};\n") {
|
|
210
273
|
const absolutePath = path.join(productDir, relativePath);
|
|
211
274
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
212
|
-
fs.writeFileSync(absolutePath,
|
|
275
|
+
fs.writeFileSync(absolutePath, content);
|
|
213
276
|
}
|
package/lib/config/index.mjs
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
normalizeRuntimeToolchain,
|
|
21
21
|
normalizeToolchainRegistry,
|
|
22
22
|
} from "../toolchains/index.mjs";
|
|
23
|
+
import {
|
|
24
|
+
mergeDiscoveryConfigs,
|
|
25
|
+
normalizeDiscoveryConfig,
|
|
26
|
+
} from "../discovery/path-policy.mjs";
|
|
23
27
|
|
|
24
28
|
const TESTKIT_K6_BIN = "TESTKIT_K6_BIN";
|
|
25
29
|
const DEFAULT_LOCAL_IMAGE = "pgvector/pgvector:pg16";
|
|
@@ -38,8 +42,12 @@ export async function loadConfigContext(opts = {}) {
|
|
|
38
42
|
const execution = normalizeRepoExecution(setup.execution);
|
|
39
43
|
const reporting = normalizeReportingConfig(setup.reporting);
|
|
40
44
|
const toolchains = normalizeToolchainRegistry(setup.toolchains);
|
|
45
|
+
const discoveryConfig = normalizeRepoDiscoveryConfig(setup.discovery);
|
|
41
46
|
const explicitServices = setup.services || {};
|
|
42
|
-
const discovery = discoverProject(productDir, explicitServices,
|
|
47
|
+
const discovery = discoverProject(productDir, explicitServices, {
|
|
48
|
+
...(opts.discoveryOptions || {}),
|
|
49
|
+
discovery: discoveryConfig,
|
|
50
|
+
});
|
|
43
51
|
const serviceNames = new Set([
|
|
44
52
|
...Object.keys(explicitServices),
|
|
45
53
|
...Object.keys(discovery.suitesByService),
|
|
@@ -54,6 +62,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
54
62
|
setup,
|
|
55
63
|
setupFile,
|
|
56
64
|
execution,
|
|
65
|
+
discovery: discoveryConfig,
|
|
57
66
|
reporting,
|
|
58
67
|
toolchains,
|
|
59
68
|
explicitService: explicitServices[name] || {},
|
|
@@ -69,6 +78,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
69
78
|
setup,
|
|
70
79
|
setupFile,
|
|
71
80
|
execution,
|
|
81
|
+
discovery: discoveryConfig,
|
|
72
82
|
reporting,
|
|
73
83
|
toolchains,
|
|
74
84
|
explicitServices,
|
|
@@ -132,6 +142,7 @@ function normalizeServiceConfig({
|
|
|
132
142
|
setup,
|
|
133
143
|
setupFile,
|
|
134
144
|
execution,
|
|
145
|
+
discovery,
|
|
135
146
|
reporting,
|
|
136
147
|
toolchains,
|
|
137
148
|
explicitService,
|
|
@@ -144,6 +155,11 @@ function normalizeServiceConfig({
|
|
|
144
155
|
...loadServiceEnv(productDir, envFiles),
|
|
145
156
|
...(explicitService.env || {}),
|
|
146
157
|
};
|
|
158
|
+
const browser = normalizeBrowserServiceConfig(explicitService.browser, name);
|
|
159
|
+
const normalizedDiscovery = mergeDiscoveryConfigs(
|
|
160
|
+
discovery,
|
|
161
|
+
normalizeServiceDiscoveryConfig(explicitService.discovery, name)
|
|
162
|
+
);
|
|
147
163
|
if (explicitService.migrate || explicitService.seed) {
|
|
148
164
|
throw new Error(
|
|
149
165
|
`Service "${name}" uses removed migrate/seed hooks. Move template lifecycle to database.template.{migrate,seed,verify}.`
|
|
@@ -183,6 +199,7 @@ function normalizeServiceConfig({
|
|
|
183
199
|
execution,
|
|
184
200
|
reporting,
|
|
185
201
|
dependsOn: explicitService.dependsOn || [],
|
|
202
|
+
discovery: normalizedDiscovery,
|
|
186
203
|
database,
|
|
187
204
|
databaseFrom: explicitService.databaseFrom,
|
|
188
205
|
envFiles,
|
|
@@ -190,11 +207,24 @@ function normalizeServiceConfig({
|
|
|
190
207
|
serviceEnv,
|
|
191
208
|
skip,
|
|
192
209
|
runtime,
|
|
210
|
+
browser,
|
|
193
211
|
local,
|
|
194
212
|
},
|
|
195
213
|
};
|
|
196
214
|
}
|
|
197
215
|
|
|
216
|
+
function normalizeRepoDiscoveryConfig(value) {
|
|
217
|
+
return normalizeDiscoveryConfig(value, { allowRoots: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeServiceDiscoveryConfig(value, serviceName) {
|
|
221
|
+
try {
|
|
222
|
+
return normalizeDiscoveryConfig(value, { allowRoots: true });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(`Service "${serviceName}" has invalid discovery config: ${error.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
198
228
|
function normalizeReportingConfig(value) {
|
|
199
229
|
if (!value) return null;
|
|
200
230
|
|
|
@@ -702,6 +732,33 @@ function normalizeSkipReason(reason, label) {
|
|
|
702
732
|
return normalized;
|
|
703
733
|
}
|
|
704
734
|
|
|
735
|
+
function normalizeBrowserServiceConfig(value, serviceName) {
|
|
736
|
+
if (!value) return undefined;
|
|
737
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
738
|
+
throw new Error(`Service "${serviceName}" browser config must be an object`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const origins = Array.isArray(value.origins)
|
|
742
|
+
? value.origins
|
|
743
|
+
.map((origin) => normalizeOptionalString(origin))
|
|
744
|
+
.filter(Boolean)
|
|
745
|
+
: [];
|
|
746
|
+
|
|
747
|
+
for (const origin of origins) {
|
|
748
|
+
try {
|
|
749
|
+
const parsed = new URL(origin);
|
|
750
|
+
if (!parsed.origin) {
|
|
751
|
+
throw new Error("missing origin");
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
throw new Error(`Service "${serviceName}" browser.origins contains an invalid URL: ${origin}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (origins.length === 0) return undefined;
|
|
759
|
+
return { origins };
|
|
760
|
+
}
|
|
761
|
+
|
|
705
762
|
function loadServiceEnv(productDir, envFiles) {
|
|
706
763
|
const env = {};
|
|
707
764
|
for (const envFile of envFiles) {
|