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
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `e2e run` process's stdout and stderr. User code runs in this process
3
+ * too (the config's top-level code, test files while they are collected,
4
+ * reporters) and may print a secret, so every write to either stream goes
5
+ * through a `RunnerOutput`, redacted, until the process exits; only the list
6
+ * reporter writes to the terminal past it.
7
+ */
8
+ import { RunnerOutput } from '../run/process-output.ts';
9
+ /**
10
+ * Takes over `process.stdout` and `process.stderr` for the rest of the
11
+ * process, once: a reporter the run stopped waiting for, or a timer the
12
+ * config or a test file set, can still print after the run. The caller ends
13
+ * the run's output with `end()`, which flushes what is held and stops
14
+ * showing output through the list reporter; later writes stay redacted, and
15
+ * their last unfinished piece prints at exit.
16
+ */
17
+ export declare function claimRunnerOutput(): RunnerOutput;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The `e2e run` process's stdout and stderr. User code runs in this process
3
+ * too (the config's top-level code, test files while they are collected,
4
+ * reporters) and may print a secret, so every write to either stream goes
5
+ * through a `RunnerOutput`, redacted, until the process exits; only the list
6
+ * reporter writes to the terminal past it.
7
+ */
8
+ import { RunnerOutput } from '../run/process-output.js';
9
+ import { processSecrets } from '../run/secrecy.js';
10
+ /**
11
+ * Encodings in which a string spells bytes rather than text: the write puts
12
+ * those bytes on the terminal, so they are what is redacted. A string in any
13
+ * other encoding is the text itself, redacted and written as UTF-8.
14
+ */
15
+ const BYTE_ENCODINGS = new Set(['hex', 'base64', 'base64url']);
16
+ /** The process's claimed output, once claimed: the wrappers stay for the rest of the process. */
17
+ let claimed;
18
+ /**
19
+ * Takes over `process.stdout` and `process.stderr` for the rest of the
20
+ * process, once: a reporter the run stopped waiting for, or a timer the
21
+ * config or a test file set, can still print after the run. The caller ends
22
+ * the run's output with `end()`, which flushes what is held and stops
23
+ * showing output through the list reporter; later writes stay redacted, and
24
+ * their last unfinished piece prints at exit.
25
+ */
26
+ export function claimRunnerOutput() {
27
+ if (claimed !== undefined)
28
+ return claimed;
29
+ const streams = { stdout: process.stdout, stderr: process.stderr };
30
+ const terminal = {
31
+ stdout: streams.stdout.write.bind(streams.stdout),
32
+ stderr: streams.stderr.write.bind(streams.stderr),
33
+ };
34
+ const output = new RunnerOutput(terminal, processSecrets);
35
+ for (const name of ['stdout', 'stderr']) {
36
+ streams[name].write = ((chunk, encoding, callback) => {
37
+ const done = typeof encoding === 'function' ? encoding : callback;
38
+ const bytes = typeof chunk === 'string' && typeof encoding === 'string' && BYTE_ENCODINGS.has(encoding);
39
+ return output.write(name, bytes ? Buffer.from(chunk, encoding) : chunk, done);
40
+ });
41
+ }
42
+ process.once('exit', () => output.end());
43
+ claimed = output;
44
+ return output;
45
+ }
@@ -2,7 +2,7 @@
2
2
  import { realpathSync, statSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { compileGlob, compileGlobList, discoverFiles, GLOB_SYNTAX, literalPrefix, matchesGlob } from '../internal/globs.js';
5
- import { CollectionError } from '../internal/errors.js';
5
+ import { CollectionError, isE2EError } from '../internal/errors.js';
6
6
  import { setupTestId, testId } from '../internal/ids.js';
7
7
  import { explainModuleError } from '../config/diagnose.js';
8
8
  import { importModule } from '../config/load.js';
@@ -338,6 +338,10 @@ export async function collect(config, positionals = []) {
338
338
  uncollected,
339
339
  };
340
340
  }
