@warp-drive/holodeck 0.1.0-alpha.84 → 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/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -23
- package/dist/index.js.map +1 -1
- package/dist/mock.d.ts +21 -4
- package/dist/mock.d.ts.map +1 -1
- package/dist/mock.js +99 -71
- package/dist/mock.js.map +1 -1
- package/package.json +12 -12
- package/server/ensure-cert.js +90 -36
- package/server/index.js +8 -25
- package/server/node.js +15 -16
- package/server/tsconfig.json +2 -1
- package/server/bun-worker.js +0 -3
- package/server/bun.js +0 -406
- package/server/compat-shim.js +0 -95
- package/server/node-compat-start.js +0 -12
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.
|
|
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.
|
|
38
|
-
"@warp-drive/legacy": "5.10.0-alpha.
|
|
39
|
-
"@warp-drive/core": "5.10.0-alpha.
|
|
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": {
|
|
@@ -48,16 +47,17 @@
|
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
50
49
|
"@babel/core": "^7.29.7",
|
|
51
|
-
"@babel/plugin-transform-typescript": "^7.29.
|
|
50
|
+
"@babel/plugin-transform-typescript": "^7.29.9",
|
|
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.
|
|
56
|
-
"@warp-drive/legacy": "5.10.0-alpha.
|
|
57
|
-
"@warp-drive/core": "5.10.0-alpha.
|
|
58
|
-
"@warp-drive/internal-config": "5.10.0-alpha.
|
|
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.
|
|
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"
|
package/server/ensure-cert.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 '
|
|
40
|
+
case 'bash':
|
|
13
41
|
return path.join(homedir(), '.bashrc');
|
|
14
|
-
case '
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
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
|
|
10
|
-
|
|
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('./
|
|
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
|
|
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 `
|
|
109
|
+
title: 'Request to the http mock server is missing the `__xTestId` query parameter',
|
|
110
110
|
detail:
|
|
111
|
-
|
|
112
|
-
source: {
|
|
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 `
|
|
128
|
+
title: 'Request to the http mock server is missing the `__xTestRequestNumber` query parameter',
|
|
129
129
|
detail:
|
|
130
|
-
|
|
131
|
-
source: {
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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('
|
|
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('
|
|
401
|
+
console.log(styleText('gray', `\n\tEnding Holodeck Subroutines`));
|
|
403
402
|
project.server.close();
|
|
404
|
-
console.log(styleText('
|
|
403
|
+
console.log(styleText('gray', `\n\tHolodeck program ended`));
|
|
405
404
|
}
|
|
406
405
|
|
|
407
406
|
const endProgram = createCloseHandler(shutdown);
|
package/server/tsconfig.json
CHANGED
package/server/bun-worker.js
DELETED