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.
Files changed (256) hide show
  1. package/README.md +10 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +7 -4
  4. package/dist/agent/action-dispatcher.js +4 -5
  5. package/dist/agent/actions.d.ts +32 -23
  6. package/dist/agent/actions.js +38 -24
  7. package/dist/agent/ai-sdk.js +5 -4
  8. package/dist/agent/error.js +3 -3
  9. package/dist/agent/invocation.js +3 -4
  10. package/dist/agent/model/sdk.js +2 -0
  11. package/dist/agent/model/tool-choice.d.ts +9 -0
  12. package/dist/agent/model/tool-choice.js +14 -0
  13. package/dist/agent/observation-feed.d.ts +5 -6
  14. package/dist/agent/observation-feed.js +8 -11
  15. package/dist/agent/observation.d.ts +40 -11
  16. package/dist/agent/observation.js +109 -59
  17. package/dist/agent/phases.d.ts +7 -1
  18. package/dist/agent/phases.js +12 -3
  19. package/dist/agent/point-tap.d.ts +8 -8
  20. package/dist/agent/point-tap.js +1 -1
  21. package/dist/agent/prompts.js +3 -0
  22. package/dist/agent/replay.d.ts +19 -8
  23. package/dist/agent/replay.js +29 -29
  24. package/dist/agent/scroll-target.d.ts +2 -3
  25. package/dist/agent/step-cache.d.ts +43 -15
  26. package/dist/agent/step-cache.js +156 -67
  27. package/dist/agent/tool-loop.js +94 -5
  28. package/dist/cache/anchors.d.ts +72 -23
  29. package/dist/cache/anchors.js +233 -60
  30. package/dist/cache/context.d.ts +25 -14
  31. package/dist/cache/context.js +50 -8
  32. package/dist/cache/decide.d.ts +2 -2
  33. package/dist/cache/decide.js +4 -6
  34. package/dist/cache/identity.d.ts +18 -4
  35. package/dist/cache/identity.js +9 -4
  36. package/dist/cache/recorder.d.ts +15 -1
  37. package/dist/cache/recorder.js +36 -23
  38. package/dist/cache/rekeyed.d.ts +46 -0
  39. package/dist/cache/rekeyed.js +89 -0
  40. package/dist/cache/relocate.d.ts +14 -14
  41. package/dist/cache/relocate.js +33 -25
  42. package/dist/cache/route.d.ts +32 -33
  43. package/dist/cache/route.js +94 -50
  44. package/dist/cache/store.d.ts +6 -0
  45. package/dist/cache/store.js +21 -1
  46. package/dist/cache/trace.d.ts +53 -14
  47. package/dist/cache/trace.js +76 -30
  48. package/dist/cli/bin.js +2 -2
  49. package/dist/cli/index.js +43 -32
  50. package/dist/cli/init/engines.d.ts +1 -3
  51. package/dist/cli/init/engines.js +2 -2
  52. package/dist/cli/init/gateways.d.ts +1 -1
  53. package/dist/cli/init/gateways.js +11 -1
  54. package/dist/cli/init/mcp-config.d.ts +2 -1
  55. package/dist/cli/init/mcp-config.js +8 -3
  56. package/dist/cli/init/sibling-versions.json +1 -1
  57. package/dist/cli/init/versions.d.ts +0 -8
  58. package/dist/cli/init/versions.js +1 -10
  59. package/dist/cli/mcp.d.ts +3 -2
  60. package/dist/cli/mcp.js +52 -16
  61. package/dist/cli/run-output.d.ts +17 -0
  62. package/dist/cli/run-output.js +45 -0
  63. package/dist/collect/collect.js +8 -4
  64. package/dist/collect/registry.js +27 -16
  65. package/dist/config/compiled-files.d.ts +36 -0
  66. package/dist/config/compiled-files.js +50 -0
  67. package/dist/config/diagnose.js +21 -19
  68. package/dist/config/esm-hooks.d.ts +39 -44
  69. package/dist/config/esm-hooks.js +238 -63
  70. package/dist/config/load.d.ts +1 -7
  71. package/dist/config/load.js +23 -66
  72. package/dist/config/register.d.ts +8 -0
  73. package/dist/config/register.js +9 -0
  74. package/dist/config/resolve.d.ts +1 -0
  75. package/dist/config/resolve.js +33 -1
  76. package/dist/config/specifiers.d.ts +19 -0
  77. package/dist/config/specifiers.js +33 -0
  78. package/dist/config/tsconfig.d.ts +56 -0
  79. package/dist/config/tsconfig.js +115 -0
  80. package/dist/config/typescript.d.ts +19 -0
  81. package/dist/config/typescript.js +253 -0
  82. package/dist/engine/contract.d.ts +1 -0
  83. package/dist/engine/index.d.ts +4 -2
  84. package/dist/engine/session.js +2 -2
  85. package/dist/expect/async.js +4 -2
  86. package/dist/expect/poll-scope.d.ts +52 -0
  87. package/dist/expect/poll-scope.js +113 -0
  88. package/dist/expect/poll.js +33 -6
  89. package/dist/expect/values.js +11 -4
  90. package/dist/internal/abandoned.d.ts +11 -0
  91. package/dist/internal/abandoned.js +23 -0
  92. package/dist/internal/brands.d.ts +2 -0
  93. package/dist/internal/brands.js +2 -0
  94. package/dist/internal/client-identity.d.ts +9 -0
  95. package/dist/internal/client-identity.js +14 -0
  96. package/dist/internal/errors.d.ts +10 -3
  97. package/dist/internal/errors.js +23 -8
  98. package/dist/internal/node-version.d.ts +16 -7
  99. package/dist/internal/node-version.js +50 -15
  100. package/dist/internal/package-manager.js +1 -1
  101. package/dist/internal/realm-slot.d.ts +5 -4
  102. package/dist/internal/redact.d.ts +9 -4
  103. package/dist/internal/redact.js +120 -23
  104. package/dist/internal/time.d.ts +26 -4
  105. package/dist/internal/time.js +58 -14
  106. package/dist/internal/urls.d.ts +9 -5
  107. package/dist/internal/urls.js +19 -10
  108. package/dist/locator/engine.d.ts +12 -2
  109. package/dist/locator/engine.js +55 -14
  110. package/dist/locator/screen.js +59 -9
  111. package/dist/mcp/catalog.d.ts +3 -2
  112. package/dist/mcp/catalog.js +6 -6
  113. package/dist/mcp/output.d.ts +59 -0
  114. package/dist/mcp/output.js +129 -0
  115. package/dist/mcp/server.d.ts +4 -0
  116. package/dist/mcp/server.js +9 -12
  117. package/dist/mcp/session.d.ts +8 -1
  118. package/dist/mcp/session.js +56 -15
  119. package/dist/mcp/tools.js +7 -5
  120. package/dist/oauth/chatgpt.js +3 -23
  121. package/dist/oauth/cli.js +1 -0
  122. package/dist/oauth/copilot.d.ts +5 -4
  123. package/dist/oauth/copilot.js +80 -7
  124. package/dist/oauth/grok.js +2 -1
  125. package/dist/oauth/models.js +2 -1
  126. package/dist/oauth/opencode-console.d.ts +10 -0
  127. package/dist/oauth/opencode-console.js +133 -0
  128. package/dist/oauth/providers/github-copilot.d.ts +12 -0
  129. package/dist/oauth/providers/github-copilot.js +77 -23
  130. package/dist/oauth/providers/opencode-console.d.ts +31 -0
  131. package/dist/oauth/providers/opencode-console.js +148 -0
  132. package/dist/oauth/providers.d.ts +2 -6
  133. package/dist/oauth/providers.js +3 -7
  134. package/dist/oauth/responses.d.ts +9 -0
  135. package/dist/oauth/responses.js +23 -0
  136. package/dist/report/build.d.ts +26 -0
  137. package/dist/report/build.js +16 -8
  138. package/dist/report/failure-text.d.ts +6 -1
  139. package/dist/report/failure-text.js +14 -4
  140. package/dist/report/format.d.ts +10 -5
  141. package/dist/report/format.js +12 -5
  142. package/dist/report/junit.js +11 -12
  143. package/dist/report/list.d.ts +12 -0
  144. package/dist/report/list.js +61 -24
  145. package/dist/report/markdown.d.ts +1 -1
  146. package/dist/report/markdown.js +37 -14
  147. package/dist/report/repeats.d.ts +6 -1
  148. package/dist/report/repeats.js +12 -3
  149. package/dist/run/artifacts.d.ts +22 -4
  150. package/dist/run/artifacts.js +71 -3
  151. package/dist/run/execute.d.ts +34 -5
  152. package/dist/run/execute.js +93 -44
  153. package/dist/run/extended-fixtures.js +6 -2
  154. package/dist/run/failure-evidence.js +10 -4
  155. package/dist/run/fixtures.js +6 -8
  156. package/dist/run/in-process.d.ts +2 -0
  157. package/dist/run/in-process.js +52 -10
  158. package/dist/run/last-run.d.ts +42 -8
  159. package/dist/run/last-run.js +121 -14
  160. package/dist/run/process-output.d.ts +73 -0
  161. package/dist/run/process-output.js +130 -0
  162. package/dist/run/provision.d.ts +2 -0
  163. package/dist/run/provision.js +1 -0
  164. package/dist/run/realm.d.ts +4 -1
  165. package/dist/run/realm.js +20 -6
  166. package/dist/run/records.d.ts +20 -4
  167. package/dist/run/records.js +26 -0
  168. package/dist/run/retry.d.ts +10 -1
  169. package/dist/run/retry.js +30 -12
  170. package/dist/run/runner.d.ts +9 -1
  171. package/dist/run/runner.js +101 -39
  172. package/dist/run/scheduler.js +255 -19
  173. package/dist/run/secrecy.d.ts +22 -19
  174. package/dist/run/secrecy.js +22 -18
  175. package/dist/run/serial.d.ts +75 -2
  176. package/dist/run/serial.js +243 -59
  177. package/dist/run/standalone.js +1 -1
  178. package/dist/run/steps.d.ts +0 -6
  179. package/dist/run/steps.js +3 -20
  180. package/dist/run/trace-redaction.d.ts +5 -6
  181. package/dist/run/trace-redaction.js +5 -6
  182. package/dist/run/worker/entry.js +40 -18
  183. package/dist/run/worker/handle.js +10 -5
  184. package/dist/run/worker/protocol.d.ts +85 -3
  185. package/dist/run/worker/session.d.ts +2 -0
  186. package/dist/run/worker/session.js +29 -0
  187. package/dist/telemetry/environment.d.ts +2 -1
  188. package/dist/telemetry/environment.js +12 -2
  189. package/dist/telemetry/events.js +1 -0
  190. package/dist/telemetry/project.d.ts +5 -6
  191. package/dist/telemetry/project.js +6 -11
  192. package/dist/telemetry/store.d.ts +5 -5
  193. package/dist/telemetry/store.js +5 -7
  194. package/dist/telemetry/telemetry.d.ts +2 -3
  195. package/dist/telemetry/telemetry.js +2 -4
  196. package/dist/types.d.ts +15 -6
  197. package/docs/agent-steps.mdx +1 -0
  198. package/docs/agents.mdx +7 -3
  199. package/docs/api-testing.mdx +2 -1
  200. package/docs/authentication.mdx +6 -3
  201. package/docs/browser.mdx +49 -12
  202. package/docs/bug-bash.mdx +2 -1
  203. package/docs/cache.mdx +66 -19
  204. package/docs/ci.mdx +14 -9
  205. package/docs/coding-agents.mdx +2 -1
  206. package/docs/core-concepts.mdx +208 -0
  207. package/docs/debugging.mdx +25 -12
  208. package/docs/decision-models.mdx +142 -0
  209. package/docs/example-projects.mdx +32 -0
  210. package/docs/executors.mdx +7 -2
  211. package/docs/explore.mdx +1 -0
  212. package/docs/github.mdx +14 -5
  213. package/docs/index.mdx +9 -5
  214. package/docs/integrations/eas.mdx +1 -0
  215. package/docs/integrations/index.mdx +1 -0
  216. package/docs/integrations/kernel.mdx +3 -2
  217. package/docs/migrate/cypress.mdx +26 -3
  218. package/docs/migrate/detox.mdx +24 -1
  219. package/docs/migrate/maestro.mdx +28 -4
  220. package/docs/migrate/playwright.mdx +44 -18
  221. package/docs/migrate/selenium.mdx +27 -3
  222. package/docs/mobile-ci.mdx +6 -5
  223. package/docs/mobile.mdx +96 -8
  224. package/docs/models.mdx +60 -9
  225. package/docs/quickstart.mdx +28 -16
  226. package/docs/reference/agent.mdx +13 -3
  227. package/docs/reference/app.mdx +2 -1
  228. package/docs/reference/cli.mdx +41 -18
  229. package/docs/reference/config.mdx +67 -13
  230. package/docs/reference/engine.mdx +25 -13
  231. package/docs/reference/environment.mdx +10 -2
  232. package/docs/reference/errors.mdx +13 -10
  233. package/docs/reference/expect.mdx +36 -13
  234. package/docs/reference/mcp.mdx +21 -8
  235. package/docs/reference/mobile.mdx +24 -13
  236. package/docs/reference/reporters.mdx +9 -2
  237. package/docs/reference/screen.mdx +49 -16
  238. package/docs/reference/test.mdx +32 -5
  239. package/docs/reference/web.mdx +133 -23
  240. package/docs/security.mdx +58 -42
  241. package/docs/subscriptions.mdx +92 -12
  242. package/docs/telemetry.mdx +9 -6
  243. package/docs/tools.mdx +1 -0
  244. package/docs/{starting-your-app.mdx → web.mdx} +157 -11
  245. package/docs/writing-an-engine.mdx +2 -1
  246. package/package.json +25 -8
  247. package/schema/report-v1.schema.json +14 -1
  248. package/skills/e2e/SKILL.md +5 -3
  249. package/skills/e2e/references/agent.md +33 -11
  250. package/skills/e2e/references/debugging.md +4 -4
  251. package/skills/e2e/references/mcp.md +8 -3
  252. package/skills/e2e/references/running.md +15 -9
  253. package/skills/e2e/references/setup.md +45 -16
  254. package/skills/e2e/references/writing-tests.md +35 -18
  255. package/docs/viewports.mdx +0 -146
  256. package/docs/writing-tests.mdx +0 -256