341
+ /** A `CollectionError` from this module copy or the one a test file's imports loaded. */
342
+ function isCollectionError(cause) {
343
+ return isE2EError(cause) && cause.code === 'COLLECTION_ERROR';
344
+ }
341
345
  /**
342
346
  * Imports each discovered file once in the collection realm. A collection
343
347
  * error in a selected file, or in any file of a run nothing narrowed, throws;
@@ -356,11 +360,11 @@ async function collectFiles(config, discovered, isSelected, narrowed, lines) {
356
360
  }
357
361
  catch (cause) {
358
362
  if (skippable) {
359
- uncollected.push({ file, reason: cause instanceof CollectionError ? cause.message : explainModuleError(cause, absolutePath) });
363
+ uncollected.push({ file, reason: isCollectionError(cause) ? cause.message : explainModuleError(cause, absolutePath) });
360
364
  continue;
361
365
  }
362
366
  // A registration error names the option but not the module it came from.
363
- if (cause instanceof CollectionError)
367
+ if (isCollectionError(cause))
364
368
  throw new CollectionError(`${file}: ${cause.message}`, { cause });
365
369
  throw new CollectionError(`failed to collect ${file}: ${explainModuleError(cause, absolutePath)}`, { cause });
366
370
  }
@@ -368,7 +372,7 @@ async function collectFiles(config, discovered, isSelected, narrowed, lines) {
368
372
  files.push(collectFromRegistration(config.projectRoot, absolutePath, registration, isSelected(file), lines.get(file)));
369
373
  }
370
374
  catch (cause) {
371
- if (!skippable || !(cause instanceof CollectionError))
375
+ if (!skippable || !isCollectionError(cause))
372
376
  throw cause;
373
377
  uncollected.push({ file, reason: cause.message });
374
378
  }
@@ -318,8 +318,9 @@ function validateDescribeOptions(options, parent) {
318
318
  }
319
319
  }
320
320
  /**
321
- * The active collector lives on globalThis because test modules load in an
322
- * isolated module realm (tsx) and must reach the runner's collector instance.
321
+ * The active collector lives on globalThis because a test module may import a
322
+ * copy of e2e other than the runner's and must still reach the runner's
323
+ * collector instance.
323
324
  */
324
325
  const collectorSlot = realmSlot('e2e.activeCollector.v1');
325
326
  /**
@@ -355,12 +356,10 @@ function requireCollector(api) {
355
356
  const PACKAGE_ROOT = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
356
357
  /**
357
358
  * The runner's own source roots, whose frames are never a test's location.
358
- * `dist/` is where the published module runs; `src/` is where tsx's source maps
359
- * relocate those very frames (and where the module runs in this repository).
359
+ * `dist/` is where the published module runs; `src/` is where it runs in this
360
+ * repository.
360
361
  */
361
362
  const RUNNER_ROOTS = ['src', 'dist'].map((dir) => `${path.join(PACKAGE_ROOT, dir)}${path.sep}`);
362
- /** `at name (file:line:column)` or `at file:line:column`, with or without a `file://` scheme. */
363
- const STACK_FRAME = /\(?(?:file:\/\/)?([^()\s]+?):(\d+):(\d+)\)?$/;
364
363
  const NODE_MODULES_SEGMENT = /[\\/]node_modules[\\/]/;
365
364
  /** The path with symlinks resolved, or the path itself when it cannot be resolved. */
366
365
  function realPath(file) {
@@ -371,6 +370,26 @@ function realPath(file) {
371
370
  return file;
372
371
  }
373
372
  }
