@warp-drive/holodeck 0.1.0-alpha.85 → 0.1.0-alpha.86

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@warp-drive/holodeck",
3
3
  "description": "⚡️ Simple, Fast HTTP Mocking for Tests",
4
- "version": "0.1.0-alpha.85",
4
+ "version": "0.1.0-alpha.86",
5
5
  "license": "MIT",
6
6
  "author": "Chris Thoburn <runspired@users.noreply.github.com>",
7
7
  "repository": {
@@ -23,7 +23,6 @@
23
23
  },
24
24
  "type": "module",
25
25
  "files": [
26
- "bin",
27
26
  "dist",
28
27
  "README.md",
29
28
  "LICENSE.md",
@@ -34,9 +33,9 @@
34
33
  "ensure-cert": "./server/ensure-cert.js"
35
34
  },
36
35
  "peerDependencies": {
37
- "@warp-drive/utilities": "5.10.0-alpha.9",
38
- "@warp-drive/legacy": "5.10.0-alpha.9",
39
- "@warp-drive/core": "5.10.0-alpha.9"
36
+ "@warp-drive/utilities": "5.10.0-alpha.10",
37
+ "@warp-drive/legacy": "5.10.0-alpha.10",
38
+ "@warp-drive/core": "5.10.0-alpha.10"
40
39
  },
