@warp-drive/holodeck 0.0.0-beta.9 → 0.0.1-beta.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.
- package/LICENSE.md +19 -5
- package/README.md +148 -2
- package/dist/index.js.map +1 -1
- package/dist/mock.js.map +1 -1
- package/logos/NCC-1701-a-gold.svg +4 -0
- package/logos/NCC-1701-a-gold_100.svg +1 -0
- package/logos/NCC-1701-a-gold_base-64.txt +1 -0
- package/logos/README.md +4 -0
- package/logos/docs-badge.svg +2 -0
- package/logos/ember-data-logo-dark.svg +12 -0
- package/logos/ember-data-logo-light.svg +12 -0
- package/logos/github-header.svg +444 -0
- package/logos/social1.png +0 -0
- package/logos/social2.png +0 -0
- package/logos/warp-drive-logo-dark.svg +4 -0
- package/logos/warp-drive-logo-gold.svg +4 -0
- package/package.json +23 -32
- package/server/ensure-cert.js +20 -12
- package/server/index.js +250 -186
- package/server/start-node.js +3 -0
- package/server/tsconfig.json +12 -0
- package/server/worker.js +3 -0
- /package/{NCC-1701-a-blue.svg → logos/NCC-1701-a-blue.svg} +0 -0
- /package/{NCC-1701-a.svg → logos/NCC-1701-a.svg} +0 -0
package/server/index.js
CHANGED
|
@@ -1,66 +1,73 @@
|
|
|
1
1
|
/* global Bun */
|
|
2
|
-
import { serve } from '@hono/node-server';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
import { Hono } from 'hono';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { serve } from '@hono/node-server';
|
|
5
|
+
import { createSecureServer } from 'node:http2';
|
|
7
6
|
import { logger } from 'hono/logger';
|
|
8
|
-
import {
|
|
7
|
+
import { HTTPException } from 'hono/http-exception';
|
|
8
|
+
import { cors } from 'hono/cors';
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
|
-
import http2 from 'node:http2';
|
|
12
11
|
import zlib from 'node:zlib';
|
|
13
|
-
import { homedir
|
|
12
|
+
import { homedir } from 'os';
|
|
14
13
|
import path from 'path';
|
|
15
14
|
|
|
16
|
-
/** @type {import('bun-types')} */
|
|
17
15
|
const isBun = typeof Bun !== 'undefined';
|
|
18
|
-
const DEBUG =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function getShellConfigFilePath() {
|
|
22
|
-
const shell = userInfo().shell;
|
|
23
|
-
switch (shell) {
|
|
24
|
-
case '/bin/zsh':
|
|
25
|
-
return path.join(homedir(), '.zshrc');
|
|
26
|
-
case '/bin/bash':
|
|
27
|
-
return path.join(homedir(), '.bashrc');
|
|
28
|
-
default:
|
|
29
|
-
throw Error(
|
|
30
|
-
`Unable to determine configuration file for shell: ${shell}. Manual SSL Cert Setup Required for Holodeck.`
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
16
|
+
const DEBUG =
|
|
17
|
+
process.env.DEBUG?.includes('wd:holodeck') || process.env.DEBUG === '*' || process.env.DEBUG?.includes('wd:*');
|
|
34
18
|
|
|
35
|
-
function getCertInfo() {
|
|
19
|
+
async function getCertInfo() {
|
|
36
20
|
let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
|
|
37
21
|
let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
|
|
38
|
-
const configFilePath = getShellConfigFilePath();
|
|
39
22
|
|
|
40
23
|
if (!CERT_PATH) {
|
|
41
24
|
CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
|
|
42
25
|
process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
|
|
43
|
-
|
|
44
|
-
console.log(
|
|
26
|
+
|
|
27
|
+
console.log(
|
|
28
|
+
`HOLODECK_SSL_CERT_PATH was not found in the current environment. Setting it to default value of ${CERT_PATH}`
|
|
29
|
+
);
|
|
45
30
|
}
|
|
46
31
|
|
|
47
32
|
if (!KEY_PATH) {
|
|
48
33
|
KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
|
|
49
34
|
process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
|
|
50
|
-
execSync(`echo '\nexport HOLODECK_SSL_KEY_PATH="${KEY_PATH}"' >> ${configFilePath}`);
|
|
51
|
-
console.log(`Added HOLODECK_SSL_KEY_PATH to ${configFilePath}`);
|
|
52
|
-
}
|
|
53
35
|
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
console.log(
|
|
37
|
+
`HOLODECK_SSL_KEY_PATH was not found in the current environment. Setting it to default value of ${KEY_PATH}`
|
|
38
|
+
);
|
|
56
39
|
}
|
|
57
40
|
|
|
58
|
-
|
|
59
|
-
CERT_PATH
|
|
60
|
-
KEY_PATH
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
if (isBun) {
|
|
42
|
+
const CERT = Bun.file(CERT_PATH);
|
|
43
|
+
const KEY = Bun.file(KEY_PATH);
|
|
44
|
+
|
|
45
|
+
if (!(await CERT.exists()) || !(await KEY.exists())) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'SSL certificate or key not found, you may need to run `pnpm dlx @warp-drive/holodeck ensure-cert`'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
CERT_PATH,
|
|
53
|
+
KEY_PATH,
|
|
54
|
+
CERT: await CERT.text(),
|
|
55
|
+
KEY: await KEY.text(),
|
|
56
|
+
};
|
|
57
|
+
} else {
|
|
58
|
+
if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'SSL certificate or key not found, you may need to run `pnpm dlx @warp-drive/holodeck ensure-cert`'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
CERT_PATH,
|
|
66
|
+
KEY_PATH,
|
|
67
|
+
CERT: fs.readFileSync(CERT_PATH, 'utf8'),
|
|
68
|
+
KEY: fs.readFileSync(KEY_PATH, 'utf8'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
const DEFAULT_PORT = 1135;
|
|
@@ -97,7 +104,7 @@ function getNiceUrl(url) {
|
|
|
97
104
|
*/
|
|
98
105
|
function generateFilepath(options) {
|
|
99
106
|
const { body } = options;
|
|
100
|
-
const bodyHash = body ? crypto.createHash('md5').update(body).digest('hex') : null;
|
|
107
|
+
const bodyHash = body ? crypto.createHash('md5').update(JSON.stringify(body)).digest('hex') : null;
|
|
101
108
|
const cacheDir = generateFileDir(options);
|
|
102
109
|
return `${cacheDir}/${bodyHash ? `${bodyHash}-` : 'res'}`;
|
|
103
110
|
}
|
|
@@ -106,10 +113,14 @@ function generateFileDir(options) {
|
|
|
106
113
|
return `${projectRoot}/.mock-cache/${testId}/${method}-${testRequestNumber}-${url}`;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
function replayRequest(context, cacheKey) {
|
|
110
|
-
let
|
|
116
|
+
async function replayRequest(context, cacheKey) {
|
|
117
|
+
let metaJson;
|
|
111
118
|
try {
|
|
112
|
-
|
|
119
|
+
if (isBun) {
|
|
120
|
+
metaJson = await Bun.file(`${cacheKey}.meta.json`).json();
|
|
121
|
+
} else {
|
|
122
|
+
metaJson = JSON.parse(fs.readFileSync(`${cacheKey}.meta.json`, 'utf8'));
|
|
123
|
+
}
|
|
113
124
|
} catch (e) {
|
|
114
125
|
context.header('Content-Type', 'application/vnd.api+json');
|
|
115
126
|
context.status(400);
|
|
@@ -120,18 +131,23 @@ function replayRequest(context, cacheKey) {
|
|
|
120
131
|
status: '400',
|
|
121
132
|
code: 'MOCK_NOT_FOUND',
|
|
122
133
|
title: 'Mock not found',
|
|
123
|
-
detail: `No
|
|
134
|
+
detail: `No meta was found for ${context.req.method} ${context.req.url}. You may need to record a mock for this request.`,
|
|
124
135
|
},
|
|
125
136
|
],
|
|
126
137
|
})
|
|
127
138
|
);
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
const metaJson = JSON.parse(meta);
|
|
131
141
|
const bodyPath = `${cacheKey}.body.br`;
|
|
142
|
+
const bodyInit =
|
|
143
|
+
metaJson.status !== 204 && metaJson.status < 500
|
|
144
|
+
? isBun
|
|
145
|
+
? Bun.file(bodyPath)
|
|
146
|
+
: fs.createReadStream(bodyPath)
|
|
147
|
+
: '';
|
|
132
148
|
|
|
133
149
|
const headers = new Headers(metaJson.headers || {});
|
|
134
|
-
|
|
150
|
+
// @ts-expect-error - createReadStream is supported in node
|
|
135
151
|
const response = new Response(bodyInit, {
|
|
136
152
|
status: metaJson.status,
|
|
137
153
|
statusText: metaJson.statusText,
|
|
@@ -147,105 +163,195 @@ function replayRequest(context, cacheKey) {
|
|
|
147
163
|
|
|
148
164
|
function createTestHandler(projectRoot) {
|
|
149
165
|
const TestHandler = async (context) => {
|
|
150
|
-
|
|
166
|
+
try {
|
|
167
|
+
const { req } = context;
|
|
168
|
+
|
|
169
|
+
const testId = req.query('__xTestId');
|
|
170
|
+
const testRequestNumber = req.query('__xTestRequestNumber');
|
|
171
|
+
const niceUrl = getNiceUrl(req.url);
|
|
172
|
+
|
|
173
|
+
if (!testId) {
|
|
174
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
175
|
+
context.status(400);
|
|
176
|
+
return context.body(
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
errors: [
|
|
179
|
+
{
|
|
180
|
+
status: '400',
|
|
181
|
+
code: 'MISSING_X_TEST_ID_HEADER',
|
|
182
|
+
title: 'Request to the http mock server is missing the `X-Test-Id` header',
|
|
183
|
+
detail:
|
|
184
|
+
"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.",
|
|
185
|
+
source: { header: 'X-Test-Id' },
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
}
|
|
151
191
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
192
|
+
if (!testRequestNumber) {
|
|
193
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
194
|
+
context.status(400);
|
|
195
|
+
return context.body(
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
errors: [
|
|
198
|
+
{
|
|
199
|
+
status: '400',
|
|
200
|
+
code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
|
|
201
|
+
title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
|
|
202
|
+
detail:
|
|
203
|
+
"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.",
|
|
204
|
+
source: { header: 'X-Test-Request-Number' },
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
155
210
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
if (req.method === 'POST' || niceUrl === '__record') {
|
|
212
|
+
const payload = await req.json();
|
|
213
|
+
const { url, headers, method, status, statusText, body, response } = payload;
|
|
214
|
+
const cacheKey = generateFilepath({
|
|
215
|
+
projectRoot,
|
|
216
|
+
testId,
|
|
217
|
+
url,
|
|
218
|
+
method,
|
|
219
|
+
body: body ? JSON.stringify(body) : null,
|
|
220
|
+
testRequestNumber,
|
|
221
|
+
});
|
|
222
|
+
const compressedResponse = compress(JSON.stringify(response));
|
|
223
|
+
// allow Content-Type to be overridden
|
|
224
|
+
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
|
|
225
|
+
// We always compress and chunk the response
|
|
226
|
+
headers['Content-Encoding'] = 'br';
|
|
227
|
+
// we don't cache since tests will often reuse similar urls for different payload
|
|
228
|
+
headers['Cache-Control'] = 'no-store';
|
|
229
|
+
// streaming requires Content-Length
|
|
230
|
+
headers['Content-Length'] = compressedResponse.length;
|
|
231
|
+
|
|
232
|
+
const cacheDir = generateFileDir({
|
|
233
|
+
projectRoot,
|
|
234
|
+
testId,
|
|
235
|
+
url,
|
|
236
|
+
method,
|
|
237
|
+
testRequestNumber,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
241
|
+
|
|
242
|
+
if (isBun) {
|
|
243
|
+
const newMetaFile = Bun.file(`${cacheKey}.meta.json`);
|
|
244
|
+
await newMetaFile.write(JSON.stringify({ url, status, statusText, headers, method, requestBody: body }));
|
|
245
|
+
const newBodyFile = Bun.file(`${cacheKey}.body.br`);
|
|
246
|
+
await newBodyFile.write(compressedResponse);
|
|
247
|
+
} else {
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
`${cacheKey}.meta.json`,
|
|
250
|
+
JSON.stringify({ url, status, statusText, headers, method, requestBody: body })
|
|
251
|
+
);
|
|
252
|
+
fs.writeFileSync(`${cacheKey}.body.br`, compressedResponse);
|
|
253
|
+
}
|
|
174
254
|
|
|
175
|
-
|
|
255
|
+
context.status(204);
|
|
256
|
+
return context.body(null);
|
|
257
|
+
} else {
|
|
258
|
+
const body = req.body;
|
|
259
|
+
const cacheKey = generateFilepath({
|
|
260
|
+
projectRoot,
|
|
261
|
+
testId,
|
|
262
|
+
url: niceUrl,
|
|
263
|
+
method: req.method,
|
|
264
|
+
body: body ? JSON.stringify(body) : null,
|
|
265
|
+
testRequestNumber,
|
|
266
|
+
});
|
|
267
|
+
return replayRequest(context, cacheKey);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
if (e instanceof HTTPException) {
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
176
273
|
context.header('Content-Type', 'application/vnd.api+json');
|
|
177
|
-
context.status(
|
|
274
|
+
context.status(500);
|
|
178
275
|
return context.body(
|
|
179
276
|
JSON.stringify({
|
|
180
277
|
errors: [
|
|
181
278
|
{
|
|
182
|
-
status: '
|
|
183
|
-
code: '
|
|
184
|
-
title: '
|
|
185
|
-
detail:
|
|
186
|
-
"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.",
|
|
187
|
-
source: { header: 'X-Test-Request-Number' },
|
|
279
|
+
status: '500',
|
|
280
|
+
code: 'MOCK_SERVER_ERROR',
|
|
281
|
+
title: 'Mock Server Error during Request',
|
|
282
|
+
detail: e.message,
|
|
188
283
|
},
|
|
189
284
|
],
|
|
190
285
|
})
|
|
191
286
|
);
|
|
192
287
|
}
|
|
193
|
-
|
|
194
|
-
if (req.method === 'POST' || niceUrl === '__record') {
|
|
195
|
-
const payload = await req.json();
|
|
196
|
-
const { url, headers, method, status, statusText, body, response } = payload;
|
|
197
|
-
const cacheKey = generateFilepath({
|
|
198
|
-
projectRoot,
|
|
199
|
-
testId,
|
|
200
|
-
url,
|
|
201
|
-
method,
|
|
202
|
-
body: body ? JSON.stringify(body) : null,
|
|
203
|
-
testRequestNumber,
|
|
204
|
-
});
|
|
205
|
-
// allow Content-Type to be overridden
|
|
206
|
-
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
|
|
207
|
-
// We always compress and chunk the response
|
|
208
|
-
headers['Content-Encoding'] = 'br';
|
|
209
|
-
// we don't cache since tests will often reuse similar urls for different payload
|
|
210
|
-
headers['Cache-Control'] = 'no-store';
|
|
211
|
-
|
|
212
|
-
const cacheDir = generateFileDir({
|
|
213
|
-
projectRoot,
|
|
214
|
-
testId,
|
|
215
|
-
url,
|
|
216
|
-
method,
|
|
217
|
-
testRequestNumber,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
221
|
-
fs.writeFileSync(
|
|
222
|
-
`${cacheKey}.meta.json`,
|
|
223
|
-
JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2)
|
|
224
|
-
);
|
|
225
|
-
fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response)));
|
|
226
|
-
context.status(204);
|
|
227
|
-
return context.body(null);
|
|
228
|
-
} else {
|
|
229
|
-
const body = await req.text();
|
|
230
|
-
const cacheKey = generateFilepath({
|
|
231
|
-
projectRoot,
|
|
232
|
-
testId,
|
|
233
|
-
url: niceUrl,
|
|
234
|
-
method: req.method,
|
|
235
|
-
body,
|
|
236
|
-
testRequestNumber,
|
|
237
|
-
});
|
|
238
|
-
return replayRequest(context, cacheKey);
|
|
239
|
-
}
|
|
240
288
|
};
|
|
241
289
|
|
|
242
290
|
return TestHandler;
|
|
243
291
|
}
|
|
244
292
|
|
|
293
|
+
export function startNodeServer() {
|
|
294
|
+
const args = process.argv.slice();
|
|
295
|
+
|
|
296
|
+
if (!isBun && args.length) {
|
|
297
|
+
const options = JSON.parse(args[2]);
|
|
298
|
+
_createServer(options);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function startWorker() {
|
|
303
|
+
// listen for launch message
|
|
304
|
+
globalThis.onmessage = async (event) => {
|
|
305
|
+
const { options } = event.data;
|
|
306
|
+
|
|
307
|
+
const { server } = await _createServer(options);
|
|
308
|
+
|
|
309
|
+
// listen for messages
|
|
310
|
+
globalThis.onmessage = (event) => {
|
|
311
|
+
const message = event.data;
|
|
312
|
+
if (message === 'end') {
|
|
313
|
+
server.close();
|
|
314
|
+
globalThis.close();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
245
320
|
/*
|
|
246
321
|
{ port?: number, projectRoot: string }
|
|
247
322
|
*/
|
|
248
|
-
export function createServer(options) {
|
|
323
|
+
export async function createServer(options, useBun = false) {
|
|
324
|
+
if (!useBun) {
|
|
325
|
+
const CURRENT_FILE = new URL(import.meta.url).pathname;
|
|
326
|
+
const START_FILE = path.join(CURRENT_FILE, '../start-node.js');
|
|
327
|
+
const server = Bun.spawn(['node', '--experimental-default-type=module', START_FILE, JSON.stringify(options)], {
|
|
328
|
+
env: process.env,
|
|
329
|
+
cwd: process.cwd(),
|
|
330
|
+
stdin: 'inherit',
|
|
331
|
+
stdout: 'inherit',
|
|
332
|
+
stderr: 'inherit',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
terminate() {
|
|
337
|
+
server.kill();
|
|
338
|
+
// server.unref();
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
344
|
+
|
|
345
|
+
worker.postMessage({
|
|
346
|
+
type: 'launch',
|
|
347
|
+
options,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return worker;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function _createServer(options) {
|
|
354
|
+
const { CERT, KEY } = await getCertInfo();
|
|
249
355
|
const app = new Hono();
|
|
250
356
|
if (DEBUG) {
|
|
251
357
|
app.use('*', logger());
|
|
@@ -264,38 +370,26 @@ export function createServer(options) {
|
|
|
264
370
|
);
|
|
265
371
|
app.all('*', createTestHandler(options.projectRoot));
|
|
266
372
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
serve({
|
|
373
|
+
const server = serve({
|
|
374
|
+
overrideGlobalObjects: !isBun,
|
|
270
375
|
fetch: app.fetch,
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
{
|
|
275
|
-
key: KEY,
|
|
276
|
-
cert: CERT,
|
|
277
|
-
},
|
|
278
|
-
requestListener
|
|
279
|
-
);
|
|
280
|
-
} catch (e) {
|
|
281
|
-
console.log(chalk.yellow(`Failed to create secure server, falling back to http server. Error: ${e.message}`));
|
|
282
|
-
return http2.createServer(requestListener);
|
|
283
|
-
}
|
|
376
|
+
serverOptions: {
|
|
377
|
+
key: KEY,
|
|
378
|
+
cert: CERT,
|
|
284
379
|
},
|
|
380
|
+
createServer: createSecureServer,
|
|
285
381
|
port: options.port ?? DEFAULT_PORT,
|
|
286
|
-
hostname: 'localhost',
|
|
287
|
-
// bun uses TLS options
|
|
288
|
-
// tls: {
|
|
289
|
-
// key: Bun.file(KEY_PATH),
|
|
290
|
-
// cert: Bun.file(CERT_PATH),
|
|
291
|
-
// },
|
|
382
|
+
hostname: options.hostname ?? 'localhost',
|
|
292
383
|
});
|
|
293
384
|
|
|
294
385
|
console.log(
|
|
295
|
-
`\tMock server running at ${chalk.
|
|
386
|
+
`\tMock server running at ${chalk.yellow('https://') + chalk.magenta((options.hostname ?? 'localhost') + ':') + chalk.yellow(options.port ?? DEFAULT_PORT)}`
|
|
296
387
|
);
|
|
388
|
+
|
|
389
|
+
return { app, server };
|
|
297
390
|
}
|
|
298
391
|
|
|
392
|
+
/** @type {Map<string, Awaited<ReturnType<typeof createServer>>>} */
|
|
299
393
|
const servers = new Map();
|
|
300
394
|
|
|
301
395
|
export default {
|
|
@@ -317,59 +411,29 @@ export default {
|
|
|
317
411
|
)}`
|
|
318
412
|
)
|
|
319
413
|
);
|
|
320
|
-
console.log(chalk.grey(`\n\tStarting Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
321
|
-
|
|
322
|
-
if (isBun) {
|
|
323
|
-
const serverProcess = Bun.spawn(
|
|
324
|
-
['node', '--experimental-default-type=module', CURRENT_FILE, JSON.stringify(options)],
|
|
325
|
-
{
|
|
326
|
-
env: process.env,
|
|
327
|
-
cwd: process.cwd(),
|
|
328
|
-
stdout: 'inherit',
|
|
329
|
-
stderr: 'inherit',
|
|
330
|
-
}
|
|
331
|
-
);
|
|
332
|
-
servers.set(projectRoot, serverProcess);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
414
|
+
console.log(chalk.grey(`\n\tStarting Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
335
415
|
|
|
336
416
|
if (servers.has(projectRoot)) {
|
|
337
417
|
throw new Error(`Holodeck is already running for project '${name}' at '${projectRoot}'`);
|
|
338
418
|
}
|
|
339
419
|
|
|
340
|
-
|
|
420
|
+
// toggle to true if Bun fixes CORS support for HTTP/2
|
|
421
|
+
const project = await createServer(options, false);
|
|
422
|
+
servers.set(projectRoot, project);
|
|
341
423
|
},
|
|
342
424
|
async endProgram() {
|
|
343
|
-
console.log(chalk.grey(`\n\tEnding Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
425
|
+
console.log(chalk.grey(`\n\tEnding Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
344
426
|
const projectRoot = process.cwd();
|
|
345
|
-
const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then(
|
|
346
|
-
(pkg) => pkg.name
|
|
347
|
-
);
|
|
348
427
|
|
|
349
428
|
if (!servers.has(projectRoot)) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (isBun) {
|
|
354
|
-
const serverProcess = servers.get(projectRoot);
|
|
355
|
-
serverProcess.kill();
|
|
429
|
+
const name = require(path.join(projectRoot, 'package.json')).name;
|
|
430
|
+
console.log(chalk.red(`\n\nHolodeck was not running for project '${name}' at '${projectRoot}'\n\n`));
|
|
356
431
|
return;
|
|
357
432
|
}
|
|
358
433
|
|
|
359
|
-
servers.get(projectRoot)
|
|
434
|
+
const project = servers.get(projectRoot);
|
|
360
435
|
servers.delete(projectRoot);
|
|
436
|
+
project.terminate();
|
|
437
|
+
console.log(chalk.grey(`\n\tHolodeck program ended`));
|
|
361
438
|
},
|
|
362
439
|
};
|
|
363
|
-
|
|
364
|
-
function main() {
|
|
365
|
-
const args = process.argv.slice();
|
|
366
|
-
if (!isBun && args.length) {
|
|
367
|
-
if (args[1] !== CURRENT_FILE) {
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
const options = JSON.parse(args[2]);
|
|
371
|
-
createServer(options);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
main();
|
package/server/worker.js
ADDED
|
File without changes
|
|
File without changes
|