373
+ /** `at name (location:line:column)` or `at location:line:column`, either one after `async` for an awaiting caller. */
374
+ const STACK_FRAME = /^at (?:async )?(?:[^(]*? \()?(.+?):(\d+):(\d+)\)?$/;
375
+ /**
376
+ * Every frame of the current stack as a source location, the async frames
377
+ * of awaiting callers included (a test file that awaits a helper declaring
378
+ * tests is one), at the source positions Node.js's source maps give them.
379
+ * Read from the stack text: `util.getCallSites` leaves the async frames out,
380
+ * and with source maps on Node.js does not hand `Error.prepareStackTrace`
381
+ * the call sites at all.
382
+ */
383
+ function stackLocations() {
384
+ return (new Error().stack ?? '').split('\n').slice(1).flatMap((line) => {
385
+ const match = STACK_FRAME.exec(line.trim());
386
+ if (match === null)
387
+ return [];
388
+ // An ES module names itself by URL, the loader's cache-busting query included; a mapped frame or CommonJS by path.
389
+ const file = match[1].startsWith('file:') ? fileURLToPath(match[1]) : match[1];
390
+ return [{ file, line: Number(match[2]), column: Number(match[3]) }];
391
+ });
392
+ }
374
393
  /**
375
394
  * Where a test was declared, read off the stack of its `test()` call. The
376
395
  * innermost frame in the module being collected wins, so a test declared
@@ -382,19 +401,11 @@ function realPath(file) {
382
401
  * package's frames are never a test's location.
383
402
  */
384
403
  function captureSource(moduleFile) {
385
- const stack = new Error().stack;
386
- if (stack === undefined)
387
- return undefined;
388
404
  let outsideModule;
389
- for (const line of stack.split('\n').slice(1)) {
390
- const match = STACK_FRAME.exec(line.trim());
391
- if (match === null)
392
- continue;
393
- // The loader imports every module with a cache-busting query, which is not part of the file.
394
- const file = decodeURIComponent(match[1]).replace(/[?#].*$/, '');
405
+ for (const location of stackLocations()) {
406
+ const { file } = location;
395
407
  if (RUNNER_ROOTS.some((root) => file.startsWith(root)) || file.startsWith('node:'))
396
408
  continue;
397
- const location = { file, line: Number(match[2]), column: Number(match[3]) };
398
409
  if (moduleFile !== undefined && (file === moduleFile || realPath(file) === moduleFile)) {
399
410
  return location;
400
411
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The files e2e's loader compiles, and the one policy both of its hooks
3
+ * follow: TypeScript and JSX are compiled wherever they are; the project's
4
+ * tsconfig.json governs a file the project owns (outside `node_modules`),
5
+ * while an installed package's source compiles with defaults, as its author
6
+ * shipped it without the project's settings.
7
+ */
8
+ /** The module system a compiled file runs under. */
9
+ export type ModuleFormat = 'module' | 'commonjs';
10
+ /** What the loader knows about one compiled extension. */
11
+ export interface CompiledExtension {
12
+ /** The syntax oxc parses. */
13
+ readonly lang: 'ts' | 'tsx' | 'jsx';
14
+ readonly format: ModuleFormat;
15
+ }
16
+ /** Whether the `file:` URL `url` is the project's own, outside `node_modules`. */
17
+ export declare function isProjectFile(url: URL): boolean;
18
+ /** A file the loader compiles. */
19
+ export interface CompiledSource {
20
+ readonly file: string;
21
+ readonly kind: CompiledExtension;
22
+ /** Whether the project's tsconfig.json governs it; false for an installed package's source. */
23
+ readonly project: boolean;
24
+ }
25
+ /** The compiled file `url` names, or undefined for any other module. */
26
+ export declare function compiledSource(url: string | undefined): CompiledSource | undefined;
27
+ /**
28
+ * The files TypeScript tries, in order, for an import that wrote
29
+ * `extension` (`x.ts`, `x.tsx`, `x.js`, `x.jsx` for `./x.js`). Empty for an
30
+ * extension TypeScript does not swap.
31
+ */
32
+ export declare function writtenCandidates(extension: string): readonly string[];
33
+ /** Tried, in order, after an extensionless path and after a directory's `index`: what `./x.js` would find, then JSON. */
34
+ export declare const IMPLIED_EXTENSIONS: readonly string[];
35
+ /** The `globalThis` symbol key of the `require` compiled CommonJS uses where Node.js's skips resolve hooks. */
36
+ export declare const HOOKED_REQUIRE_KEY = "e2e.hooked-require.v1";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The files e2e's loader compiles, and the one policy both of its hooks
3
+ * follow: TypeScript and JSX are compiled wherever they are; the project's
4
+ * tsconfig.json governs a file the project owns (outside `node_modules`),
5
+ * while an installed package's source compiles with defaults, as its author
6
+ * shipped it without the project's settings.
7
+ */
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ /** Every extension the loader compiles. */
11
+ const COMPILED = {
12
+ '.ts': { lang: 'ts', format: 'module' },
13
+ '.tsx': { lang: 'tsx', format: 'module' },
14
+ '.jsx': { lang: 'jsx', format: 'module' },
15
+ '.mts': { lang: 'ts', format: 'module' },
16
+ '.cts': { lang: 'ts', format: 'commonjs' },
17
+ };
18
+ /** The files TypeScript tries, in order, for an import that writes each JavaScript extension. */
19
+ const WRITTEN = {
20
+ '.js': ['.ts', '.tsx', '.js', '.jsx'],
21
+ '.jsx': ['.tsx', '.ts', '.jsx', '.js'],
22
+ '.mjs': ['.mts', '.mjs'],
23
+ '.cjs': ['.cts', '.cjs'],
24
+ };
25
+ /** A file inside an installed package. */
26
+ const INSTALLED = /\/node_modules\//;
27
+ /** Whether the `file:` URL `url` is the project's own, outside `node_modules`. */
28
+ export function isProjectFile(url) {
29
+ return url.protocol === 'file:' && !INSTALLED.test(url.pathname);
30
+ }
31
+ /** The compiled file `url` names, or undefined for any other module. */
32
+ export function compiledSource(url) {
33
+ if (url?.startsWith('file:') !== true)
34
+ return undefined;
35
+ const parsed = new URL(url);
36
+ const kind = COMPILED[path.extname(parsed.pathname)];
37
+ return kind === undefined ? undefined : { file: fileURLToPath(parsed), kind, project: isProjectFile(parsed) };
38
+ }
39
+ /**
40
+ * The files TypeScript tries, in order, for an import that wrote
41
+ * `extension` (`x.ts`, `x.tsx`, `x.js`, `x.jsx` for `./x.js`). Empty for an
42
+ * extension TypeScript does not swap.
43
+ */
44
+ export function writtenCandidates(extension) {
45
+ return WRITTEN[extension] ?? [];
46
+ }
47
+ /** Tried, in order, after an extensionless path and after a directory's `index`: what `./x.js` would find, then JSON. */
48
+ export const IMPLIED_EXTENSIONS = [...writtenCandidates('.js'), '.json'];
49
+ /** The `globalThis` symbol key of the `require` compiled CommonJS uses where Node.js's skips resolve hooks. */
50
+ export const HOOKED_REQUIRE_KEY = 'e2e.hooked-require.v1';
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { existsSync, readFileSync } from 'node:fs';
8
8
  import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
9
10
  import { withHint } from '../internal/errors.js';
10
11
  import { addDevDependencyCommand, detectPackageManager } from '../internal/package-manager.js';
11
12
  import { didYouMean, suggest } from '../internal/suggest.js';
@@ -44,18 +45,6 @@ const REMOVED_EXPORTS = {
44
45
  toolAppliesTo: 'toolAppliesTo was removed from e2e/agent: the built-in agent offers a defined tool only on the platforms its annotations name',
45
46
  },
46
47
  };
47
- /**
48
- * The hint for TypeScript an installed package ships under a CommonJS scope:
49
- * the file is there, but it loads through `require`, which the TypeScript
50
- * loader does not hook, so Node reports it missing under the loader's query.
51
- */
52
- function commonJsTypeScriptHint(file) {
53
- let dir = path.dirname(file);
54
- while (!existsSync(path.join(dir, 'package.json')) && path.dirname(dir) !== dir)
55
- dir = path.dirname(dir);
56
- const manifest = path.join(dir, 'package.json');
57
- return `${file} exists: ${manifest} declares no "type": "module", so the file loads as CommonJS, which e2e's TypeScript loader does not transform; import the package's compiled JavaScript, or have the package declare "type": "module"`;
58
- }
59
48
  /**
60
49
  * The message of a failed import of `importer`, extended with what to do
61
50
  * about it when the cause is one the runner recognizes. Any other cause keeps
@@ -63,13 +52,23 @@ function commonJsTypeScriptHint(file) {
63
52
  */
64
53
  export function explainModuleError(cause, importer) {
65
54
  const message = cause instanceof Error ? cause.message : String(cause);
66
- return withHint(message, moduleErrorHint(message, cause?.code, importer));
55
+ return withHint(`${message}${syntaxErrorLocation(cause, message)}`, moduleErrorHint(message, cause?.code, importer));
56
+ }
57
+ /**
58
+ * ` (file:line)` for a SyntaxError Node.js raised linking or parsing a
59
+ * module, which names the line only in the first line of its stack, not in
60
+ * its message; empty for any other error.
61
+ */
62
+ function syntaxErrorLocation(cause, message) {
63
+ if (!(cause instanceof SyntaxError))
64
+ return '';
65
+ const located = /^(.+?):(\d+)\n/.exec(cause.stack ?? '');
66
+ if (located === null || message.includes(located[1]))
67
+ return '';
68
+ const file = located[1].startsWith('file:') ? fileURLToPath(located[1]) : located[1];
69
+ return ` (${file}:${located[2]})`;
67
70
  }
68
71
  function moduleErrorHint(message, code, importer) {
69
- if (code === 'MODULE_NOT_FOUND') {
70
- const loaded = /Cannot find module '([^']+)\?namespace=[^']*'/.exec(message)?.[1];
71
- return loaded !== undefined && existsSync(loaded) ? commonJsTypeScriptHint(loaded) : '';
72
- }
73
72
  if (code === 'ERR_MODULE_NOT_FOUND') {
74
73
  const missing = /Cannot find (?:package|module) '([^']+)'/.exec(message)?.[1];
75
74
  if (missing !== undefined && !missing.startsWith('.') && !path.isAbsolute(missing)) {
@@ -117,8 +116,11 @@ function missingExportHint(specifier, exportName) {
117
116
  const removed = REMOVED_EXPORTS[specifier]?.[exportName];
118
117
  if (removed !== undefined)
119
118
  return removed;
120
- if (specifier !== 'e2e')
121
- return '';
119
+ if (specifier !== 'e2e') {
120
+ if (!/^[A-Z]/.test(exportName))
121
+ return '';
122
+ return `if ${exportName} is a type (an interface or a type alias), import it with import type { ${exportName} }: e2e compiles each file on its own, without type information, so an import of a type has to say so, as under TypeScript's isolatedModules; with emitDecoratorMetadata, the same holds for a type a decorated member's annotation names`;
123
+ }
122
124
  const suggestion = suggest(exportName, RUNTIME_EXPORTS);
123
125
  if (suggestion !== undefined)
124
126
  return `did you mean "${suggestion}"?`;
@@ -1,56 +1,51 @@
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 type { ResolveHook, ResolveHookContext, ResolveHookSync } from 'node:module';
30
- type Resolution = ReturnType<ResolveHookSync>;
31
- /** The tsx namespace e2e registers; tsx tags every URL it loads with it. */
32
- export declare const TSX_NAMESPACE = "e2e";
38
+ import { type LoadHookSync, type ResolveHookSync } from 'node:module';
33
39
  /**
34
40
  * A URL that evaluates `absolutePath` afresh under `key`; with `graph`,
35
41
  * every project file it imports by path or `#` import is fresh too.
36
42
  */
37
43
  export declare function freshModuleURL(absolutePath: string, key: string, graph: boolean): string;
44
+ export declare const resolve: ResolveHookSync;
45
+ export declare const load: LoadHookSync;
38
46
  /**
39
- * The resolution, with TypeScript marked as an ES module when it is reached
40
- * by path, or is a project file reached any other way from a module tsx
41
- * loads under e2e's namespace.
42
- */
43
- export declare function asModule(specifier: string, context: Pick<ResolveHookContext, 'parentURL'>, resolution: Resolution): Resolution;
44
- /**
45
- * The resolution, joined to its importer's fresh graph: a project file
46
- * reached by path or by a `#` import from a module that carries
47
- * `GRAPH_PARAM` carries it too. A package reached by name keeps its shared
48
- * instance, even a workspace package that resolves outside `node_modules`: a
49
- * second instance of it would import a second instance of e2e itself.
47
+ * Registers the hooks once per process, and has stack traces follow the
48
+ * source maps the compiled files carry. Refuses a Node.js whose module hooks
49
+ * the loader cannot rely on (`node-version.ts`).
50
50
  */
51
- export declare function inGraph(specifier: string, context: Pick<ResolveHookContext, 'parentURL'>, resolution: Resolution): Resolution;
52
- /** For `module.registerHooks`. */
53
- export declare const resolveSync: ResolveHookSync;
54
- /** For `module.register`, which reads this export from the hook module. */
55
- export declare const resolve: ResolveHook;
56
- export {};
51
+ export declare function registerLoader(): void;