@@ -1,57 +1,64 @@
1
1
  /**
2
- * Module customization hook: the TypeScript a project owns is ESM, whatever
3
- * the nearest package.json says, and a module graph loaded afresh stays
4
- * fresh down every path import.
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
- * Config and tests load through tsx's ESM API. tsx types a `.ts` file by its
7
- * package scope and hands a CommonJS-scoped one to Node's require, which that
8
- * API never hooks, so the import fails as "cannot find module" on a path that
9
- * carries e2e's cache-busting query. Registered before tsx, this hook runs
10
- * inside tsx's chain: tsx's resolve receives `format: 'module'` from it and
11
- * transforms the file as ESM. A Next.js app, or any package without
12
- * `"type": "module"`, keeps its module type.
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
- * Forced: a file reached by path (the entry file URL, `./helper.ts`, and
15
- * tsconfig `paths` aliases, which tsx maps to `file:` URLs on every platform
16
- * before calling this hook), and TypeScript a bare or `#` specifier resolves
17
- * to outside `node_modules` from a module tsx loads for e2e: a workspace
18
- * package that exports its `.ts` source, the monorepo pattern, would
19
- * otherwise hit the same dead end. A module Node loads itself, such as one a
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
- * A module imported with the `e2e-graph` query parameter hands it on to every
25
- * file it reaches by path or `#` import outside `node_modules`, so the project's own graph
26
- * (a config and the base config or targets file it imports) evaluates afresh
27
- * with it, while packages stay one shared instance.
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 { pathToFileURL } from 'node:url';
30
- const TYPESCRIPT_MODULE = /\.(?:ts|mts|tsx)$/;
31
- const PATH_SPECIFIER = /^(?:\.{1,2}\/|\/|file:)/;
32
- /** A file inside an installed package. */
33
- const INSTALLED = /\/node_modules\//;
34
- /** The tsx namespace e2e registers; tsx tags every URL it loads with it. */
35
- export const TSX_NAMESPACE = 'e2e';
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` reaches its file. */
53
+ /** How `specifier`, as written, reaches its file. */
39
54
  function reach(specifier) {
40
- if (PATH_SPECIFIER.test(specifier))
55
+ if (isPathSpecifier(specifier, false))
41
56
  return 'path';
42
57
  return specifier.startsWith('#') ? 'own-imports' : 'package';
43
58
  }
44
- /** A file the project owns: a `file:` URL outside `node_modules`. */
45
- function projectFile(url) {
46
- if (!url.startsWith('file:'))
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 resolution, with TypeScript marked as an ES module when it is reached
69
- * by path, or is a project file reached any other way from a module tsx
70
- * loads under e2e's namespace.
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
- export function asModule(specifier, context, resolution) {
73
- if (resolution.format === 'module' || resolution.format === 'module-typescript')
74
- return resolution;
75
- if (!resolution.url.startsWith('file:') || !TYPESCRIPT_MODULE.test(new URL(resolution.url).pathname))
76
- return resolution;
77
- const forced = reach(specifier) === 'path' ||
78
- (projectFile(resolution.url) !== undefined && parentParam(context, 'tsx-namespace') === TSX_NAMESPACE);
79
- return forced ? { ...resolution, format: 'module' } : resolution;
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
- export function inGraph(specifier, context, resolution) {
89
- if (reach(specifier) === 'package')
156
+ function inGraph(how, parentURL, resolution) {
157
+ if (how === 'package' || !resolution.url.startsWith('file:'))
90
158
  return resolution;
91
- const graph = parentParam(context, GRAPH_PARAM);
92
- const file = projectFile(resolution.url);
93
- if (graph === null || file === undefined || file.searchParams.has(GRAPH_PARAM))
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
- /** For `module.registerHooks`. */
99
- export const resolveSync = (specifier, context, nextResolve) => inGraph(specifier, context, asModule(specifier, context, nextResolve(specifier, context)));
100
- /** For `module.register`, which reads this export from the hook module. */
101
- export const resolve = async (specifier, context, nextResolve) => inGraph(specifier, context, asModule(specifier, context, await nextResolve(specifier, context)));
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
+ }
@@ -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/ESM module with erasable-syntax support. Every call
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
@@ -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 { register } from 'tsx/esm/api';
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, resolveSync, TSX_NAMESPACE } from './esm-hooks.js';
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
- * The process-wide TypeScript loader, registered on first use. `tsImport`
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
- loader ??= registerLoader();
140
- return loader.import(freshModuleURL(absolutePath, `${cacheKey}-${imports}`, graph), import.meta.url);
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
- if (cause instanceof ConfigurationError)
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 (isForeignE2EError(cause) && cause.category === 'configuration') {
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();
@@ -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;
@@ -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
+ }