e2e 0.17.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/ai-sdk.js +5 -4
- package/dist/agent/error.js +3 -3
- package/dist/agent/invocation.js +3 -4
- package/dist/agent/model/tool-choice.d.ts +9 -0
- package/dist/agent/model/tool-choice.js +14 -0
- package/dist/agent/observation-feed.js +3 -4
- package/dist/agent/phases.d.ts +7 -1
- package/dist/agent/phases.js +12 -3
- package/dist/agent/prompts.js +3 -0
- package/dist/agent/replay.js +18 -15
- package/dist/agent/step-cache.d.ts +2 -1
- package/dist/agent/step-cache.js +9 -5
- package/dist/agent/tool-loop.js +92 -5
- package/dist/cache/trace.js +6 -2
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.js +35 -24
- 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/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/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 +2 -0
- package/dist/engine/session.js +2 -2
- package/dist/expect/async.js +2 -1
- package/dist/expect/poll.js +2 -2
- 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 +2 -0
- package/dist/internal/redact.js +4 -0
- package/dist/internal/time.d.ts +21 -5
- package/dist/internal/time.js +35 -3
- package/dist/locator/engine.js +47 -11
- package/dist/locator/screen.js +31 -5
- 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 +55 -14
- package/dist/mcp/tools.js +7 -5
- package/dist/report/list.d.ts +11 -0
- package/dist/report/list.js +23 -6
- package/dist/run/artifacts.d.ts +4 -4
- package/dist/run/artifacts.js +3 -2
- package/dist/run/execute.d.ts +25 -5
- package/dist/run/execute.js +44 -20
- package/dist/run/failure-evidence.js +6 -2
- package/dist/run/in-process.js +51 -10
- 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/records.d.ts +4 -4
- package/dist/run/retry.d.ts +7 -0
- package/dist/run/retry.js +27 -11
- package/dist/run/runner.d.ts +8 -0
- package/dist/run/runner.js +32 -9
- 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 +73 -2
- package/dist/run/serial.js +239 -58
- package/dist/run/standalone.js +1 -1
- package/dist/run/trace-redaction.d.ts +5 -6
- package/dist/run/trace-redaction.js +5 -6
- package/dist/run/worker/entry.js +35 -14
- package/dist/run/worker/handle.js +10 -5
- package/dist/run/worker/protocol.d.ts +83 -3
- package/dist/run/worker/session.js +28 -0
- package/dist/telemetry/environment.d.ts +2 -1
- package/dist/telemetry/environment.js +12 -2
- 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 +6 -3
- package/docs/agent-steps.mdx +1 -0
- package/docs/agents.mdx +1 -0
- package/docs/api-testing.mdx +2 -1
- package/docs/authentication.mdx +3 -2
- package/docs/browser.mdx +7 -6
- package/docs/bug-bash.mdx +2 -1
- package/docs/cache.mdx +2 -1
- package/docs/ci.mdx +3 -2
- package/docs/coding-agents.mdx +2 -1
- package/docs/core-concepts.mdx +208 -0
- package/docs/debugging.mdx +19 -9
- package/docs/decision-models.mdx +142 -0
- package/docs/example-projects.mdx +32 -0
- package/docs/executors.mdx +3 -0
- package/docs/explore.mdx +1 -0
- package/docs/github.mdx +2 -1
- 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 +2 -1
- 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 +27 -4
- package/docs/migrate/selenium.mdx +27 -3
- package/docs/mobile-ci.mdx +6 -5
- package/docs/mobile.mdx +79 -8
- package/docs/models.mdx +7 -2
- package/docs/quickstart.mdx +26 -15
- package/docs/reference/agent.mdx +13 -3
- package/docs/reference/app.mdx +1 -0
- package/docs/reference/cli.mdx +11 -5
- package/docs/reference/config.mdx +42 -6
- package/docs/reference/engine.mdx +22 -10
- package/docs/reference/environment.mdx +2 -1
- package/docs/reference/errors.mdx +11 -8
- package/docs/reference/expect.mdx +2 -1
- package/docs/reference/mcp.mdx +21 -8
- package/docs/reference/mobile.mdx +1 -0
- package/docs/reference/reporters.mdx +1 -0
- package/docs/reference/screen.mdx +19 -9
- package/docs/reference/test.mdx +21 -1
- package/docs/reference/web.mdx +8 -2
- package/docs/security.mdx +33 -27
- package/docs/subscriptions.mdx +1 -0
- package/docs/telemetry.mdx +8 -5
- 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 +11 -7
- package/skills/e2e/SKILL.md +3 -1
- package/skills/e2e/references/agent.md +11 -0
- package/skills/e2e/references/debugging.md +1 -1
- package/skills/e2e/references/mcp.md +8 -3
- package/skills/e2e/references/running.md +2 -1
- package/skills/e2e/references/setup.md +9 -1
- package/skills/e2e/references/writing-tests.md +1 -1
- package/docs/assertions.mdx +0 -94
- package/docs/goals.mdx +0 -176
- package/docs/locators.mdx +0 -64
- package/docs/physical-devices.mdx +0 -76
- package/docs/viewports.mdx +0 -146
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();
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the loader reads about the project besides the modules themselves:
|
|
3
|
+
* the tsconfig.json that governs a file (the nearest one above it, the way
|
|
4
|
+
* `tsc` finds it, with `extends` applied, or the project it references that
|
|
5
|
+
* includes the file) and which candidate files exist.
|
|
6
|
+
*
|
|
7
|
+
* Both are kept with the module graph that read them, the way the graph's
|
|
8
|
+
* modules are: a fresh graph (a config loaded with `graph: true`, which
|
|
9
|
+
* carries `e2e-graph` on every project file) reads its tsconfig.json and
|
|
10
|
+
* file lookups once, as they are when it loads. Every other load in the
|
|
11
|
+
* process shares one view, whose tsconfig.json is read once (collecting
|
|
12
|
+
* fifty test files reads the `extends` chain once) and whose file lookups
|
|
13
|
+
* ask the disk each time, as Node.js's own resolution does, so a test file
|
|
14
|
+
* imported again sees files added or removed since.
|
|
15
|
+
*/
|
|
16
|
+
import { type PathsMatcher, type TsConfigJson } from 'get-tsconfig';
|
|
17
|
+
export type CompilerOptions = TsConfigJson.CompilerOptions;
|
|
18
|
+
export interface ProjectTsconfig {
|
|
19
|
+
readonly compilerOptions: CompilerOptions;
|
|
20
|
+
/** The candidate files a bare specifier maps to through `paths` or `baseUrl`; null when it sets neither. */
|
|
21
|
+
readonly paths: PathsMatcher | null;
|
|
22
|
+
}
|
|
23
|
+
/** Carries the tsconfig.json files the runner warned about to its workers, so each file is reported once per run. */
|
|
24
|
+
export declare const WARNED_TSCONFIGS_ENV = "E2E_WARNED_TSCONFIGS";
|
|
25
|
+
/** The tsconfig.json files warned about so far, as the value of `WARNED_TSCONFIGS_ENV` for a worker. */
|
|
26
|
+
export declare function warnedTsconfigs(): string;
|
|
27
|
+
/** The project as one module graph sees it. */
|
|
28
|
+
export declare class ProjectView {
|
|
29
|
+
/** The nearest tsconfig.json of each directory looked up so far; undefined where none is above it. */
|
|
30
|
+
private readonly nearest;
|
|
31
|
+
/** Each tsconfig.json read so far; undefined for one that could not be read. */
|
|
32
|
+
private readonly read;
|
|
33
|
+
/** get-tsconfig's own cache of the files it read. */
|
|
34
|
+
private readonly reads;
|
|
35
|
+
/** File lookups answered so far, for a graph's view; undefined where the disk is asked each time. */
|
|
36
|
+
private readonly files;
|
|
37
|
+
constructor(memoizeFiles: boolean);
|
|
38
|
+
/**
|
|
39
|
+
* The tsconfig that governs `file`, or undefined when no readable
|
|
40
|
+
* tsconfig.json is above it. A solution-style tsconfig.json (`files: []`
|
|
41
|
+
* and `references`, as Vite writes) hands a file to the referenced
|
|
42
|
+
* tsconfig that includes it, the way TypeScript's editor support does.
|
|
43
|
+
*/
|
|
44
|
+
tsconfigFor(file: string): ProjectTsconfig | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* `tsconfigPath` read with its `extends` chain, or undefined when it
|
|
47
|
+
* cannot be, with a warning: an `extends` naming a package that is not
|
|
48
|
+
* installed (`@tsconfig/node22`) should not stop a run, so TypeScript
|
|
49
|
+
* compiles with default settings and no `paths` instead.
|
|
50
|
+
*/
|
|
51
|
+
private readTsconfig;
|
|
52
|
+
/** Whether `file` is a regular file. */
|
|
53
|
+
isFile(file: string): boolean;
|
|
54
|
+
}
|
|
55
|
+
/** The view of the module graph `graph` names (the `e2e-graph` value of a URL), or the shared one for null. */
|
|
56
|
+
export declare function projectView(graph: string | null): ProjectView;
|