@warp-drive/holodeck 0.1.0-beta.1 → 0.1.0-beta.2
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/CHANGELOG.md +139 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -210
- package/dist/index.js.map +1 -0
- package/dist/mock.d.ts +89 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +179 -133
- package/dist/mock.js.map +1 -0
- package/package.json +23 -28
- package/server/bun.js +31 -23
- package/server/node.js +42 -33
- package/server/utils.js +55 -0
- package/declarations/index.d.ts +0 -52
- package/declarations/mock.d.ts +0 -85
package/server/node.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { Hono } from 'hono';
|
|
3
1
|
import { serve } from '@hono/node-server';
|
|
4
|
-
import {
|
|
5
|
-
import { logger } from 'hono/logger';
|
|
6
|
-
import { HTTPException } from 'hono/http-exception';
|
|
2
|
+
import { Hono } from 'hono';
|
|
7
3
|
import { cors } from 'hono/cors';
|
|
4
|
+
import { HTTPException } from 'hono/http-exception';
|
|
5
|
+
import { logger } from 'hono/logger';
|
|
8
6
|
import fs from 'node:fs';
|
|
9
|
-
import
|
|
7
|
+
import { createSecureServer } from 'node:http2';
|
|
8
|
+
import { styleText } from 'node:util';
|
|
10
9
|
import { Worker, threadId, parentPort } from 'node:worker_threads';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
11
12
|
import {
|
|
13
|
+
bindWithRetry,
|
|
12
14
|
compress,
|
|
13
15
|
createCloseHandler,
|
|
14
16
|
DEFAULT_PORT,
|
|
@@ -308,26 +310,28 @@ async function _createServer(options) {
|
|
|
308
310
|
hostname: options.hostname ?? 'localhost',
|
|
309
311
|
};
|
|
310
312
|
|
|
311
|
-
const server =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
313
|
+
const server = await bindWithRetry(() =>
|
|
314
|
+
serve({
|
|
315
|
+
overrideGlobalObjects: true,
|
|
316
|
+
fetch: app.fetch,
|
|
317
|
+
serverOptions: {
|
|
318
|
+
key: KEY,
|
|
319
|
+
cert: CERT,
|
|
320
|
+
// rejectUnauthorized: false,
|
|
321
|
+
// enableTrace: true,
|
|
322
|
+
// Allow HTTP/1.1 fallback for ALPN negotiation
|
|
323
|
+
// allowHTTP1: true,
|
|
324
|
+
// ALPNProtocols: ['h2', 'http/1.1', 'http/1.0'],
|
|
325
|
+
// origins: ['*'],
|
|
326
|
+
},
|
|
327
|
+
createServer: createSecureServer,
|
|
328
|
+
port: location.port,
|
|
329
|
+
hostname: location.hostname,
|
|
330
|
+
})
|
|
331
|
+
);
|
|
328
332
|
|
|
329
333
|
console.log(
|
|
330
|
-
`\tServing Holodeck HTTP Mocks from ${
|
|
334
|
+
`\tServing Holodeck HTTP Mocks from ${styleText('yellow', 'https://') + styleText('magenta', location.hostname + ':') + styleText('yellow', String(location.port))}\n`
|
|
331
335
|
);
|
|
332
336
|
|
|
333
337
|
if (typeof threadId === 'number' && threadId !== 0) {
|
|
@@ -363,27 +367,32 @@ export async function launchProgram(config = {}) {
|
|
|
363
367
|
}
|
|
364
368
|
const options = { name, projectRoot, ...config };
|
|
365
369
|
console.log(
|
|
366
|
-
|
|
367
|
-
|
|
370
|
+
styleText(
|
|
371
|
+
'grey',
|
|
372
|
+
`\n\t@${styleText('greenBright', 'warp-drive')}/${styleText(
|
|
373
|
+
'magentaBright',
|
|
368
374
|
'holodeck'
|
|
369
375
|
)} 🌅\n\t=================================\n`
|
|
370
376
|
) +
|
|
371
|
-
|
|
372
|
-
|
|
377
|
+
styleText(
|
|
378
|
+
'grey',
|
|
379
|
+
`\n\tHolodeck Access Granted\n\t\tprogram: ${styleText('magenta', name)}\n\t\tsettings: ${styleText(
|
|
380
|
+
'green',
|
|
373
381
|
JSON.stringify(config).split('\n').join(' ')
|
|
374
|
-
)}\n\t\tdirectory: ${
|
|
382
|
+
)}\n\t\tdirectory: ${styleText('cyan', projectRoot)}\n\t\tengine: ${styleText(
|
|
383
|
+
'cyan',
|
|
375
384
|
'node'
|
|
376
|
-
)}@${
|
|
385
|
+
)}@${styleText('yellow', process.version)}\n`
|
|
377
386
|
)
|
|
378
387
|
);
|
|
379
|
-
console.log(
|
|
388
|
+
console.log(styleText('grey', `\n\tStarting Holodeck Subroutines`));
|
|
380
389
|
|
|
381
390
|
const project = await createServer(options);
|
|
382
391
|
|
|
383
392
|
async function shutdown() {
|
|
384
|
-
console.log(
|
|
393
|
+
console.log(styleText('grey', `\n\tEnding Holodeck Subroutines`));
|
|
385
394
|
project.server.close();
|
|
386
|
-
console.log(
|
|
395
|
+
console.log(styleText('grey', `\n\tHolodeck program ended`));
|
|
387
396
|
}
|
|
388
397
|
|
|
389
398
|
const endProgram = createCloseHandler(shutdown);
|
package/server/utils.js
CHANGED
|
@@ -127,3 +127,58 @@ export function createCloseHandler(cb) {
|
|
|
127
127
|
cb();
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Binds a server created by `createServerFn()`, retrying on the same port if
|
|
133
|
+
* it's already in use.
|
|
134
|
+
*
|
|
135
|
+
* Holodeck's port is never renegotiated on conflict -- every test app's
|
|
136
|
+
* client-side test-helper derives holodeck's URL as `window.location.port +
|
|
137
|
+
* 1` (the diagnostic server's own port, plus one), so holodeck must bind
|
|
138
|
+
* exactly the port it was asked for or the browser can never find it.
|
|
139
|
+
* Instead, this retries the *same* port a few times with a short delay, to
|
|
140
|
+
* ride out the narrow window where a concurrently-starting sibling process's
|
|
141
|
+
* diagnostic server hasn't released this exact port yet (see
|
|
142
|
+
* packages/diagnostic/server/utils/port-lock.js for the reservation scheme
|
|
143
|
+
* that's supposed to prevent that from happening at all -- this is a
|
|
144
|
+
* defense-in-depth fallback for anything outside that convention).
|
|
145
|
+
*
|
|
146
|
+
* `createServerFn` must synchronously create the server and start binding,
|
|
147
|
+
* returning the server instance. Some implementations (Bun.serve) throw
|
|
148
|
+
* synchronously on a bind failure; others (Node's http/http2 `.listen()`,
|
|
149
|
+
* which is what @hono/node-server uses under the hood) bind asynchronously
|
|
150
|
+
* and emit an 'error' event on the returned server instead of throwing. This
|
|
151
|
+
* handles both.
|
|
152
|
+
*/
|
|
153
|
+
export async function bindWithRetry(createServerFn, { maxAttempts = 5, retryDelayMs = 150 } = {}) {
|
|
154
|
+
for (let attempt = 1; ; attempt++) {
|
|
155
|
+
try {
|
|
156
|
+
const server = createServerFn();
|
|
157
|
+
if (typeof server?.on !== 'function') {
|
|
158
|
+
// no event emitter to observe an async bind failure on -- the
|
|
159
|
+
// synchronous creation above already succeeded, so assume the bind
|
|
160
|
+
// did too (this is the Bun.serve path, which throws synchronously
|
|
161
|
+
// instead)
|
|
162
|
+
return server;
|
|
163
|
+
}
|
|
164
|
+
const bindError = await new Promise((resolve) => {
|
|
165
|
+
const onError = (e) => resolve(e);
|
|
166
|
+
server.once('error', onError);
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
server.off('error', onError);
|
|
169
|
+
resolve(null);
|
|
170
|
+
}, retryDelayMs);
|
|
171
|
+
});
|
|
172
|
+
if (!bindError) {
|
|
173
|
+
return server;
|
|
174
|
+
}
|
|
175
|
+
throw bindError;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
if (e?.code !== 'EADDRINUSE' || attempt >= maxAttempts) {
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
console.log(`\tPort in use, retrying bind (attempt ${attempt}/${maxAttempts})...`);
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
package/declarations/index.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { Handler, NextFn } from "@warp-drive/core/request";
|
|
2
|
-
import type { RequestContext, StructuredDataDocument } from "@warp-drive/core/types/request";
|
|
3
|
-
import type { Store } from "@warp-drive/legacy/store";
|
|
4
|
-
import type { ScaffoldGenerator } from "./mock.js";
|
|
5
|
-
/**
|
|
6
|
-
* @public
|
|
7
|
-
*/
|
|
8
|
-
export declare function setConfig({ host }: {
|
|
9
|
-
host: string;
|
|
10
|
-
}): void;
|
|
11
|
-
/**
|
|
12
|
-
* @public
|
|
13
|
-
*/
|
|
14
|
-
export declare function setTestId(context: object, str: string | null): void;
|
|
15
|
-
/**
|
|
16
|
-
* @public
|
|
17
|
-
*/
|
|
18
|
-
export declare function setIsRecording(value: boolean): void;
|
|
19
|
-
/**
|
|
20
|
-
* @public
|
|
21
|
-
*/
|
|
22
|
-
export declare function getIsRecording(): boolean;
|
|
23
|
-
/**
|
|
24
|
-
* A request handler that intercepts requests and routes them through
|
|
25
|
-
* the Holodeck mock server.
|
|
26
|
-
*
|
|
27
|
-
* This handler modifies the request URL to include test identifiers
|
|
28
|
-
* and manages request counts for accurate mocking.
|
|
29
|
-
*
|
|
30
|
-
* Requires that the test context be configured with a testId using `setTestId`.
|
|
31
|
-
*
|
|
32
|
-
* @param owner - the test context object used to retrieve the test ID.
|
|
33
|
-
*/
|
|
34
|
-
export declare class MockServerHandler implements Handler {
|
|
35
|
-
owner: object;
|
|
36
|
-
constructor(owner: object);
|
|
37
|
-
request<T>(context: RequestContext, next: NextFn<T>): Promise<StructuredDataDocument<T>>;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Creates an adapterFor function that wraps the provided adapterFor function
|
|
41
|
-
* to override the adapter's _fetchRequest method to route requests through
|
|
42
|
-
* the Holodeck mock server.
|
|
43
|
-
*
|
|
44
|
-
* @param owner - The test context object used to retrieve the test ID.
|
|
45
|
-
*/
|
|
46
|
-
export declare function installAdapterFor(owner: object, store: Store): void;
|
|
47
|
-
/**
|
|
48
|
-
* Mock a request by sending the scaffold to the mock server.
|
|
49
|
-
*
|
|
50
|
-
* @public
|
|
51
|
-
*/
|
|
52
|
-
export declare function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean): Promise<void>;
|
package/declarations/mock.d.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @public
|
|
3
|
-
*/
|
|
4
|
-
export interface Scaffold {
|
|
5
|
-
status: number;
|
|
6
|
-
statusText?: string;
|
|
7
|
-
headers: Record<string, string>;
|
|
8
|
-
body: Record<string, string> | string | null;
|
|
9
|
-
method: string;
|
|
10
|
-
url: string;
|
|
11
|
-
response: Record<string, unknown>;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* @public
|
|
15
|
-
*/
|
|
16
|
-
export type ScaffoldGenerator = () => Scaffold;
|
|
17
|
-
/**
|
|
18
|
-
* @public
|
|
19
|
-
*/
|
|
20
|
-
export type ResponseGenerator = () => Record<string, unknown>;
|
|
21
|
-
/**
|
|
22
|
-
* Sets up Mocking for a GET request on the mock server
|
|
23
|
-
* for the supplied url.
|
|
24
|
-
*
|
|
25
|
-
* The response body is generated by the supplied response function.
|
|
26
|
-
*
|
|
27
|
-
* Available options:
|
|
28
|
-
* - status: the status code to return (default: 200)
|
|
29
|
-
* - headers: the headers to return (default: {})
|
|
30
|
-
* - body: the body to match against for the request (default: null)
|
|
31
|
-
* - RECORD: whether to record the request (default: false)
|
|
32
|
-
*
|
|
33
|
-
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
34
|
-
* @param response a function which generates the response to return
|
|
35
|
-
* @param options status, headers for the response, body to match against for the request, and whether to record the request
|
|
36
|
-
* @return
|
|
37
|
-
*/
|
|
38
|
-
export declare function GET(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
39
|
-
RECORD?: boolean;
|
|
40
|
-
}): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Mock a POST request
|
|
43
|
-
*/
|
|
44
|
-
export declare function POST(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
45
|
-
RECORD?: boolean;
|
|
46
|
-
}): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* mock a PUT request
|
|
49
|
-
*/
|
|
50
|
-
export declare function PUT(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
51
|
-
RECORD?: boolean;
|
|
52
|
-
}): Promise<void>;
|
|
53
|
-
/**
|
|
54
|
-
* mock a PATCH request
|
|
55
|
-
*
|
|
56
|
-
*/
|
|
57
|
-
export declare function PATCH(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
58
|
-
RECORD?: boolean;
|
|
59
|
-
}): Promise<void>;
|
|
60
|
-
/**
|
|
61
|
-
* mock a DELETE request
|
|
62
|
-
*/
|
|
63
|
-
export declare function DELETE(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
64
|
-
RECORD?: boolean;
|
|
65
|
-
}): Promise<void>;
|
|
66
|
-
/**
|
|
67
|
-
* Sets up Mocking for a HEAD request on the mock server
|
|
68
|
-
* for the supplied url.
|
|
69
|
-
*
|
|
70
|
-
* The response body is generated by the supplied response function.
|
|
71
|
-
*
|
|
72
|
-
* Available options:
|
|
73
|
-
* - status: the status code to return (default: 200)
|
|
74
|
-
* - headers: the headers to return (default: {})
|
|
75
|
-
* - body: the body to match against for the request (default: null)
|
|
76
|
-
* - RECORD: whether to record the request (default: false)
|
|
77
|
-
*
|
|
78
|
-
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
79
|
-
* @param response a function which generates the response to return
|
|
80
|
-
* @param options status, headers for the response, body to match against for the request, and whether to record the request
|
|
81
|
-
* @return
|
|
82
|
-
*/
|
|
83
|
-
export declare function HEAD(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
84
|
-
RECORD?: boolean;
|
|
85
|
-
}): Promise<void>;
|