e2e 0.16.0 → 0.18.0
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 +10 -0
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +7 -4
- package/dist/agent/action-dispatcher.js +4 -5
- package/dist/agent/actions.d.ts +32 -23
- package/dist/agent/actions.js +38 -24
- package/dist/agent/ai-sdk.js +5 -4
- package/dist/agent/error.js +3 -3
- package/dist/agent/invocation.js +3 -4
- package/dist/agent/model/sdk.js +2 -0
- package/dist/agent/model/tool-choice.d.ts +9 -0
- package/dist/agent/model/tool-choice.js +14 -0
- package/dist/agent/observation-feed.d.ts +5 -6
- package/dist/agent/observation-feed.js +8 -11
- package/dist/agent/observation.d.ts +40 -11
- package/dist/agent/observation.js +109 -59
- package/dist/agent/phases.d.ts +7 -1
- package/dist/agent/phases.js +12 -3
- package/dist/agent/point-tap.d.ts +8 -8
- package/dist/agent/point-tap.js +1 -1
- package/dist/agent/prompts.js +3 -0
- package/dist/agent/replay.d.ts +19 -8
- package/dist/agent/replay.js +29 -29
- package/dist/agent/scroll-target.d.ts +2 -3
- package/dist/agent/step-cache.d.ts +43 -15
- package/dist/agent/step-cache.js +156 -67
- package/dist/agent/tool-loop.js +94 -5
- package/dist/cache/anchors.d.ts +72 -23
- package/dist/cache/anchors.js +233 -60
- package/dist/cache/context.d.ts +25 -14
- package/dist/cache/context.js +50 -8
- package/dist/cache/decide.d.ts +2 -2
- package/dist/cache/decide.js +4 -6
- package/dist/cache/identity.d.ts +18 -4
- package/dist/cache/identity.js +9 -4
- package/dist/cache/recorder.d.ts +15 -1
- package/dist/cache/recorder.js +36 -23
- package/dist/cache/rekeyed.d.ts +46 -0
- package/dist/cache/rekeyed.js +89 -0
- package/dist/cache/relocate.d.ts +14 -14
- package/dist/cache/relocate.js +33 -25
- package/dist/cache/route.d.ts +32 -33
- package/dist/cache/route.js +94 -50
- package/dist/cache/store.d.ts +6 -0
- package/dist/cache/store.js +21 -1
- package/dist/cache/trace.d.ts +53 -14
- package/dist/cache/trace.js +76 -30
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.js +43 -32
- package/dist/cli/init/engines.d.ts +1 -3
- package/dist/cli/init/engines.js +2 -2
- package/dist/cli/init/gateways.d.ts +1 -1
- package/dist/cli/init/gateways.js +11 -1
- package/dist/cli/init/mcp-config.d.ts +2 -1
- package/dist/cli/init/mcp-config.js +8 -3
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/cli/init/versions.d.ts +0 -8
- package/dist/cli/init/versions.js +1 -10
- package/dist/cli/mcp.d.ts +3 -2
- package/dist/cli/mcp.js +52 -16
- package/dist/cli/run-output.d.ts +17 -0
- package/dist/cli/run-output.js +45 -0
- package/dist/collect/collect.js +8 -4
- package/dist/collect/registry.js +27 -16
- package/dist/config/compiled-files.d.ts +36 -0
- package/dist/config/compiled-files.js +50 -0
- package/dist/config/diagnose.js +21 -19
- package/dist/config/esm-hooks.d.ts +39 -44
- package/dist/config/esm-hooks.js +238 -63
- package/dist/config/load.d.ts +1 -7
- package/dist/config/load.js +23 -66
- package/dist/config/register.d.ts +8 -0
- package/dist/config/register.js +9 -0
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +33 -1
- package/dist/config/specifiers.d.ts +19 -0
- package/dist/config/specifiers.js +33 -0
- package/dist/config/tsconfig.d.ts +56 -0
- package/dist/config/tsconfig.js +115 -0
- package/dist/config/typescript.d.ts +19 -0
- package/dist/config/typescript.js +253 -0
- package/dist/engine/contract.d.ts +1 -0
- package/dist/engine/index.d.ts +4 -2
- package/dist/engine/session.js +2 -2
- package/dist/expect/async.js +4 -2
- package/dist/expect/poll-scope.d.ts +52 -0
- package/dist/expect/poll-scope.js +113 -0
- package/dist/expect/poll.js +33 -6
- package/dist/expect/values.js +11 -4
- package/dist/internal/abandoned.d.ts +11 -0
- package/dist/internal/abandoned.js +23 -0
- package/dist/internal/brands.d.ts +2 -0
- package/dist/internal/brands.js +2 -0
- package/dist/internal/client-identity.d.ts +9 -0
- package/dist/internal/client-identity.js +14 -0
- package/dist/internal/errors.d.ts +10 -3
- package/dist/internal/errors.js +23 -8
- package/dist/internal/node-version.d.ts +16 -7
- package/dist/internal/node-version.js +50 -15
- package/dist/internal/package-manager.js +1 -1
- package/dist/internal/realm-slot.d.ts +5 -4
- package/dist/internal/redact.d.ts +9 -4
- package/dist/internal/redact.js +120 -23
- package/dist/internal/time.d.ts +26 -4
- package/dist/internal/time.js +58 -14
- package/dist/internal/urls.d.ts +9 -5
- package/dist/internal/urls.js +19 -10
- package/dist/locator/engine.d.ts +12 -2
- package/dist/locator/engine.js +55 -14
- package/dist/locator/screen.js +59 -9
- package/dist/mcp/catalog.d.ts +3 -2
- package/dist/mcp/catalog.js +6 -6
- package/dist/mcp/output.d.ts +59 -0
- package/dist/mcp/output.js +129 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +9 -12
- package/dist/mcp/session.d.ts +8 -1
- package/dist/mcp/session.js +56 -15
- package/dist/mcp/tools.js +7 -5
- package/dist/oauth/chatgpt.js +3 -23
- package/dist/oauth/cli.js +1 -0
- package/dist/oauth/copilot.d.ts +5 -4
- package/dist/oauth/copilot.js +80 -7
- package/dist/oauth/grok.js +2 -1
- package/dist/oauth/models.js +2 -1
- package/dist/oauth/opencode-console.d.ts +10 -0
- package/dist/oauth/opencode-console.js +133 -0
- package/dist/oauth/providers/github-copilot.d.ts +12 -0
- package/dist/oauth/providers/github-copilot.js +77 -23
- package/dist/oauth/providers/opencode-console.d.ts +31 -0
- package/dist/oauth/providers/opencode-console.js +148 -0
- package/dist/oauth/providers.d.ts +2 -6
- package/dist/oauth/providers.js +3 -7
- package/dist/oauth/responses.d.ts +9 -0
- package/dist/oauth/responses.js +23 -0
- package/dist/report/build.d.ts +26 -0
- package/dist/report/build.js +16 -8
- package/dist/report/failure-text.d.ts +6 -1
- package/dist/report/failure-text.js +14 -4
- package/dist/report/format.d.ts +10 -5
- package/dist/report/format.js +12 -5
- package/dist/report/junit.js +11 -12
- package/dist/report/list.d.ts +12 -0
- package/dist/report/list.js +61 -24
- package/dist/report/markdown.d.ts +1 -1
- package/dist/report/markdown.js +37 -14
- package/dist/report/repeats.d.ts +6 -1
- package/dist/report/repeats.js +12 -3
- package/dist/run/artifacts.d.ts +22 -4
- package/dist/run/artifacts.js +71 -3
- package/dist/run/execute.d.ts +34 -5
- package/dist/run/execute.js +93 -44
- package/dist/run/extended-fixtures.js +6 -2
- package/dist/run/failure-evidence.js +10 -4
- package/dist/run/fixtures.js +6 -8
- package/dist/run/in-process.d.ts +2 -0
- package/dist/run/in-process.js +52 -10
- package/dist/run/last-run.d.ts +42 -8
- package/dist/run/last-run.js +121 -14
- package/dist/run/process-output.d.ts +73 -0
- package/dist/run/process-output.js +130 -0
- package/dist/run/provision.d.ts +2 -0
- package/dist/run/provision.js +1 -0
- package/dist/run/realm.d.ts +4 -1
- package/dist/run/realm.js +20 -6
- package/dist/run/records.d.ts +20 -4
- package/dist/run/records.js +26 -0
- package/dist/run/retry.d.ts +10 -1
- package/dist/run/retry.js +30 -12
- package/dist/run/runner.d.ts +9 -1
- package/dist/run/runner.js +101 -39
- package/dist/run/scheduler.js +255 -19
- package/dist/run/secrecy.d.ts +22 -19
- package/dist/run/secrecy.js +22 -18
- package/dist/run/serial.d.ts +75 -2
- package/dist/run/serial.js +243 -59
- package/dist/run/standalone.js +1 -1
- package/dist/run/steps.d.ts +0 -6
- package/dist/run/steps.js +3 -20
- package/dist/run/trace-redaction.d.ts +5 -6
- package/dist/run/trace-redaction.js +5 -6
- package/dist/run/worker/entry.js +40 -18
- package/dist/run/worker/handle.js +10 -5
- package/dist/run/worker/protocol.d.ts +85 -3
- package/dist/run/worker/session.d.ts +2 -0
- package/dist/run/worker/session.js +29 -0
- package/dist/telemetry/environment.d.ts +2 -1
- package/dist/telemetry/environment.js +12 -2
- package/dist/telemetry/events.js +1 -0
- package/dist/telemetry/project.d.ts +5 -6
- package/dist/telemetry/project.js +6 -11
- package/dist/telemetry/store.d.ts +5 -5
- package/dist/telemetry/store.js +5 -7
- package/dist/telemetry/telemetry.d.ts +2 -3
- package/dist/telemetry/telemetry.js +2 -4
- package/dist/types.d.ts +15 -6
- package/docs/agent-steps.mdx +1 -0
- package/docs/agents.mdx +7 -3
- package/docs/api-testing.mdx +2 -1
- package/docs/authentication.mdx +6 -3
- package/docs/browser.mdx +49 -12
- package/docs/bug-bash.mdx +2 -1
- package/docs/cache.mdx +66 -19
- package/docs/ci.mdx +14 -9
- package/docs/coding-agents.mdx +2 -1
- package/docs/core-concepts.mdx +208 -0
- package/docs/debugging.mdx +25 -12
- package/docs/decision-models.mdx +142 -0
- package/docs/example-projects.mdx +32 -0
- package/docs/executors.mdx +7 -2
- package/docs/explore.mdx +1 -0
- package/docs/github.mdx +14 -5
- package/docs/index.mdx +9 -5
- package/docs/integrations/eas.mdx +1 -0
- package/docs/integrations/index.mdx +1 -0
- package/docs/integrations/kernel.mdx +3 -2
- package/docs/migrate/cypress.mdx +26 -3
- package/docs/migrate/detox.mdx +24 -1
- package/docs/migrate/maestro.mdx +28 -4
- package/docs/migrate/playwright.mdx +44 -18
- package/docs/migrate/selenium.mdx +27 -3
- package/docs/mobile-ci.mdx +6 -5
- package/docs/mobile.mdx +96 -8
- package/docs/models.mdx +60 -9
- package/docs/quickstart.mdx +28 -16
- package/docs/reference/agent.mdx +13 -3
- package/docs/reference/app.mdx +2 -1
- package/docs/reference/cli.mdx +41 -18
- package/docs/reference/config.mdx +67 -13
- package/docs/reference/engine.mdx +25 -13
- package/docs/reference/environment.mdx +10 -2
- package/docs/reference/errors.mdx +13 -10
- package/docs/reference/expect.mdx +36 -13
- package/docs/reference/mcp.mdx +21 -8
- package/docs/reference/mobile.mdx +24 -13
- package/docs/reference/reporters.mdx +9 -2
- package/docs/reference/screen.mdx +49 -16
- package/docs/reference/test.mdx +32 -5
- package/docs/reference/web.mdx +133 -23
- package/docs/security.mdx +58 -42
- package/docs/subscriptions.mdx +92 -12
- package/docs/telemetry.mdx +9 -6
- package/docs/tools.mdx +1 -0
- package/docs/{starting-your-app.mdx → web.mdx} +157 -11
- package/docs/writing-an-engine.mdx +2 -1
- package/package.json +25 -8
- package/schema/report-v1.schema.json +14 -1
- package/skills/e2e/SKILL.md +5 -3
- package/skills/e2e/references/agent.md +33 -11
- package/skills/e2e/references/debugging.md +4 -4
- package/skills/e2e/references/mcp.md +8 -3
- package/skills/e2e/references/running.md +15 -9
- package/skills/e2e/references/setup.md +45 -16
- package/skills/e2e/references/writing-tests.md +35 -18
- package/docs/viewports.mdx +0 -146
- package/docs/writing-tests.mdx +0 -256
package/dist/config/esm-hooks.js
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* e2e's TypeScript loader: synchronous module customization hooks
|
|
3
|
+
* (`module.registerHooks`) that run config, test, and helper TypeScript and
|
|
4
|
+
* JSX on Node.js, for `import` and `require` alike. `compiled-files.ts` says
|
|
5
|
+
* which files it compiles and which tsconfig.json governs them.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Resolve: an import written in a compiled file follows TypeScript's rules;
|
|
8
|
+
* every other import, JavaScript's included, resolves as Node.js resolves it.
|
|
9
|
+
* - A relative or absolute path: `./x.js` names the first of `x.ts`,
|
|
10
|
+
* `x.tsx`, `x.js`, and `x.jsx` that exists (`./x.jsx` tries `x.tsx`, `x.ts`,
|
|
11
|
+
* `x.jsx`, `x.js`; `./x.mjs` names `x.mts`, `./x.cjs` names `x.cts`), an
|
|
12
|
+
* extensionless `./x` names `x.ts`, `x.tsx`, `x.js`, `x.jsx`, or `x.json`,
|
|
13
|
+
* and a directory names its index the same way.
|
|
14
|
+
* - A bare specifier the project's tsconfig.json maps through `paths` or
|
|
15
|
+
* `baseUrl` names the mapped file, before any package of that name.
|
|
16
|
+
* - A `#` import or package export that names a missing `./x.js` (or `.jsx`,
|
|
17
|
+
* `.mjs`, `.cjs`) names the TypeScript file behind it, for an `import`.
|
|
13
18
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `.cjs` helper requires, keeps its format, so Node strips its types; forced
|
|
21
|
-
* to ESM there, nothing would. An installed package under `node_modules`
|
|
22
|
-
* keeps the format its own manifest declares; `.cts` files stay CommonJS.
|
|
19
|
+
* Format: `.ts`, `.mts`, `.tsx`, and `.jsx` are ES modules wherever they are
|
|
20
|
+
* and whatever the nearest package.json says, so a Next.js app, or any
|
|
21
|
+
* package without `"type": "module"`, keeps its module type and still runs
|
|
22
|
+
* its TypeScript tests, whether it is imported or a CommonJS file requires it;
|
|
23
|
+
* `.cts` is CommonJS. A JSON file imported without `with { type: 'json' }`
|
|
24
|
+
* loads as a module whose default export is the parsed file.
|
|
23
25
|
*
|
|
24
|
-
*
|
|
25
|
-
* file it reaches by path or `#` import outside `node_modules`,
|
|
26
|
-
* (a config and the base config or targets file
|
|
27
|
-
* with it, while packages stay one shared
|
|
26
|
+
* Freshness: a module imported with the `e2e-graph` query parameter hands it
|
|
27
|
+
* on to every file it reaches by path or `#` import outside `node_modules`,
|
|
28
|
+
* so the project's own graph (a config and the base config or targets file
|
|
29
|
+
* it imports) evaluates afresh with it, while packages stay one shared
|
|
30
|
+
* instance. The graph's tsconfig.json and file lookups are its own too
|
|
31
|
+
* (`tsconfig.ts`).
|
|
32
|
+
*
|
|
33
|
+
* This module, and everything it imports from e2e, is loaded by Node.js's
|
|
34
|
+
* own type stripping when a worker runs from source (`register.ts`), so it
|
|
35
|
+
* holds to erasable TypeScript: no enums, namespaces, or parameter
|
|
36
|
+
* properties.
|
|
28
37
|
*/
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
import { readFileSync } from 'node:fs';
|
|
39
|
+
import nodeModule, {} from 'node:module';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
import { InfrastructureError } from '../internal/errors.js';
|
|
43
|
+
import { unsupportedRuntimeMessage } from '../internal/node-version.js';
|
|
44
|
+
import { realmSlot } from '../internal/realm-slot.js';
|
|
45
|
+
import { compiledSource, HOOKED_REQUIRE_KEY, IMPLIED_EXTENSIONS, isProjectFile, writtenCandidates } from './compiled-files.js';
|
|
46
|
+
import { isPathSpecifier, pathTarget } from './specifiers.js';
|
|
47
|
+
import { projectView } from './tsconfig.js';
|
|
48
|
+
import { compileTypeScript } from './typescript.js';
|
|
49
|
+
/** A URL scheme other than `file:` (`node:`, `data:`, `https:`); never a tsconfig alias. */
|
|
50
|
+
const URL_SCHEME = /^[a-z][\d+.a-z-]*:/i;
|
|
36
51
|
/** The query parameter a fresh module graph carries on every project file. */
|
|
37
52
|
const GRAPH_PARAM = 'e2e-graph';
|
|
38
|
-
/** How `specifier
|
|
53
|
+
/** How `specifier`, as written, reaches its file. */
|
|
39
54
|
function reach(specifier) {
|
|
40
|
-
if (
|
|
55
|
+
if (isPathSpecifier(specifier, false))
|
|
41
56
|
return 'path';
|
|
42
57
|
return specifier.startsWith('#') ? 'own-imports' : 'package';
|
|
43
58
|
}
|
|
44
|
-
/**
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
return undefined;
|
|
48
|
-
const parsed = new URL(url);
|
|
49
|
-
return INSTALLED.test(parsed.pathname) ? undefined : parsed;
|
|
50
|
-
}
|
|
51
|
-
/** A query parameter of the importing module's URL. */
|
|
52
|
-
function parentParam(context, name) {
|
|
53
|
-
const { parentURL } = context;
|
|
54
|
-
return parentURL?.startsWith('file:') === true ? new URL(parentURL).searchParams.get(name) : null;
|
|
59
|
+
/** The `e2e-graph` value of a module URL: the fresh graph it belongs to, or null. */
|
|
60
|
+
function graphOf(url) {
|
|
61
|
+
return url?.startsWith('file:') === true ? new URL(url).searchParams.get(GRAPH_PARAM) : null;
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
64
|
* A URL that evaluates `absolutePath` afresh under `key`; with `graph`,
|
|
@@ -64,19 +71,80 @@ export function freshModuleURL(absolutePath, key, graph) {
|
|
|
64
71
|
url.searchParams.set(GRAPH_PARAM, key);
|
|
65
72
|
return url.href;
|
|
66
73
|
}
|
|
74
|
+
/** The first of `candidates` that is a file. */
|
|
75
|
+
function firstFile(candidates, view) {
|
|
76
|
+
return candidates.find((candidate) => view.isFile(candidate));
|
|
77
|
+
}
|
|
78
|
+
/** The TypeScript file behind `target`'s written extension (`x.ts` for `x.js`), or the file as written. */
|
|
79
|
+
function swappedFile(target, view) {
|
|
80
|
+
const extension = path.extname(target);
|
|
81
|
+
const stem = target.slice(0, target.length - extension.length);
|
|
82
|
+
return firstFile(writtenCandidates(extension).map((candidate) => `${stem}${candidate}`), view);
|
|
83
|
+
}
|
|
84
|
+
/** The `main` a directory's package.json names, as a path, or undefined. */
|
|
85
|
+
function directoryMain(directory, view) {
|
|
86
|
+
const manifest = path.join(directory, 'package.json');
|
|
87
|
+
if (!view.isFile(manifest))
|
|
88
|
+
return undefined;
|
|
89
|
+
let main;
|
|
90
|
+
try {
|
|
91
|
+
({ main } = JSON.parse(readFileSync(manifest, 'utf8')));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return typeof main === 'string' && main !== '' ? path.resolve(directory, main) : undefined;
|
|
97
|
+
}
|
|
67
98
|
/**
|
|
68
|
-
* The
|
|
69
|
-
*
|
|
70
|
-
*
|
|
99
|
+
* The file an import of the path `target` names under TypeScript's rules:
|
|
100
|
+
* the TypeScript file behind a written extension, the file itself, the file
|
|
101
|
+
* an extensionless path leaves out, or for a directory the file its
|
|
102
|
+
* package.json `main` names, else its index.
|
|
71
103
|
*/
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
return
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
function typeScriptFile(target, view) {
|
|
105
|
+
if (writtenCandidates(path.extname(target)).length > 0)
|
|
106
|
+
return swappedFile(target, view);
|
|
107
|
+
if (view.isFile(target))
|
|
108
|
+
return target;
|
|
109
|
+
const implied = firstFile(IMPLIED_EXTENSIONS.map((extension) => `${target}${extension}`), view);
|
|
110
|
+
if (implied !== undefined)
|
|
111
|
+
return implied;
|
|
112
|
+
const main = directoryMain(target, view);
|
|
113
|
+
const fromMain = main === undefined || main === target ? undefined : typeScriptFile(main, view);
|
|
114
|
+
return fromMain ?? firstFile(IMPLIED_EXTENSIONS.map((extension) => `${path.join(target, 'index')}${extension}`), view);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The file an import written in `importer` names under TypeScript's rules,
|
|
118
|
+
* when that is not what Node.js would resolve: a path (`./x.js`, `./x`,
|
|
119
|
+
* `./dir`), or a bare specifier the project's tsconfig maps.
|
|
120
|
+
*/
|
|
121
|
+
function typeScriptTarget(specifier, importer, requiring, view) {
|
|
122
|
+
const byPath = pathTarget(specifier, importer.file, requiring);
|
|
123
|
+
if (byPath !== undefined) {
|
|
124
|
+
const file = typeScriptFile(byPath.file, view);
|
|
125
|
+
return file === undefined || file === byPath.file ? undefined : { file, suffix: byPath.suffix };
|
|
126
|
+
}
|
|
127
|
+
if (!importer.project || specifier.startsWith('#') || URL_SCHEME.test(specifier) || nodeModule.isBuiltin(specifier))
|
|
128
|
+
return undefined;
|
|
129
|
+
for (const candidate of view.tsconfigFor(importer.file)?.paths?.(specifier) ?? []) {
|
|
130
|
+
const file = typeScriptFile(candidate, view);
|
|
131
|
+
if (file !== undefined)
|
|
132
|
+
return { file, suffix: '' };
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
/** Whether `context` is a `require()`, whose resolver takes paths where `import` takes `file:` URLs. */
|
|
137
|
+
function requires(context) {
|
|
138
|
+
return context.conditions.includes('require');
|
|
139
|
+
}
|
|
140
|
+
/** `target` as a specifier the resolver behind `context` accepts. */
|
|
141
|
+
function targetSpecifier(target, context) {
|
|
142
|
+
return requires(context) ? target.file : `${pathToFileURL(target.file).href}${target.suffix}`;
|
|
143
|
+
}
|
|
144
|
+
/** The resolution, with a compiled file's format set to the one the loader compiles it for. */
|
|
145
|
+
function withFormat(resolution) {
|
|
146
|
+
const format = compiledSource(resolution.url)?.kind.format;
|
|
147
|
+
return format === undefined || resolution.format === format ? resolution : { ...resolution, format };
|
|
80
148
|
}
|
|
81
149
|
/**
|
|
82
150
|
* The resolution, joined to its importer's fresh graph: a project file
|
|
@@ -85,17 +153,124 @@ export function asModule(specifier, context, resolution) {
|
|
|
85
153
|
* instance, even a workspace package that resolves outside `node_modules`: a
|
|
86
154
|
* second instance of it would import a second instance of e2e itself.
|
|
87
155
|
*/
|
|
88
|
-
|
|
89
|
-
if (
|
|
156
|
+
function inGraph(how, parentURL, resolution) {
|
|
157
|
+
if (how === 'package' || !resolution.url.startsWith('file:'))
|
|
90
158
|
return resolution;
|
|
91
|
-
const graph =
|
|
92
|
-
const file =
|
|
93
|
-
if (graph === null || file
|
|
159
|
+
const graph = graphOf(parentURL);
|
|
160
|
+
const file = new URL(resolution.url);
|
|
161
|
+
if (graph === null || !isProjectFile(file) || file.searchParams.has(GRAPH_PARAM))
|
|
94
162
|
return resolution;
|
|
95
163
|
file.searchParams.set(GRAPH_PARAM, graph);
|
|
96
164
|
return { ...resolution, url: file.href };
|
|
97
165
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
166
|
+
/** The file TypeScript's rules find for `missing`, a target URL that is not on disk, as a URL with the target's query and hash. */
|
|
167
|
+
function typeScriptFileFor(missing, view) {
|
|
168
|
+
const target = fileURLToPath(missing);
|
|
169
|
+
const file = typeScriptFile(target, view);
|
|
170
|
+
// The file keeps the target's query and hash, which are part of its module identity.
|
|
171
|
+
return file === undefined || file === target ? undefined : `${pathToFileURL(file).href}${missing.search}${missing.hash}`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The resolution of `request`, a `#` import or package export written in a
|
|
175
|
+
* compiled file, moved to the file TypeScript's rules find when the target
|
|
176
|
+
* the map names is not there (`x.ts` for a mapped `./x.js` or `./x`, a
|
|
177
|
+
* directory's index). Node.js throws for such a target, naming it on the
|
|
178
|
+
* error of an `import` (not of a `require()`); a resolver earlier in the
|
|
179
|
+
* chain may return it anyway (Yarn PnP's does), to fail when it loads.
|
|
180
|
+
*/
|
|
181
|
+
function resolveExport(request, context, nextResolve, view) {
|
|
182
|
+
let resolution;
|
|
183
|
+
try {
|
|
184
|
+
resolution = nextResolve(request, context);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const { code, url } = error;
|
|
188
|
+
const file = code === 'ERR_MODULE_NOT_FOUND' && typeof url === 'string' && url.startsWith('file:') ? typeScriptFileFor(new URL(url), view) : undefined;
|
|
189
|
+
if (file === undefined)
|
|
190
|
+
throw error;
|
|
191
|
+
return nextResolve(file, context);
|
|
192
|
+
}
|
|
193
|
+
if (!resolution.url.startsWith('file:'))
|
|
194
|
+
return resolution;
|
|
195
|
+
const target = new URL(resolution.url);
|
|
196
|
+
const file = view.isFile(fileURLToPath(target)) ? undefined : typeScriptFileFor(target, view);
|
|
197
|
+
return file === undefined ? resolution : nextResolve(requires(context) ? fileURLToPath(file) : file, context);
|
|
198
|
+
}
|
|
199
|
+
export const resolve = (specifier, context, nextResolve) => {
|
|
200
|
+
const importer = compiledSource(context.parentURL);
|
|
201
|
+
// Only an import written in a compiled file looks at the project's files.
|
|
202
|
+
const view = importer === undefined ? undefined : projectView(graphOf(context.parentURL));
|
|
203
|
+
const target = importer === undefined || view === undefined ? undefined : typeScriptTarget(specifier, importer, requires(context), view);
|
|
204
|
+
const how = target === undefined ? reach(specifier) : 'path';
|
|
205
|
+
const request = target === undefined ? specifier : targetSpecifier(target, context);
|
|
206
|
+
const resolution = withFormat(view !== undefined && how !== 'path' ? resolveExport(request, context, nextResolve, view) : nextResolve(request, context));
|
|
207
|
+
// CommonJS caches modules by file, with no query to make one fresh.
|
|
208
|
+
return requires(context) ? resolution : inGraph(how, context.parentURL, resolution);
|
|
209
|
+
};
|
|
210
|
+
export const load = (url, context, nextLoad) => {
|
|
211
|
+
const source = compiledSource(url);
|
|
212
|
+
if (source !== undefined) {
|
|
213
|
+
const { format } = source.kind;
|
|
214
|
+
const loaded = nextLoad(url, { ...context, format });
|
|
215
|
+
const compilerOptions = source.project ? (projectView(graphOf(url)).tsconfigFor(source.file)?.compilerOptions ?? {}) : {};
|
|
216
|
+
return { format, source: compileTypeScript(source.file, text(loaded.source, source.file), source.kind, compilerOptions), shortCircuit: true };
|
|
217
|
+
}
|
|
218
|
+
// A require() has no import attributes, whatever the hook types say.
|
|
219
|
+
if (url.startsWith('file:') && path.extname(new URL(url).pathname) === '.json' && !requires(context) && context.importAttributes?.type === undefined) {
|
|
220
|
+
const loaded = nextLoad(url, { ...context, format: 'json', importAttributes: { ...context.importAttributes, type: 'json' } });
|
|
221
|
+
return { format: 'module', source: `export default JSON.parse(${JSON.stringify(text(loaded.source, fileURLToPath(url)))});\n`, shortCircuit: true };
|
|
222
|
+
}
|
|
223
|
+
return nextLoad(url, context);
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Module source as text, a byte order mark dropped, whichever form the next
|
|
227
|
+
* hook returned it in. A hook may hand CommonJS on with no source at all,
|
|
228
|
+
* for Node.js's CommonJS loader to read (Yarn PnP's does); the loader reads
|
|
229
|
+
* the file itself then, as that loader would.
|
|
230
|
+
*/
|
|
231
|
+
function text(source, file) {
|
|
232
|
+
if (source === undefined || source === null)
|
|
233
|
+
return readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
|
|
234
|
+
return typeof source === 'string' ? source.replace(/^\uFEFF/, '') : new TextDecoder().decode(source);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* `require` for the CommonJS module at `filename`, for compiled CommonJS on
|
|
238
|
+
* a Node.js whose `require` in a CommonJS module an ES module imported skips
|
|
239
|
+
* resolve hooks (`typescript.ts`). Its `resolve` runs this module's resolve
|
|
240
|
+
* hook itself: before nodejs/node#62028 (24.15.0; not in Node.js 22),
|
|
241
|
+
* `require.resolve` skips module hooks even on a `require` from
|
|
242
|
+
* `module.createRequire`.
|
|
243
|
+
*/
|
|
244
|
+
function hookedRequire(filename) {
|
|
245
|
+
const base = nodeModule.createRequire(filename);
|
|
246
|
+
const parentURL = pathToFileURL(filename).href;
|
|
247
|
+
const resolveRequest = (request, options) => {
|
|
248
|
+
const { url } = resolve(request, { conditions: ['require', 'node'], importAttributes: {}, parentURL }, (next) => {
|
|
249
|
+
const resolved = base.resolve(next, options);
|
|
250
|
+
return { url: nodeModule.isBuiltin(resolved) ? resolved : pathToFileURL(resolved).href };
|
|
251
|
+
});
|
|
252
|
+
return url.startsWith('file:') ? fileURLToPath(url) : url;
|
|
253
|
+
};
|
|
254
|
+
const hooked = ((request) => base(request));
|
|
255
|
+
return Object.assign(hooked, base, { resolve: Object.assign(resolveRequest, { paths: base.resolve.paths }) });
|
|
256
|
+
}
|
|
257
|
+
/** Marks the process whose module loader already runs these hooks, whichever copy of e2e registered them. */
|
|
258
|
+
const registration = realmSlot('e2e.typescript-loader.v1');
|
|
259
|
+
/** Where compiled CommonJS finds `hookedRequire`. */
|
|
260
|
+
const hookedRequireSlot = realmSlot(HOOKED_REQUIRE_KEY);
|
|
261
|
+
/**
|
|
262
|
+
* Registers the hooks once per process, and has stack traces follow the
|
|
263
|
+
* source maps the compiled files carry. Refuses a Node.js whose module hooks
|
|
264
|
+
* the loader cannot rely on (`node-version.ts`).
|
|
265
|
+
*/
|
|
266
|
+
export function registerLoader() {
|
|
267
|
+
if (registration.get(globalThis) === true)
|
|
268
|
+
return;
|
|
269
|
+
const unsupported = unsupportedRuntimeMessage();
|
|
270
|
+
if (unsupported !== undefined)
|
|
271
|
+
throw new InfrastructureError('NODE_UNSUPPORTED', unsupported);
|
|
272
|
+
nodeModule.registerHooks({ resolve, load });
|
|
273
|
+
registration.set(globalThis, true);
|
|
274
|
+
hookedRequireSlot.set(globalThis, hookedRequire);
|
|
275
|
+
process.setSourceMapsEnabled(true);
|
|
276
|
+
}
|
package/dist/config/load.d.ts
CHANGED
|
@@ -18,12 +18,6 @@ export declare function discoverConfig(cwd: string, explicitPath?: string): Disc
|
|
|
18
18
|
* never for a supplied config value.
|
|
19
19
|
*/
|
|
20
20
|
export declare function missingConfigError(cwd: string): ConfigurationError;
|
|
21
|
-
/**
|
|
22
|
-
* Whether tsx registers synchronous hooks on this Node.js. Hook chains only
|
|
23
|
-
* compose within a kind, so e2e's hook has to be registered the same way.
|
|
24
|
-
* A TypeScript `--import` in NODE_OPTIONS makes tsx fall back to async hooks.
|
|
25
|
-
*/
|
|
26
|
-
export declare function tsxUsesSyncHooks(version?: string, hasRegisterHooks?: boolean, nodeOptions?: string): boolean;
|
|
27
21
|
export interface ConfigLoadOptions {
|
|
28
22
|
/**
|
|
29
23
|
* Evaluates every project file the config reaches by path or `#` import
|
|
@@ -38,7 +32,7 @@ export interface ConfigLoadOptions {
|
|
|
38
32
|
readonly graph?: boolean | undefined;
|
|
39
33
|
}
|
|
40
34
|
/**
|
|
41
|
-
* Imports a TypeScript
|
|
35
|
+
* Imports a TypeScript or JavaScript module through e2e's loader. Every call
|
|
42
36
|
* evaluates the module file afresh: the query carries the caller's key (what
|
|
43
37
|
* the instance is for) plus a process-unique sequence, so a realm never
|
|
44
38
|
* receives another realm's instance of it and a second `run()` in one
|
package/dist/config/load.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/** Config discovery and ESM/TypeScript loading. */
|
|
2
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
3
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
4
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
5
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
return path;
|
|
9
|
+
};
|
|
2
10
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import nodeModule from 'node:module';
|
|
4
11
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { ConfigurationError, isForeignE2EError } from '../internal/errors.js';
|
|
12
|
+
import { ConfigurationError, InfrastructureError, isE2EError } from '../internal/errors.js';
|
|
7
13
|
import { explainModuleError } from './diagnose.js';
|
|
8
|
-
import { freshModuleURL,
|
|
14
|
+
import { freshModuleURL, registerLoader } from './esm-hooks.js';
|
|
9
15
|
const CONFIG_NAMES = ['e2e.config.ts', 'e2e.config.mts'];
|
|
10
16
|
/** Spellings a config author might reach for that the loader does not read. */
|
|
11
17
|
const CONFIG_LOOKALIKES = [
|
|
@@ -67,64 +73,7 @@ export function missingConfigError(cwd) {
|
|
|
67
73
|
return new ConfigurationError('CONFIG_NOT_FOUND', `${searched}; ${remedy}`);
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
70
|
-
*
|
|
71
|
-
* would register a fresh, never-removed loader hook per call - once per
|
|
72
|
-
* collected file and once per realm, so every import would slow every later
|
|
73
|
-
* one. One namespaced registration serves them all.
|
|
74
|
-
*/
|
|
75
|
-
let loader;
|
|
76
|
-
let imports = 0;
|
|
77
|
-
/**
|
|
78
|
-
* Node.js floors per major above which tsx registers synchronous hooks,
|
|
79
|
-
* mirroring tsx 4.23.13's `supportsRegisterHooks`: `module.registerHooks`
|
|
80
|
-
* exists and CommonJS can reload from a sync hook. Earlier releases, and
|
|
81
|
-
* Node 23, get asynchronous hooks.
|
|
82
|
-
*/
|
|
83
|
-
const TSX_SYNC_HOOK_FLOORS = [
|
|
84
|
-
[22, 22, 3],
|
|
85
|
-
[24, 11, 1],
|
|
86
|
-
[25, 1, 0],
|
|
87
|
-
[26, 0, 0],
|
|
88
|
-
];
|
|
89
|
-
/**
|
|
90
|
-
* Whether tsx registers synchronous hooks on this Node.js. Hook chains only
|
|
91
|
-
* compose within a kind, so e2e's hook has to be registered the same way.
|
|
92
|
-
* A TypeScript `--import` in NODE_OPTIONS makes tsx fall back to async hooks.
|
|
93
|
-
*/
|
|
94
|
-
export function tsxUsesSyncHooks(version = process.versions.node, hasRegisterHooks = typeof nodeModule.registerHooks === 'function', nodeOptions = process.env.NODE_OPTIONS ?? '') {
|
|
95
|
-
if (!hasRegisterHooks)
|
|
96
|
-
return false;
|
|
97
|
-
if (/(?:^|\s)--import(?:=|\s+)\S+\.(?:[cm]?ts|tsx)(?:[?#]\S*)?(?=\s|$)/.test(nodeOptions))
|
|
98
|
-
return false;
|
|
99
|
-
const [major = 0, minor = 0, patch = 0] = version.split('.').map(Number);
|
|
100
|
-
const last = TSX_SYNC_HOOK_FLOORS.length - 1;
|
|
101
|
-
const floor = TSX_SYNC_HOOK_FLOORS.find((entry, index) => index === last || entry[0] === major);
|
|
102
|
-
if (floor === undefined)
|
|
103
|
-
return false;
|
|
104
|
-
if (major !== floor[0])
|
|
105
|
-
return major > floor[0];
|
|
106
|
-
if (minor !== floor[1])
|
|
107
|
-
return minor > floor[1];
|
|
108
|
-
return patch >= floor[2];
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Registers e2e's ESM hook, then tsx. The last registered hook runs first, so
|
|
112
|
-
* tsx's resolve calls e2e's and receives `format: 'module'` for project
|
|
113
|
-
* TypeScript; see esm-hooks.ts.
|
|
114
|
-
*/
|
|
115
|
-
function registerLoader() {
|
|
116
|
-
if (tsxUsesSyncHooks()) {
|
|
117
|
-
nodeModule.registerHooks({ resolve: resolveSync });
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// The sibling module with this module's own extension: .js in dist, .ts when
|
|
121
|
-
// tests run from source (tsc rewrites import specifiers, not URLs).
|
|
122
|
-
nodeModule.register(import.meta.url.replace(/load(\.[jt]s)$/, 'esm-hooks$1'));
|
|
123
|
-
}
|
|
124
|
-
return register({ namespace: TSX_NAMESPACE });
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Imports a TypeScript/ESM module with erasable-syntax support. Every call
|
|
76
|
+
* Imports a TypeScript or JavaScript module through e2e's loader. Every call
|
|
128
77
|
* evaluates the module file afresh: the query carries the caller's key (what
|
|
129
78
|
* the instance is for) plus a process-unique sequence, so a realm never
|
|
130
79
|
* receives another realm's instance of it and a second `run()` in one
|
|
@@ -134,13 +83,17 @@ function registerLoader() {
|
|
|
134
83
|
export async function importModule(absolutePath, cacheKey = 'module') {
|
|
135
84
|
return importFresh(absolutePath, cacheKey, false);
|
|
136
85
|
}
|
|
86
|
+
/** Process-unique sequence that keeps every fresh import's URL distinct. */
|
|
87
|
+
let imports = 0;
|
|
137
88
|
async function importFresh(absolutePath, cacheKey, graph) {
|
|
138
89
|
imports += 1;
|
|
139
|
-
|
|
140
|
-
return
|
|
90
|
+
registerLoader();
|
|
91
|
+
return import(__rewriteRelativeImportExtension(freshModuleURL(absolutePath, `${cacheKey}-${imports}`, graph)));
|
|
141
92
|
}
|
|
142
93
|
/** Loads and returns the raw default export of a config module. */
|
|
143
94
|
export async function loadConfigModule(configPath, options = {}) {
|
|
95
|
+
// Outside the try: an unsupported Node.js is that, not a config that failed to load.
|
|
96
|
+
registerLoader();
|
|
144
97
|
let moduleValue;
|
|
145
98
|
try {
|
|
146
99
|
moduleValue = await importFresh(configPath, 'module', options.graph === true);
|
|
@@ -149,11 +102,15 @@ export async function loadConfigModule(configPath, options = {}) {
|
|
|
149
102
|
// A factory the config calls (web(), mobile()) or
|
|
150
103
|
// `secrets.get()` refusing its options at evaluation is a config error
|
|
151
104
|
// with its own code; only a failed import is a load failure.
|
|
152
|
-
|
|
105
|
+
// So is the loader's own failure (oxc's native binding missing): the project's config is not at fault.
|
|
106
|
+
if (cause instanceof ConfigurationError || cause instanceof InfrastructureError)
|
|
153
107
|
throw cause;
|
|
154
|
-
if (
|
|
108
|
+
if (isE2EError(cause) && cause.category === 'configuration') {
|
|
155
109
|
throw new ConfigurationError(cause.code, cause.message, { cause });
|
|
156
110
|
}
|
|
111
|
+
if (isE2EError(cause) && cause.category === 'infrastructure') {
|
|
112
|
+
throw new InfrastructureError(cause.code, cause.message, { cause });
|
|
113
|
+
}
|
|
157
114
|
throw new ConfigurationError('CONFIG_LOAD_FAILED', `failed to load config ${configPath}: ${explainModuleError(cause, configPath)}`, { cause });
|
|
158
115
|
}
|
|
159
116
|
const defaultExport = moduleValue.default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers e2e's TypeScript loader as a side effect, for `node --import`:
|
|
3
|
+
* how a worker runs e2e from its TypeScript source in this repository
|
|
4
|
+
* (`run/worker/handle.ts`). Node.js loads this module and `esm-hooks.ts`
|
|
5
|
+
* with its own type stripping, before the loader exists, so they hold to
|
|
6
|
+
* erasable TypeScript.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers e2e's TypeScript loader as a side effect, for `node --import`:
|
|
3
|
+
* how a worker runs e2e from its TypeScript source in this repository
|
|
4
|
+
* (`run/worker/handle.ts`). Node.js loads this module and `esm-hooks.ts`
|
|
5
|
+
* with its own type stripping, before the loader exists, so they hold to
|
|
6
|
+
* erasable TypeScript.
|
|
7
|
+
*/
|
|
8
|
+
import { registerLoader } from './esm-hooks.js';
|
|
9
|
+
registerLoader();
|
package/dist/config/resolve.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface ResolvedConfig {
|
|
|
34
34
|
readonly assertionTimeout: number;
|
|
35
35
|
readonly cleanupTimeout: number;
|
|
36
36
|
readonly retries: number;
|
|
37
|
+
readonly failOnSkippedFailure: boolean;
|
|
37
38
|
readonly workers: number;
|
|
38
39
|
/** Host store every produced artifact is handed to; undefined keeps files local only. */
|
|
39
40
|
readonly artifactStore: ArtifactStore | undefined;
|
package/dist/config/resolve.js
CHANGED
|
@@ -25,6 +25,7 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
25
25
|
'assertionTimeout',
|
|
26
26
|
'cleanupTimeout',
|
|
27
27
|
'retries',
|
|
28
|
+
'failOnSkippedFailure',
|
|
28
29
|
'workers',
|
|
29
30
|
'artifacts',
|
|
30
31
|
'output',
|
|
@@ -111,6 +112,10 @@ export function resolveConfig(raw, options) {
|
|
|
111
112
|
const workers = boundedInt(cli.workers, '--workers', 1, 1024) ??
|
|
112
113
|
boundedInt(raw.workers, 'workers', 1, 1024) ??
|
|
113
114
|
(ci ? 1 : Math.max(1, Math.floor(os.availableParallelism() / 2)));
|
|
115
|
+
const failOnSkippedFailure = raw.failOnSkippedFailure === undefined ? false : raw.failOnSkippedFailure;
|
|
116
|
+
if (typeof failOnSkippedFailure !== 'boolean') {
|
|
117
|
+
throw new ConfigurationError('INVALID_CONFIG', 'failOnSkippedFailure must be a boolean');
|
|
118
|
+
}
|
|
114
119
|
const artifactStore = resolveArtifactStore(raw);
|
|
115
120
|
const { reporters, customReporters } = resolveReporters(raw, cli);
|
|
116
121
|
const projectId = resolveProjectId(raw.projectId, options.projectRoot);
|
|
@@ -134,6 +139,7 @@ export function resolveConfig(raw, options) {
|
|
|
134
139
|
assertionTimeout,
|
|
135
140
|
cleanupTimeout,
|
|
136
141
|
retries,
|
|
142
|
+
failOnSkippedFailure,
|
|
137
143
|
workers,
|
|
138
144
|
artifactStore,
|
|
139
145
|
output,
|
|
@@ -533,6 +539,8 @@ function resolveSecrets(raw, env) {
|
|
|
533
539
|
const credentials = new Map();
|
|
534
540
|
const secrets = new Map();
|
|
535
541
|
const allSecrets = new Map();
|
|
542
|
+
checkEnvNameCollisions('E2E_USER', 'credentials', Object.keys(raw.credentials ?? {}));
|
|
543
|
+
checkEnvNameCollisions('E2E_SECRET', 'secrets', Object.keys(raw.secrets ?? {}));
|
|
536
544
|
for (const [name, credential] of Object.entries(raw.credentials ?? {})) {
|
|
537
545
|
const prefix = envName('E2E_USER', name);
|
|
538
546
|
const username = env[`${prefix}_USERNAME`] ?? credential.username;
|
|
@@ -561,6 +569,29 @@ function resolveSecrets(raw, env) {
|
|
|
561
569
|
}
|
|
562
570
|
return { credentials, secrets, allSecrets };
|
|
563
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Two entries of one namespace whose names map to the same override variable
|
|
574
|
+
* (`api-key` and `api_key` both read `E2E_SECRET_API_KEY`) would both take one
|
|
575
|
+
* rotated value, so the config load refuses them, whether or not the variable
|
|
576
|
+
* is set. The message names the entries and the variable, never a value.
|
|
577
|
+
*/
|
|
578
|
+
function checkEnvNameCollisions(prefix, namespace, names) {
|
|
579
|
+
const byEnvName = new Map();
|
|
580
|
+
for (const name of names) {
|
|
581
|
+
const variable = envName(prefix, name);
|
|
582
|
+
byEnvName.set(variable, [...(byEnvName.get(variable) ?? []), name]);
|
|
583
|
+
}
|
|
584
|
+
for (const [variable, group] of byEnvName) {
|
|
585
|
+
if (group.length < 2)
|
|
586
|
+
continue;
|
|
587
|
+
const quoted = group.map((name) => `"${name}"`);
|
|
588
|
+
const entries = `${quoted.slice(0, -1).join(', ')} and ${quoted.at(-1)}`;
|
|
589
|
+
const variables = prefix === 'E2E_USER'
|
|
590
|
+
? `the override variables ${variable}_USERNAME and ${variable}_PASSWORD`
|
|
591
|
+
: `the override variable ${variable}`;
|
|
592
|
+
throw new ConfigurationError('INVALID_CONFIG', `${namespace} ${entries} share ${variables}, so one value set there would replace all of them; rename all but one`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
564
595
|
/**
|
|
565
596
|
* Every secret an engine's options hold (a `secrets.get()` the config
|
|
566
597
|
* evaluated before any run existed) names a configured secret, so a typo
|
|
@@ -643,9 +674,10 @@ function computeConfigDigest(raw, projectId, targets) {
|
|
|
643
674
|
// reporter object changes nothing about what a run records, so it never
|
|
644
675
|
// enters the digest either; the built-in ids digest as they always have,
|
|
645
676
|
// so adding a reporter to a config leaves its cache valid.
|
|
677
|
+
// `failOnSkippedFailure` decides only the exit code, never what runs.
|
|
646
678
|
// `targets` digest by declaration below and never enter the clone: an
|
|
647
679
|
// engine holds `secrets.get()` handles, which refuse to serialize.
|
|
648
|
-
const { artifacts: _artifacts, output: _output, trace: _trace, video: _video, targets: _targets, credentials: _credentials, secrets: _secrets, agents, cache, reporters, ...plain } = raw;
|
|
680
|
+
const { failOnSkippedFailure: _failOnSkippedFailure, artifacts: _artifacts, output: _output, trace: _trace, video: _video, targets: _targets, credentials: _credentials, secrets: _secrets, agents, cache, reporters, ...plain } = raw;
|
|
649
681
|
const sanitized = {
|
|
650
682
|
...structuredCloneJsonSafe(plain),
|
|
651
683
|
...(reporters === undefined
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file a relative or absolute specifier names. `import` takes URLs
|
|
3
|
+
* (`./x%20y.ts`, `file:///...`), `require()` takes paths in the platform's
|
|
4
|
+
* own form (`C:\app\helper` and `.\helper` on Windows), so the two read the
|
|
5
|
+
* same text differently.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
/** A file a specifier names, with the query and hash an import URL carried. */
|
|
9
|
+
export interface Target {
|
|
10
|
+
readonly file: string;
|
|
11
|
+
readonly suffix: string;
|
|
12
|
+
}
|
|
13
|
+
/** Whether `specifier` reaches its file by path for an `import` (a URL) or a `require()` (a path on `platform`). */
|
|
14
|
+
export declare function isPathSpecifier(specifier: string, requires: boolean, platform?: typeof path): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The file `specifier`, written in `importer`, names by path, or undefined
|
|
17
|
+
* for a specifier that is not a path.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pathTarget(specifier: string, importer: string, requires: boolean, platform?: typeof path): Target | undefined;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The file a relative or absolute specifier names. `import` takes URLs
|
|
3
|
+
* (`./x%20y.ts`, `file:///...`), `require()` takes paths in the platform's
|
|
4
|
+
* own form (`C:\app\helper` and `.\helper` on Windows), so the two read the
|
|
5
|
+
* same text differently.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
/** A relative or absolute URL an `import` writes. */
|
|
10
|
+
const IMPORT_PATH = /^(?:\.{1,2}\/|\/|file:)/i;
|
|
11
|
+
/** Whether `specifier` reaches its file by path for an `import` (a URL) or a `require()` (a path on `platform`). */
|
|
12
|
+
export function isPathSpecifier(specifier, requires, platform = path) {
|
|
13
|
+
if (!requires)
|
|
14
|
+
return IMPORT_PATH.test(specifier);
|
|
15
|
+
const separators = platform.sep === '\\' ? /^\.{1,2}(?:[\\/]|$)/ : /^\.{1,2}(?:\/|$)/;
|
|
16
|
+
return platform.isAbsolute(specifier) || separators.test(specifier);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The file `specifier`, written in `importer`, names by path, or undefined
|
|
20
|
+
* for a specifier that is not a path.
|
|
21
|
+
*/
|
|
22
|
+
export function pathTarget(specifier, importer, requires, platform = path) {
|
|
23
|
+
if (!isPathSpecifier(specifier, requires, platform))
|
|
24
|
+
return undefined;
|
|
25
|
+
if (requires)
|
|
26
|
+
return { file: platform.resolve(platform.dirname(importer), specifier), suffix: '' };
|
|
27
|
+
const windows = platform.sep === '\\';
|
|
28
|
+
const url = new URL(specifier, pathToFileURL(importer, { windows }));
|
|
29
|
+
const suffix = `${url.search}${url.hash}`;
|
|
30
|
+
url.search = '';
|
|
31
|
+
url.hash = '';
|
|
32
|
+
return { file: fileURLToPath(url, { windows }), suffix };
|
|
33
|
+
}
|