@warp-drive/holodeck 0.1.0-alpha.15 → 0.1.0-alpha.17
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/README.md +1 -1
- package/declarations/index.d.ts +1 -7
- package/dist/index.js +33 -8
- package/package.json +8 -8
- package/server/bun-worker.js +3 -0
- package/server/bun.js +393 -0
- package/server/compat-shim.js +95 -0
- package/server/index.js +24 -499
- package/server/node-compat-start.js +12 -0
- package/server/node-worker.js +3 -0
- package/server/node.js +401 -0
- package/server/utils.js +128 -0
- package/server/start-node.js +0 -3
- package/server/worker.js +0 -3
package/server/node.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { serve } from '@hono/node-server';
|
|
4
|
+
import { createSecureServer } from 'node:http2';
|
|
5
|
+
import { logger } from 'hono/logger';
|
|
6
|
+
import { HTTPException } from 'hono/http-exception';
|
|
7
|
+
import { cors } from 'hono/cors';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { Worker, threadId, parentPort } from 'node:worker_threads';
|
|
11
|
+
import {
|
|
12
|
+
compress,
|
|
13
|
+
createCloseHandler,
|
|
14
|
+
DEFAULT_PORT,
|
|
15
|
+
generateFileDir,
|
|
16
|
+
generateFilepath,
|
|
17
|
+
getCertInfo,
|
|
18
|
+
getNiceUrl,
|
|
19
|
+
} from './utils.js';
|
|
20
|
+
|
|
21
|
+
async function replayRequest(context, cacheKey) {
|
|
22
|
+
let metaJson;
|
|
23
|
+
try {
|
|
24
|
+
metaJson = JSON.parse(fs.readFileSync(`${cacheKey}.meta.json`, 'utf8'));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
27
|
+
context.status(400);
|
|
28
|
+
return context.body(
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
errors: [
|
|
31
|
+
{
|
|
32
|
+
status: '400',
|
|
33
|
+
code: 'MOCK_NOT_FOUND',
|
|
34
|
+
title: 'Mock not found',
|
|
35
|
+
detail: `No meta was found for ${context.req.method} ${context.req.url}. The expected cacheKey was ${cacheKey}. You may need to record a mock for this request.`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const bodyPath = `${cacheKey}.body.br`;
|
|
44
|
+
const bodyInit = metaJson.status !== 204 && metaJson.status < 500 ? fs.createReadStream(bodyPath) : '';
|
|
45
|
+
|
|
46
|
+
const headers = new Headers(metaJson.headers || {});
|
|
47
|
+
// @ts-expect-error - createReadStream is supported in node
|
|
48
|
+
const response = new Response(bodyInit, {
|
|
49
|
+
status: metaJson.status,
|
|
50
|
+
statusText: metaJson.statusText,
|
|
51
|
+
headers,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (metaJson.status > 400) {
|
|
55
|
+
throw new HTTPException(metaJson.status, { res: response, message: metaJson.statusText });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return response;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e instanceof HTTPException) {
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
64
|
+
context.status(500);
|
|
65
|
+
return context.body(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
errors: [
|
|
68
|
+
{
|
|
69
|
+
status: '500',
|
|
70
|
+
code: 'MOCK_SERVER_ERROR',
|
|
71
|
+
title: 'Mock Replay Failed',
|
|
72
|
+
detail: `Failed to create the response for ${context.req.method} ${context.req.url}.\n\n\n${e.message}\n${e.stack}`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createTestHandler(projectRoot) {
|
|
81
|
+
const TestHandler = async (context) => {
|
|
82
|
+
try {
|
|
83
|
+
const { req } = context;
|
|
84
|
+
|
|
85
|
+
const testId = req.query('__xTestId');
|
|
86
|
+
const testRequestNumber = req.query('__xTestRequestNumber');
|
|
87
|
+
const niceUrl = getNiceUrl(req.url);
|
|
88
|
+
|
|
89
|
+
if (!testId) {
|
|
90
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
91
|
+
context.status(400);
|
|
92
|
+
return context.body(
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
errors: [
|
|
95
|
+
{
|
|
96
|
+
status: '400',
|
|
97
|
+
code: 'MISSING_X_TEST_ID_HEADER',
|
|
98
|
+
title: 'Request to the http mock server is missing the `X-Test-Id` header',
|
|
99
|
+
detail:
|
|
100
|
+
"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.",
|
|
101
|
+
source: { header: 'X-Test-Id' },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!testRequestNumber) {
|
|
109
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
110
|
+
context.status(400);
|
|
111
|
+
return context.body(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
errors: [
|
|
114
|
+
{
|
|
115
|
+
status: '400',
|
|
116
|
+
code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
|
|
117
|
+
title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
|
|
118
|
+
detail:
|
|
119
|
+
"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.",
|
|
120
|
+
source: { header: 'X-Test-Request-Number' },
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (req.method === 'POST' && niceUrl === '__record') {
|
|
128
|
+
const payload = await req.json();
|
|
129
|
+
const { url, headers, method, status, statusText, body, response } = payload;
|
|
130
|
+
const cacheKey = generateFilepath({
|
|
131
|
+
projectRoot,
|
|
132
|
+
testId,
|
|
133
|
+
url,
|
|
134
|
+
method,
|
|
135
|
+
body,
|
|
136
|
+
testRequestNumber,
|
|
137
|
+
});
|
|
138
|
+
const compressedResponse = compress(JSON.stringify(response));
|
|
139
|
+
// allow Content-Type to be overridden
|
|
140
|
+
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
|
|
141
|
+
// We always compress and chunk the response
|
|
142
|
+
headers['Content-Encoding'] = 'br';
|
|
143
|
+
// we don't cache since tests will often reuse similar urls for different payload
|
|
144
|
+
headers['Cache-Control'] = 'no-store';
|
|
145
|
+
// streaming requires Content-Length
|
|
146
|
+
headers['Content-Length'] = compressedResponse.length;
|
|
147
|
+
|
|
148
|
+
const cacheDir = generateFileDir({
|
|
149
|
+
projectRoot,
|
|
150
|
+
testId,
|
|
151
|
+
url,
|
|
152
|
+
method,
|
|
153
|
+
testRequestNumber,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
157
|
+
|
|
158
|
+
fs.writeFileSync(
|
|
159
|
+
`${cacheKey}.meta.json`,
|
|
160
|
+
JSON.stringify({ url, status, statusText, headers, method, requestBody: body })
|
|
161
|
+
);
|
|
162
|
+
fs.writeFileSync(`${cacheKey}.body.br`, compressedResponse);
|
|
163
|
+
|
|
164
|
+
context.status(201);
|
|
165
|
+
return context.body(
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
message: `Recorded ${method} ${url} for test ${testId} request #${testRequestNumber}`,
|
|
168
|
+
cacheKey,
|
|
169
|
+
cacheDir,
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
const body = req.raw.body ? await req.text() : null;
|
|
174
|
+
const cacheKey = generateFilepath({
|
|
175
|
+
projectRoot,
|
|
176
|
+
testId,
|
|
177
|
+
url: niceUrl,
|
|
178
|
+
method: req.method,
|
|
179
|
+
body: body ? body : null,
|
|
180
|
+
testRequestNumber,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// console.log(
|
|
184
|
+
// `Replaying mock for ${req.method} ${niceUrl} (test: ${testId} request #${testRequestNumber}) from '${cacheKey}' if available`
|
|
185
|
+
// );
|
|
186
|
+
return replayRequest(context, cacheKey);
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (e instanceof HTTPException) {
|
|
190
|
+
console.log(`HTTPException Encountered`);
|
|
191
|
+
console.error(e);
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
console.log(`500 MOCK_SERVER_ERROR Encountered`);
|
|
195
|
+
console.error(e);
|
|
196
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
197
|
+
context.status(500);
|
|
198
|
+
return context.body(
|
|
199
|
+
JSON.stringify({
|
|
200
|
+
errors: [
|
|
201
|
+
{
|
|
202
|
+
status: '500',
|
|
203
|
+
code: 'MOCK_SERVER_ERROR',
|
|
204
|
+
title: 'Mock Server Error during Request',
|
|
205
|
+
detail: e.message,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return TestHandler;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function startWorker() {
|
|
217
|
+
let close;
|
|
218
|
+
parentPort.postMessage('ready');
|
|
219
|
+
// listen for launch message
|
|
220
|
+
parentPort.on('message', async (event) => {
|
|
221
|
+
// console.log('worker message received', event);
|
|
222
|
+
if (typeof event === 'object' && event?.type === 'launch') {
|
|
223
|
+
// console.log('worker launching');
|
|
224
|
+
const { options } = event;
|
|
225
|
+
const result = await _createServer(options);
|
|
226
|
+
parentPort.postMessage({
|
|
227
|
+
type: 'launched',
|
|
228
|
+
protocol: 'https',
|
|
229
|
+
hostname: result.location.hostname,
|
|
230
|
+
port: result.location.port,
|
|
231
|
+
});
|
|
232
|
+
close = result.close;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (event === 'end') {
|
|
236
|
+
// console.log('worker shutting down');
|
|
237
|
+
close();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
{ port?: number, projectRoot: string }
|
|
244
|
+
*/
|
|
245
|
+
async function createServer(options) {
|
|
246
|
+
if (options.useWorker) {
|
|
247
|
+
// console.log('starting holodeck worker');
|
|
248
|
+
const worker = new Worker(new URL('./node-worker.js', import.meta.url));
|
|
249
|
+
|
|
250
|
+
const started = new Promise((resolve) => {
|
|
251
|
+
worker.on('message', (v) => {
|
|
252
|
+
// console.log('worker message received', v);
|
|
253
|
+
if (v === 'ready') {
|
|
254
|
+
worker.postMessage({
|
|
255
|
+
type: 'launch',
|
|
256
|
+
options,
|
|
257
|
+
});
|
|
258
|
+
} else if (v.type === 'launched') {
|
|
259
|
+
// @ts-expect-error
|
|
260
|
+
worker.location = v;
|
|
261
|
+
resolve(worker);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await started;
|
|
267
|
+
console.log('\tworker booted');
|
|
268
|
+
return {
|
|
269
|
+
worker,
|
|
270
|
+
server: {
|
|
271
|
+
close() {
|
|
272
|
+
worker.postMessage('end');
|
|
273
|
+
worker.terminate();
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
// @ts-expect-error
|
|
277
|
+
location: worker.location,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return _createServer(options);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function _createServer(options) {
|
|
285
|
+
const { CERT, KEY } = await getCertInfo();
|
|
286
|
+
const app = new Hono();
|
|
287
|
+
// app.use(logger());
|
|
288
|
+
|
|
289
|
+
app.use(
|
|
290
|
+
cors({
|
|
291
|
+
origin: (origin, context) => {
|
|
292
|
+
// console.log(context.req.raw.headers);
|
|
293
|
+
const result = origin.startsWith('http://localhost:') || origin.startsWith('https://localhost:') ? origin : '*';
|
|
294
|
+
// console.log(`CORS Origin: ${origin} => ${result}`);
|
|
295
|
+
return result;
|
|
296
|
+
},
|
|
297
|
+
allowHeaders: ['Accept', 'Content-Type'],
|
|
298
|
+
allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
|
299
|
+
exposeHeaders: ['Content-Length', 'Content-Type'],
|
|
300
|
+
maxAge: 60_000,
|
|
301
|
+
credentials: false,
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
app.all('*', createTestHandler(options.projectRoot));
|
|
305
|
+
|
|
306
|
+
const location = {
|
|
307
|
+
port: options.port ?? DEFAULT_PORT,
|
|
308
|
+
hostname: options.hostname ?? 'localhost',
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const server = serve({
|
|
312
|
+
overrideGlobalObjects: true,
|
|
313
|
+
fetch: app.fetch,
|
|
314
|
+
serverOptions: {
|
|
315
|
+
key: KEY,
|
|
316
|
+
cert: CERT,
|
|
317
|
+
// rejectUnauthorized: false,
|
|
318
|
+
// enableTrace: true,
|
|
319
|
+
// Allow HTTP/1.1 fallback for ALPN negotiation
|
|
320
|
+
// allowHTTP1: true,
|
|
321
|
+
// ALPNProtocols: ['h2', 'http/1.1', 'http/1.0'],
|
|
322
|
+
// origins: ['*'],
|
|
323
|
+
},
|
|
324
|
+
createServer: createSecureServer,
|
|
325
|
+
port: location.port,
|
|
326
|
+
hostname: location.hostname,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
console.log(
|
|
330
|
+
`\tServing Holodeck HTTP Mocks from ${chalk.yellow('https://') + chalk.magenta(location.hostname + ':') + chalk.yellow(location.port)}\n`
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (typeof threadId === 'number' && threadId !== 0) {
|
|
334
|
+
parentPort.postMessage({
|
|
335
|
+
type: 'launched',
|
|
336
|
+
protocol: 'https',
|
|
337
|
+
hostname: location.hostname,
|
|
338
|
+
port: location.port,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (typeof threadId === 'number' && threadId !== 0) {
|
|
343
|
+
const close = createCloseHandler(() => {
|
|
344
|
+
server.close();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return { app, server, location, close };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
app,
|
|
352
|
+
server,
|
|
353
|
+
location,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function launchProgram(config = {}) {
|
|
358
|
+
const projectRoot = process.cwd();
|
|
359
|
+
const pkg = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } });
|
|
360
|
+
const { name } = pkg.default ?? pkg;
|
|
361
|
+
if (!name) {
|
|
362
|
+
throw new Error(`Package name not found in package.json`);
|
|
363
|
+
}
|
|
364
|
+
const options = { name, projectRoot, ...config };
|
|
365
|
+
console.log(
|
|
366
|
+
chalk.grey(
|
|
367
|
+
`\n\t@${chalk.greenBright('warp-drive')}/${chalk.magentaBright(
|
|
368
|
+
'holodeck'
|
|
369
|
+
)} 🌅\n\t=================================\n`
|
|
370
|
+
) +
|
|
371
|
+
chalk.grey(
|
|
372
|
+
`\n\tHolodeck Access Granted\n\t\tprogram: ${chalk.magenta(name)}\n\t\tsettings: ${chalk.green(
|
|
373
|
+
JSON.stringify(config).split('\n').join(' ')
|
|
374
|
+
)}\n\t\tdirectory: ${chalk.cyan(projectRoot)}\n\t\tengine: ${chalk.cyan(
|
|
375
|
+
'node'
|
|
376
|
+
)}@${chalk.yellow(process.version)}\n`
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
console.log(chalk.grey(`\n\tStarting Holodeck Subroutines`));
|
|
380
|
+
|
|
381
|
+
const project = await createServer(options);
|
|
382
|
+
|
|
383
|
+
async function shutdown() {
|
|
384
|
+
console.log(chalk.grey(`\n\tEnding Holodeck Subroutines`));
|
|
385
|
+
project.server.close();
|
|
386
|
+
console.log(chalk.grey(`\n\tHolodeck program ended`));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const endProgram = createCloseHandler(shutdown);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
config: {
|
|
393
|
+
location: `https://${project.location.hostname}:${project.location.port}`,
|
|
394
|
+
port: project.location.port,
|
|
395
|
+
hostname: project.location.hostname,
|
|
396
|
+
protocol: 'https',
|
|
397
|
+
recordingPath: `/__record`,
|
|
398
|
+
},
|
|
399
|
+
endProgram,
|
|
400
|
+
};
|
|
401
|
+
}
|
package/server/utils.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import zlib from 'node:zlib';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
export async function getCertInfo() {
|
|
8
|
+
let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
|
|
9
|
+
let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
|
|
10
|
+
|
|
11
|
+
if (!CERT_PATH) {
|
|
12
|
+
CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
|
|
13
|
+
process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
|
|
14
|
+
|
|
15
|
+
console.log(
|
|
16
|
+
`HOLODECK_SSL_CERT_PATH was not found in the current environment. Setting it to default value of ${CERT_PATH}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!KEY_PATH) {
|
|
21
|
+
KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
|
|
22
|
+
process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
|
|
23
|
+
|
|
24
|
+
console.log(
|
|
25
|
+
`HOLODECK_SSL_KEY_PATH was not found in the current environment. Setting it to default value of ${KEY_PATH}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'SSL certificate or key not found, you may need to run `pnpm dlx @warp-drive/holodeck ensure-cert`'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
CERT_PATH,
|
|
37
|
+
KEY_PATH,
|
|
38
|
+
CERT: fs.readFileSync(CERT_PATH, 'utf8'),
|
|
39
|
+
KEY: fs.readFileSync(KEY_PATH, 'utf8'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_PORT = 1135;
|
|
44
|
+
export const BROTLI_OPTIONS = {
|
|
45
|
+
params: {
|
|
46
|
+
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
|
47
|
+
// brotli currently defaults to 11 but lets be explicit
|
|
48
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
export function compress(code) {
|
|
52
|
+
return zlib.brotliCompressSync(code, BROTLI_OPTIONS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* removes the protocol, host, and port from a url
|
|
57
|
+
*/
|
|
58
|
+
export function getNiceUrl(url) {
|
|
59
|
+
const urlObj = new URL(url);
|
|
60
|
+
urlObj.searchParams.delete('__xTestId');
|
|
61
|
+
urlObj.searchParams.delete('__xTestRequestNumber');
|
|
62
|
+
return (urlObj.pathname + urlObj.searchParams.toString()).slice(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
{
|
|
67
|
+
projectRoot: string;
|
|
68
|
+
testId: string;
|
|
69
|
+
url: string;
|
|
70
|
+
method: string;
|
|
71
|
+
body: string;
|
|
72
|
+
testRequestNumber: number
|
|
73
|
+
}
|
|
74
|
+
*/
|
|
75
|
+
export function generateFilepath(options) {
|
|
76
|
+
const { body } = options;
|
|
77
|
+
const bodyHash = body ? crypto.createHash('md5').update(JSON.stringify(body)).digest('hex') : null;
|
|
78
|
+
const cacheDir = generateFileDir(options);
|
|
79
|
+
return `${cacheDir}/${bodyHash ? bodyHash : 'res'}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
Generate a human scannable file name for the test assets to be stored in,
|
|
84
|
+
the `.mock-cache` directory should be checked-in to the codebase.
|
|
85
|
+
*/
|
|
86
|
+
export function generateFileDir(options) {
|
|
87
|
+
const { projectRoot, testId, url, method, testRequestNumber } = options;
|
|
88
|
+
const normalizedUrl = url.startsWith('/') ? url.slice(1) : url;
|
|
89
|
+
// make path look nice but not be a sub-directory
|
|
90
|
+
// using alternative `/`-like characters would be nice but results in odd encoding
|
|
91
|
+
// on disk path
|
|
92
|
+
const pathUrl = normalizedUrl.replaceAll('/', '_');
|
|
93
|
+
return `${projectRoot}/.mock-cache/${testId}/${method}::${pathUrl}::${testRequestNumber}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createCloseHandler(cb) {
|
|
97
|
+
let executed = false;
|
|
98
|
+
|
|
99
|
+
process.on('SIGINT', () => {
|
|
100
|
+
if (executed) return;
|
|
101
|
+
executed = true;
|
|
102
|
+
cb();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
process.on('SIGTERM', () => {
|
|
106
|
+
if (executed) return;
|
|
107
|
+
executed = true;
|
|
108
|
+
cb();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
process.on('SIGQUIT', () => {
|
|
112
|
+
if (executed) return;
|
|
113
|
+
executed = true;
|
|
114
|
+
cb();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
process.on('exit', () => {
|
|
118
|
+
if (executed) return;
|
|
119
|
+
executed = true;
|
|
120
|
+
cb();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return () => {
|
|
124
|
+
if (executed) return;
|
|
125
|
+
executed = true;
|
|
126
|
+
cb();
|
|
127
|
+
};
|
|
128
|
+
}
|
package/server/start-node.js
DELETED
package/server/worker.js
DELETED