41
40
  "peerDependenciesMeta": {
42
41
  "@warp-drive/utilities": {
@@ -52,12 +51,13 @@
52
51
  "@babel/preset-env": "^7.29.7",
53
52
  "@babel/preset-typescript": "^7.29.7",
54
53
  "@babel/runtime": "^7.29.7",
55
- "@warp-drive/utilities": "5.10.0-alpha.9",
56
- "@warp-drive/legacy": "5.10.0-alpha.9",
57
- "@warp-drive/core": "5.10.0-alpha.9",
58
- "@warp-drive/internal-config": "5.10.0-alpha.9",
54
+ "@warp-drive/utilities": "5.10.0-alpha.10",
55
+ "@warp-drive/legacy": "5.10.0-alpha.10",
56
+ "@warp-drive/core": "5.10.0-alpha.10",
57
+ "@warp-drive/internal-config": "5.10.0-alpha.10",
58
+ "@types/node": "^26.6.2",
59
59
  "tsdown": "^0.23.0",
60
- "typescript-7": "npm:typescript@7.1.0-dev.20260919.1"
60
+ "typescript-7": "npm:typescript@7.1.0-dev.20260920.1"
61
61
  },
62
62
  "exports": {
63
63
  ".": {
@@ -80,7 +80,7 @@
80
80
  }
81
81
  },
82
82
  "scripts": {
83
- "check:types": "node ./node_modules/typescript-7/bin/tsc --noEmit",
83
+ "check:types": "node ./node_modules/typescript-7/bin/tsc --noEmit && node ./node_modules/typescript-7/bin/tsc --noEmit -p server",
84
84
  "build:pkg": "node node_modules/tsdown/dist/run.mjs",
85
85
  "sync": "echo \"syncing\"",
86
86
  "start": "node node_modules/tsdown/dist/run.mjs --watch"
@@ -1,57 +1,83 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
+ import { pathToFileURL } from 'node:url';
4
5
  import { homedir, userInfo } from 'os';
5
6
  import path from 'path';
6
7
 
7
- function getShellConfigFilePath() {
8
- const shell = userInfo().shell;
9
- switch (shell) {
10
- case '/bin/zsh':
8
+ const DEFAULT_CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
9
+ const DEFAULT_KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
10
+
11
+ const MKCERT_INSTALL_HINT = `
12
+ mkcert was not found on your PATH. Holodeck serves over TLS and uses mkcert to
13
+ issue a certificate for localhost.
14
+
15
+ macOS brew install mkcert
16
+ Linux sudo apt install libnss3-tools
17
+ then install mkcert from https://github.com/FiloSottile/mkcert/releases
18
+ Windows choco install mkcert
19
+
20
+ Install it and run this command again.
21
+ `;
22
+
23
+ /**
24
+ * `userInfo().shell` reports the login shell from the password database, which
25
+ * is not necessarily the shell the user is running. `$SHELL` is.
26
+ */
27
+ function getShell() {
28
+ return process.env.SHELL || userInfo().shell;
29
+ }
30
+
31
+ /**
32
+ * Shells differ in where they read startup config from. A shell that isn't
33
+ * listed here still gets a working certificate, it just has to export the
34
+ * paths itself.
35
+ */
36
+ function getShellConfigFilePath(shell) {
37
+ switch (path.basename(shell ?? '')) {
38
+ case 'zsh':
11
39
  return path.join(homedir(), '.zshrc');
12
- case '/bin/bash':
40
+ case 'bash':
13
41
  return path.join(homedir(), '.bashrc');
14
- case '/opt/homebrew/bin/fish':
15
- case '/usr/local/bin/fish':
16
- case '/bin/fish':
42
+ case 'fish':
17
43
  return path.join(homedir(), '.config', 'fish', 'config.fish');
18
44
  default:
19
- throw Error(
20
- `Unable to determine configuration file for shell: ${shell}. Manual SSL Cert Setup Required for Holodeck.`
21
- );
45
+ return null;
22
46
  }
23
47
  }
24
48
 
25
- function main() {
26
- let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
27
- let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
28
- const configFilePath = getShellConfigFilePath();
29
-
30
- if (!CERT_PATH || !KEY_PATH) {
31
- console.log(`Environment variables not found, updating the environment config file...\n`);
32
-
33
- if (!CERT_PATH) {
34
- CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
35
- process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
36
- execSync(`echo '\nexport HOLODECK_SSL_CERT_PATH="${CERT_PATH}"' >> ${configFilePath}`);
37
- console.log(`Added HOLODECK_SSL_CERT_PATH to ${configFilePath}`);
38
- }
39
-
40
- if (!KEY_PATH) {
41
- KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
42
- process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
43
- execSync(`echo '\nexport HOLODECK_SSL_KEY_PATH="${KEY_PATH}"' >> ${configFilePath}`);
44
- console.log(`Added HOLODECK_SSL_KEY_PATH to ${configFilePath}`);
45
- }
49
+ function formatEnvLines(shell, vars) {
50
+ const isFish = path.basename(shell ?? '') === 'fish';
46
51
 
47
- console.log(
48
- `\n*** Please restart your terminal session to apply the changes or run \`source ${configFilePath}\`. ***\n`
49
- );
52
+ return Object.entries(vars)
53
+ .map(([name, value]) => (isFish ? `set -gx ${name} "${value}"` : `export ${name}="${value}"`))
54
+ .join('\n');
55
+ }
56
+
57
+ function hasMkcert() {
58
+ try {
59
+ execSync('mkcert -version', { stdio: 'ignore' });
60
+ return true;
61
+ } catch {
62
+ return false;
50
63
  }
64
+ }
65
+
66
+ function main() {
67
+ if (!hasMkcert()) {
68
+ console.error(MKCERT_INSTALL_HINT);
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+
73
+ const CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH ?? DEFAULT_CERT_PATH;
74
+ const KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH ?? DEFAULT_KEY_PATH;
51
75
 
52
76
  if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
53
77
  console.log('SSL certificate or key not found, generating new ones...');
54
78
 
79
+ fs.mkdirSync(path.dirname(CERT_PATH), { recursive: true });
80
+ fs.mkdirSync(path.dirname(KEY_PATH), { recursive: true });
55
81
  execSync(`mkcert -install`);
56
82
  execSync(`mkcert -key-file ${KEY_PATH} -cert-file ${CERT_PATH} localhost`);
57
83
 
@@ -62,6 +88,34 @@ function main() {
62
88
 
63
89
  console.log(`Certificate path: ${CERT_PATH}`);
64
90
  console.log(`Key path: ${KEY_PATH}`);
91
+
92
+ if (process.env.HOLODECK_SSL_CERT_PATH && process.env.HOLODECK_SSL_KEY_PATH) {
93
+ return;
94
+ }
95
+
96
+ const shell = getShell();
97
+ const envLines = formatEnvLines(shell, {
98
+ HOLODECK_SSL_CERT_PATH: CERT_PATH,
99
+ HOLODECK_SSL_KEY_PATH: KEY_PATH,
100
+ });
101
+ const configFilePath = getShellConfigFilePath(shell);
102
+
103
+ if (!configFilePath) {
104
+ console.log(
105
+ `\nCould not determine a startup file for shell: ${shell ?? 'unknown'}.` +
106
+ `\nHolodeck falls back to ${DEFAULT_CERT_PATH} when the environment variables` +
107
+ `\nare unset, so the certificate above already works as it is.` +
108
+ `\nTo set them anyway, add the equivalent of these lines to your shell config:\n\n${envLines}\n`
109
+ );
110
+ return;
111
+ }
112
+
113
+ fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
114
+ fs.appendFileSync(configFilePath, `\n${envLines}\n`);
115
+ console.log(`\nAdded HOLODECK_SSL_CERT_PATH and HOLODECK_SSL_KEY_PATH to ${configFilePath}`);
116
+ console.log(`*** Restart your terminal session or run \`source ${configFilePath}\` to apply. ***\n`);
65
117
  }
66
118
 
67
- main();
119
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
120
+ main();
121
+ }
package/server/index.js CHANGED
@@ -1,37 +1,20 @@
1
- /* global Bun */
2
- import path from 'path';
3
- const isBun = typeof Bun !== 'undefined';
1
+ import path from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
4
  let closeHandler = () => {};
5
5
 
6
6
  export default {
7
7
  async launchProgram(config = {}) {
8
8
  const projectRoot = process.cwd();
9
- const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then(
10
- (pkg) => pkg.name
11
- );
9
+ const pkg = await import(pathToFileURL(path.join(projectRoot, 'package.json')).href, {
10
+ with: { type: 'json' },
11
+ });
12
+ const { name } = pkg.default ?? pkg;
12
13
  const options = { name, projectRoot, ...config };
13
14
 
14
- if (!isBun) {
15
- // @ts-expect-error
16
- options.useWorker = config.useWorker ?? true;
17
- const nodeImpl = await import('./node.js');
18
- const program = await nodeImpl.launchProgram(options);
19
- closeHandler = program.endProgram;
20
- return program.config;
21
- }
22
-
23
- // if we are bun but should use node
24
- if (!config.useBun) {
25
- const compatImpl = await import('./compat-shim.js');
26
- const program = await compatImpl.launchProgram(options);
27
- closeHandler = program.endProgram;
28
- return program.config;
29
- }
30
-
31
- // use bun
32
15
  // @ts-expect-error
33
16
  options.useWorker = config.useWorker ?? true;
34
- const nodeImpl = await import('./bun.js');
17
+ const nodeImpl = await import('./node.js');
35
18
  const program = await nodeImpl.launchProgram(options);
36
19
  closeHandler = program.endProgram;
37
20
  return program.config;
package/server/node.js CHANGED
@@ -2,10 +2,10 @@ import { serve } from '@hono/node-server';
2
2
  import { Hono } from 'hono';
3
3
  import { cors } from 'hono/cors';
4
4
  import { HTTPException } from 'hono/http-exception';
5
- import { logger } from 'hono/logger';
6
5
  import fs from 'node:fs';
7
6
  import { createSecureServer } from 'node:http2';
8
7
  import { Readable } from 'node:stream';
8
+ import { pathToFileURL } from 'node:url';
9
9
  import { styleText } from 'node:util';
10
10
  import { Worker, threadId, parentPort } from 'node:worker_threads';
11
11
  import path from 'path';
@@ -25,7 +25,7 @@ async function replayRequest(context, cacheKey) {
25
25
  let metaJson;
26
26
  try {
27
27
  metaJson = JSON.parse(fs.readFileSync(`${cacheKey}.meta.json`, 'utf8'));
28
- } catch (e) {
28
+ } catch {
29
29
  context.header('Content-Type', 'application/vnd.api+json');
30
30
  context.status(400);
31
31
  return context.body(
@@ -56,7 +56,7 @@ async function replayRequest(context, cacheKey) {
56
56
  metaJson.status !== 204 && metaJson.status < 500 ? Readable.toWeb(fs.createReadStream(bodyPath)) : '';
57
57
 
58
58
  const headers = new Headers(metaJson.headers || {});
59
- const response = new Response(bodyInit, {
59
+ const response = new Response(/** @type {BodyInit} */ (bodyInit), {
60
60
  status: metaJson.status,
61
61
  statusText: metaJson.statusText,
62
62
  headers,
@@ -106,10 +106,10 @@ function createTestHandler(projectRoot) {
106
106
  {
107
107
  status: '400',
108
108
  code: 'MISSING_X_TEST_ID_HEADER',
109
- title: 'Request to the http mock server is missing the `X-Test-Id` header',
109
+ title: 'Request to the http mock server is missing the `__xTestId` query parameter',
110
110
  detail:
111
- "The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
112
- source: { header: 'X-Test-Id' },
111
+ 'The `__xTestId` query parameter identifies the test making the request, so that the mock server only replays fixtures belonging to the test that is currently running. MockServerHandler adds it. Add `new MockServerHandler(this)` to your RequestManager chain ahead of Fetch, and check that the code under test issues its request through that chain rather than calling fetch directly.',
112
+ source: { parameter: '__xTestId' },
113
113
  },
114
114
  ],
115
115
  })
@@ -125,10 +125,10 @@ function createTestHandler(projectRoot) {
125
125
  {
126
126
  status: '400',
127
127
  code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
128
- title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
128
+ title: 'Request to the http mock server is missing the `__xTestRequestNumber` query parameter',
129
129
  detail:
130
- "The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
131
- source: { header: 'X-Test-Request-Number' },
130
+ 'The `__xTestRequestNumber` query parameter counts requests to the same method and url within a test, so that repeated requests replay their own fixtures in order. MockServerHandler adds it. Add `new MockServerHandler(this)` to your RequestManager chain ahead of Fetch, and check that the code under test issues its request through that chain rather than calling fetch directly.',
131
+ source: { parameter: '__xTestRequestNumber' },
132
132
  },
133
133
  ],
134
134
  })
@@ -295,7 +295,6 @@ async function createServer(options) {
295
295
  async function _createServer(options) {
296
296
  const { CERT, KEY } = await getCertInfo();
297
297
  const app = new Hono();
298
- // app.use(logger());
299
298
 
300
299
  app.use(
301
300
  cors({
@@ -369,7 +368,7 @@ async function _createServer(options) {
369
368
 
370
369
  export async function launchProgram(config = {}) {
371
370
  const projectRoot = process.cwd();
372
- const pkg = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } });
371
+ const pkg = await import(pathToFileURL(path.join(projectRoot, 'package.json')).href, { with: { type: 'json' } });
373
372
  const { name } = pkg.default ?? pkg;
374
373
  if (!name) {
375
374
  throw new Error(`Package name not found in package.json`);
@@ -377,14 +376,14 @@ export async function launchProgram(config = {}) {
377
376
  const options = { name, projectRoot, ...config };
378
377
  console.log(
379
378
  styleText(
380
- 'grey',
379
+ 'gray',
381
380
  `\n\t@${styleText('greenBright', 'warp-drive')}/${styleText(
382
381
  'magentaBright',
383
382
  'holodeck'
384
383
  )} 🌅\n\t=================================\n`
385
384
  ) +
386
385
  styleText(
387
- 'grey',
386
+ 'gray',
388
387
  `\n\tHolodeck Access Granted\n\t\tprogram: ${styleText('magenta', name)}\n\t\tsettings: ${styleText(
389
388
  'green',
390
389
  JSON.stringify(config).split('\n').join(' ')
@@ -394,14 +393,14 @@ export async function launchProgram(config = {}) {
394
393
  )}@${styleText('yellow', process.version)}\n`
395
394
  )
396
395
  );
397
- console.log(styleText('grey', `\n\tStarting Holodeck Subroutines`));
396
+ console.log(styleText('gray', `\n\tStarting Holodeck Subroutines`));
398
397
 
399
398
  const project = await createServer(options);
400
399
 
401
400
  async function shutdown() {
402
- console.log(styleText('grey', `\n\tEnding Holodeck Subroutines`));
401
+ console.log(styleText('gray', `\n\tEnding Holodeck Subroutines`));
403
402
  project.server.close();
404
- console.log(styleText('grey', `\n\tHolodeck program ended`));
403
+ console.log(styleText('gray', `\n\tHolodeck program ended`));
405
404
  }
406
405
 
407
406
  const endProgram = createCloseHandler(shutdown);
@@ -4,7 +4,8 @@
4
4
  "target": "ESNext",
5
5
  "module": "ESNext",
6
6
  "moduleResolution": "bundler",
7
- "types": ["bun-types"],
7
+ "types": ["node"],
8
+ "strict": false,
8
9
  "allowJs": true,
9
10
  "checkJs": true,
10
11
  "noEmit": true
@@ -1,3 +0,0 @@
1
- import { startWorker } from './bun.js';
2
-
3
- startWorker();