@warp-drive/holodeck 0.1.0-alpha.15 → 0.1.0-alpha.16
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 +14 -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/index.js
CHANGED
|
@@ -1,474 +1,7 @@
|
|
|
1
1
|
/* global Bun */
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { Hono } from 'hono';
|
|
4
|
-
import { serve } from '@hono/node-server';
|
|
5
|
-
import { createSecureServer } from 'node:http2';
|
|
6
|
-
import { logger } from 'hono/logger';
|
|
7
|
-
import { HTTPException } from 'hono/http-exception';
|
|
8
|
-
import { cors } from 'hono/cors';
|
|
9
|
-
import crypto from 'node:crypto';
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import zlib from 'node:zlib';
|
|
12
|
-
import { homedir } from 'os';
|
|
13
2
|
import path from 'path';
|
|
14
|
-
import { threadId, parentPort } from 'node:worker_threads';
|
|
15
|
-
|
|
16
3
|
const isBun = typeof Bun !== 'undefined';
|
|
17
|
-
|
|
18
|
-
process.env.DEBUG?.includes('wd:holodeck') || process.env.DEBUG === '*' || process.env.DEBUG?.includes('wd:*');
|
|
19
|
-
|
|
20
|
-
async function getCertInfo() {
|
|
21
|
-
let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
|
|
22
|
-
let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
|
|
23
|
-
|
|
24
|
-
if (!CERT_PATH) {
|
|
25
|
-
CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
|
|
26
|
-
process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
|
|
27
|
-
|
|
28
|
-
console.log(
|
|
29
|
-
`HOLODECK_SSL_CERT_PATH was not found in the current environment. Setting it to default value of ${CERT_PATH}`
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!KEY_PATH) {
|
|
34
|
-
KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
|
|
35
|
-
process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
|
|
36
|
-
|
|
37
|
-
console.log(
|
|
38
|
-
`HOLODECK_SSL_KEY_PATH was not found in the current environment. Setting it to default value of ${KEY_PATH}`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (isBun) {
|
|
43
|
-
const CERT = Bun.file(CERT_PATH);
|
|
44
|
-
const KEY = Bun.file(KEY_PATH);
|
|
45
|
-
|
|
46
|
-
if (!(await CERT.exists()) || !(await KEY.exists())) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
'SSL certificate or key not found, you may need to run `pnpm dlx @warp-drive/holodeck ensure-cert`'
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
CERT_PATH,
|
|
54
|
-
KEY_PATH,
|
|
55
|
-
CERT: await CERT.text(),
|
|
56
|
-
KEY: await KEY.text(),
|
|
57
|
-
};
|
|
58
|
-
} else {
|
|
59
|
-
if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
|
|
60
|
-
throw new Error(
|
|
61
|
-
'SSL certificate or key not found, you may need to run `pnpm dlx @warp-drive/holodeck ensure-cert`'
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
CERT_PATH,
|
|
67
|
-
KEY_PATH,
|
|
68
|
-
CERT: fs.readFileSync(CERT_PATH, 'utf8'),
|
|
69
|
-
KEY: fs.readFileSync(KEY_PATH, 'utf8'),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const DEFAULT_PORT = 1135;
|
|
75
|
-
const BROTLI_OPTIONS = {
|
|
76
|
-
params: {
|
|
77
|
-
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
|
78
|
-
// brotli currently defaults to 11 but lets be explicit
|
|
79
|
-
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
function compress(code) {
|
|
83
|
-
return zlib.brotliCompressSync(code, BROTLI_OPTIONS);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* removes the protocol, host, and port from a url
|
|
88
|
-
*/
|
|
89
|
-
function getNiceUrl(url) {
|
|
90
|
-
const urlObj = new URL(url);
|
|
91
|
-
urlObj.searchParams.delete('__xTestId');
|
|
92
|
-
urlObj.searchParams.delete('__xTestRequestNumber');
|
|
93
|
-
return (urlObj.pathname + urlObj.searchParams.toString()).slice(1);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/*
|
|
97
|
-
{
|
|
98
|
-
projectRoot: string;
|
|
99
|
-
testId: string;
|
|
100
|
-
url: string;
|
|
101
|
-
method: string;
|
|
102
|
-
body: string;
|
|
103
|
-
testRequestNumber: number
|
|
104
|
-
}
|
|
105
|
-
*/
|
|
106
|
-
function generateFilepath(options) {
|
|
107
|
-
const { body } = options;
|
|
108
|
-
const bodyHash = body ? crypto.createHash('md5').update(JSON.stringify(body)).digest('hex') : null;
|
|
109
|
-
const cacheDir = generateFileDir(options);
|
|
110
|
-
return `${cacheDir}/${bodyHash ? bodyHash : 'res'}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/*
|
|
114
|
-
Generate a human scannable file name for the test assets to be stored in,
|
|
115
|
-
the `.mock-cache` directory should be checked-in to the codebase.
|
|
116
|
-
*/
|
|
117
|
-
function generateFileDir(options) {
|
|
118
|
-
const { projectRoot, testId, url, method, testRequestNumber } = options;
|
|
119
|
-
const normalizedUrl = url.startsWith('/') ? url.slice(1) : url;
|
|
120
|
-
// make path look nice but not be a sub-directory
|
|
121
|
-
// using alternative `/`-like characters would be nice but results in odd encoding
|
|
122
|
-
// on disk path
|
|
123
|
-
const pathUrl = normalizedUrl.replaceAll('/', '_');
|
|
124
|
-
return `${projectRoot}/.mock-cache/${testId}/${method}::${pathUrl}::${testRequestNumber}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function replayRequest(context, cacheKey) {
|
|
128
|
-
let metaJson;
|
|
129
|
-
try {
|
|
130
|
-
if (isBun) {
|
|
131
|
-
metaJson = await Bun.file(`${cacheKey}.meta.json`).json();
|
|
132
|
-
} else {
|
|
133
|
-
metaJson = JSON.parse(fs.readFileSync(`${cacheKey}.meta.json`, 'utf8'));
|
|
134
|
-
}
|
|
135
|
-
} catch (e) {
|
|
136
|
-
context.header('Content-Type', 'application/vnd.api+json');
|
|
137
|
-
context.status(400);
|
|
138
|
-
return context.body(
|
|
139
|
-
JSON.stringify({
|
|
140
|
-
errors: [
|
|
141
|
-
{
|
|
142
|
-
status: '400',
|
|
143
|
-
code: 'MOCK_NOT_FOUND',
|
|
144
|
-
title: 'Mock not found',
|
|
145
|
-
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.`,
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
})
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const bodyPath = `${cacheKey}.body.br`;
|
|
154
|
-
const bodyInit =
|
|
155
|
-
metaJson.status !== 204 && metaJson.status < 500
|
|
156
|
-
? isBun
|
|
157
|
-
? Bun.file(bodyPath)
|
|
158
|
-
: fs.createReadStream(bodyPath)
|
|
159
|
-
: '';
|
|
160
|
-
|
|
161
|
-
const headers = new Headers(metaJson.headers || {});
|
|
162
|
-
// @ts-expect-error - createReadStream is supported in node
|
|
163
|
-
const response = new Response(bodyInit, {
|
|
164
|
-
status: metaJson.status,
|
|
165
|
-
statusText: metaJson.statusText,
|
|
166
|
-
headers,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (metaJson.status > 400) {
|
|
170
|
-
throw new HTTPException(metaJson.status, { res: response, message: metaJson.statusText });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return response;
|
|
174
|
-
} catch (e) {
|
|
175
|
-
if (e instanceof HTTPException) {
|
|
176
|
-
throw e;
|
|
177
|
-
}
|
|
178
|
-
context.header('Content-Type', 'application/vnd.api+json');
|
|
179
|
-
context.status(500);
|
|
180
|
-
return context.body(
|
|
181
|
-
JSON.stringify({
|
|
182
|
-
errors: [
|
|
183
|
-
{
|
|
184
|
-
status: '500',
|
|
185
|
-
code: 'MOCK_SERVER_ERROR',
|
|
186
|
-
title: 'Mock Replay Failed',
|
|
187
|
-
detail: `Failed to create the response for ${context.req.method} ${context.req.url}.\n\n\n${e.message}\n${e.stack}`,
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
})
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function createTestHandler(projectRoot) {
|
|
196
|
-
const TestHandler = async (context) => {
|
|
197
|
-
try {
|
|
198
|
-
const { req } = context;
|
|
199
|
-
|
|
200
|
-
const testId = req.query('__xTestId');
|
|
201
|
-
const testRequestNumber = req.query('__xTestRequestNumber');
|
|
202
|
-
const niceUrl = getNiceUrl(req.url);
|
|
203
|
-
|
|
204
|
-
if (!testId) {
|
|
205
|
-
context.header('Content-Type', 'application/vnd.api+json');
|
|
206
|
-
context.status(400);
|
|
207
|
-
return context.body(
|
|
208
|
-
JSON.stringify({
|
|
209
|
-
errors: [
|
|
210
|
-
{
|
|
211
|
-
status: '400',
|
|
212
|
-
code: 'MISSING_X_TEST_ID_HEADER',
|
|
213
|
-
title: 'Request to the http mock server is missing the `X-Test-Id` header',
|
|
214
|
-
detail:
|
|
215
|
-
"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.",
|
|
216
|
-
source: { header: 'X-Test-Id' },
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
})
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!testRequestNumber) {
|
|
224
|
-
context.header('Content-Type', 'application/vnd.api+json');
|
|
225
|
-
context.status(400);
|
|
226
|
-
return context.body(
|
|
227
|
-
JSON.stringify({
|
|
228
|
-
errors: [
|
|
229
|
-
{
|
|
230
|
-
status: '400',
|
|
231
|
-
code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
|
|
232
|
-
title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
|
|
233
|
-
detail:
|
|
234
|
-
"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.",
|
|
235
|
-
source: { header: 'X-Test-Request-Number' },
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
})
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (req.method === 'POST' && niceUrl === '__record') {
|
|
243
|
-
const payload = await req.json();
|
|
244
|
-
const { url, headers, method, status, statusText, body, response } = payload;
|
|
245
|
-
const cacheKey = generateFilepath({
|
|
246
|
-
projectRoot,
|
|
247
|
-
testId,
|
|
248
|
-
url,
|
|
249
|
-
method,
|
|
250
|
-
body,
|
|
251
|
-
testRequestNumber,
|
|
252
|
-
});
|
|
253
|
-
const compressedResponse = compress(JSON.stringify(response));
|
|
254
|
-
// allow Content-Type to be overridden
|
|
255
|
-
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
|
|
256
|
-
// We always compress and chunk the response
|
|
257
|
-
headers['Content-Encoding'] = 'br';
|
|
258
|
-
// we don't cache since tests will often reuse similar urls for different payload
|
|
259
|
-
headers['Cache-Control'] = 'no-store';
|
|
260
|
-
// streaming requires Content-Length
|
|
261
|
-
headers['Content-Length'] = compressedResponse.length;
|
|
262
|
-
|
|
263
|
-
const cacheDir = generateFileDir({
|
|
264
|
-
projectRoot,
|
|
265
|
-
testId,
|
|
266
|
-
url,
|
|
267
|
-
method,
|
|
268
|
-
testRequestNumber,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
272
|
-
|
|
273
|
-
if (isBun) {
|
|
274
|
-
const newMetaFile = Bun.file(`${cacheKey}.meta.json`);
|
|
275
|
-
await newMetaFile.write(JSON.stringify({ url, status, statusText, headers, method, requestBody: body }));
|
|
276
|
-
const newBodyFile = Bun.file(`${cacheKey}.body.br`);
|
|
277
|
-
await newBodyFile.write(compressedResponse);
|
|
278
|
-
} else {
|
|
279
|
-
fs.writeFileSync(
|
|
280
|
-
`${cacheKey}.meta.json`,
|
|
281
|
-
JSON.stringify({ url, status, statusText, headers, method, requestBody: body })
|
|
282
|
-
);
|
|
283
|
-
fs.writeFileSync(`${cacheKey}.body.br`, compressedResponse);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
context.status(201);
|
|
287
|
-
return context.body(
|
|
288
|
-
JSON.stringify({
|
|
289
|
-
message: `Recorded ${method} ${url} for test ${testId} request #${testRequestNumber}`,
|
|
290
|
-
cacheKey,
|
|
291
|
-
cacheDir,
|
|
292
|
-
})
|
|
293
|
-
);
|
|
294
|
-
} else {
|
|
295
|
-
const body = req.raw.body ? await req.text() : null;
|
|
296
|
-
const cacheKey = generateFilepath({
|
|
297
|
-
projectRoot,
|
|
298
|
-
testId,
|
|
299
|
-
url: niceUrl,
|
|
300
|
-
method: req.method,
|
|
301
|
-
body: body ? body : null,
|
|
302
|
-
testRequestNumber,
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
console.log(
|
|
306
|
-
`Replaying mock for ${req.method} ${niceUrl} (test: ${testId} request #${testRequestNumber}) from '${cacheKey}' if available`
|
|
307
|
-
);
|
|
308
|
-
return replayRequest(context, cacheKey);
|
|
309
|
-
}
|
|
310
|
-
} catch (e) {
|
|
311
|
-
if (e instanceof HTTPException) {
|
|
312
|
-
console.log(`HTTPException Encountered`);
|
|
313
|
-
console.error(e);
|
|
314
|
-
throw e;
|
|
315
|
-
}
|
|
316
|
-
console.log(`500 MOCK_SERVER_ERROR Encountered`);
|
|
317
|
-
console.error(e);
|
|
318
|
-
context.header('Content-Type', 'application/vnd.api+json');
|
|
319
|
-
context.status(500);
|
|
320
|
-
return context.body(
|
|
321
|
-
JSON.stringify({
|
|
322
|
-
errors: [
|
|
323
|
-
{
|
|
324
|
-
status: '500',
|
|
325
|
-
code: 'MOCK_SERVER_ERROR',
|
|
326
|
-
title: 'Mock Server Error during Request',
|
|
327
|
-
detail: e.message,
|
|
328
|
-
},
|
|
329
|
-
],
|
|
330
|
-
})
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
return TestHandler;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export function startNodeServer() {
|
|
339
|
-
const args = process.argv.slice();
|
|
340
|
-
|
|
341
|
-
if (!isBun && args.length) {
|
|
342
|
-
const options = JSON.parse(args[2]);
|
|
343
|
-
_createServer(options);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export function startWorker() {
|
|
348
|
-
// listen for launch message
|
|
349
|
-
globalThis.onmessage = async (event) => {
|
|
350
|
-
console.log('starting holodeck worker');
|
|
351
|
-
const { options } = event.data;
|
|
352
|
-
|
|
353
|
-
const { server } = await _createServer(options);
|
|
354
|
-
|
|
355
|
-
// listen for messages
|
|
356
|
-
globalThis.onmessage = (event) => {
|
|
357
|
-
const message = event.data;
|
|
358
|
-
if (message === 'end') {
|
|
359
|
-
server.close();
|
|
360
|
-
globalThis.close();
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async function waitForLog(server, logMessage) {
|
|
367
|
-
for await (const chunk of server.stdout) {
|
|
368
|
-
process.stdout.write(chunk);
|
|
369
|
-
const txt = new TextDecoder().decode(chunk);
|
|
370
|
-
if (txt.includes(logMessage)) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function reprintLogs(server) {
|
|
377
|
-
for await (const chunk of server.stdout) {
|
|
378
|
-
process.stdout.write(chunk);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/*
|
|
383
|
-
{ port?: number, projectRoot: string }
|
|
384
|
-
*/
|
|
385
|
-
export async function createServer(options, useBun = false) {
|
|
386
|
-
if (!useBun) {
|
|
387
|
-
const CURRENT_FILE = new URL(import.meta.url).pathname;
|
|
388
|
-
const START_FILE = path.join(CURRENT_FILE, '../start-node.js');
|
|
389
|
-
const server = Bun.spawn(['node', START_FILE, JSON.stringify(options)], {
|
|
390
|
-
env: Object.assign({}, process.env, { FORCE_COLOR: 1 }),
|
|
391
|
-
cwd: process.cwd(),
|
|
392
|
-
stdin: 'inherit',
|
|
393
|
-
stdout: 'pipe',
|
|
394
|
-
stderr: 'inherit',
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
await waitForLog(server, 'Serving Holodeck HTTP Mocks');
|
|
398
|
-
void reprintLogs(server);
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
terminate() {
|
|
402
|
-
server.kill();
|
|
403
|
-
// server.unref();
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
console.log('starting holodeck worker');
|
|
409
|
-
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
410
|
-
|
|
411
|
-
worker.postMessage({
|
|
412
|
-
type: 'launch',
|
|
413
|
-
options,
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
return new Promise((resolve) => {
|
|
417
|
-
// @ts-expect-error
|
|
418
|
-
worker.onmessage((v) => {
|
|
419
|
-
console.log('worker message received', v);
|
|
420
|
-
if (v.data === 'launched') {
|
|
421
|
-
resolve(worker);
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function _createServer(options) {
|
|
428
|
-
const { CERT, KEY } = await getCertInfo();
|
|
429
|
-
const app = new Hono();
|
|
430
|
-
if (DEBUG) {
|
|
431
|
-
app.use('*', logger());
|
|
432
|
-
}
|
|
433
|
-
app.use(
|
|
434
|
-
'*',
|
|
435
|
-
cors({
|
|
436
|
-
origin: (origin) =>
|
|
437
|
-
origin.startsWith('http://localhost:') || origin.startsWith('https://localhost:') ? origin : '*',
|
|
438
|
-
allowHeaders: ['Accept', 'Content-Type'],
|
|
439
|
-
allowMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'DELETE', 'PATCH'],
|
|
440
|
-
exposeHeaders: ['Content-Length', 'Content-Type'],
|
|
441
|
-
maxAge: 60_000,
|
|
442
|
-
credentials: false,
|
|
443
|
-
})
|
|
444
|
-
);
|
|
445
|
-
app.all('*', createTestHandler(options.projectRoot));
|
|
446
|
-
|
|
447
|
-
const server = serve({
|
|
448
|
-
overrideGlobalObjects: !isBun,
|
|
449
|
-
fetch: app.fetch,
|
|
450
|
-
serverOptions: {
|
|
451
|
-
key: KEY,
|
|
452
|
-
cert: CERT,
|
|
453
|
-
},
|
|
454
|
-
createServer: createSecureServer,
|
|
455
|
-
port: options.port ?? DEFAULT_PORT,
|
|
456
|
-
hostname: options.hostname ?? 'localhost',
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
console.log(
|
|
460
|
-
`\tServing Holodeck HTTP Mocks from ${chalk.yellow('https://') + chalk.magenta((options.hostname ?? 'localhost') + ':') + chalk.yellow(options.port ?? DEFAULT_PORT)}\n`
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
if (typeof threadId === 'number' && threadId !== 0) {
|
|
464
|
-
parentPort.postMessage('launched');
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return { app, server };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/** @type {Map<string, Awaited<ReturnType<typeof createServer>>>} */
|
|
471
|
-
const servers = new Map();
|
|
4
|
+
let closeHandler = () => {};
|
|
472
5
|
|
|
473
6
|
export default {
|
|
474
7
|
async launchProgram(config = {}) {
|
|
@@ -477,41 +10,33 @@ export default {
|
|
|
477
10
|
(pkg) => pkg.name
|
|
478
11
|
);
|
|
479
12
|
const options = { name, projectRoot, ...config };
|
|
480
|
-
console.log(
|
|
481
|
-
chalk.grey(
|
|
482
|
-
`\n\t@${chalk.greenBright('warp-drive')}/${chalk.magentaBright(
|
|
483
|
-
'holodeck'
|
|
484
|
-
)} 🌅\n\t=================================\n`
|
|
485
|
-
) +
|
|
486
|
-
chalk.grey(
|
|
487
|
-
`\n\tHolodeck Access Granted\n\t\tprogram: ${chalk.magenta(name)}\n\t\tsettings: ${chalk.green(JSON.stringify(config).split('\n').join(' '))}\n\t\tdirectory: ${chalk.cyan(projectRoot)}\n\t\tengine: ${chalk.cyan(
|
|
488
|
-
isBun ? 'bun@' + Bun.version : 'node'
|
|
489
|
-
)}`
|
|
490
|
-
)
|
|
491
|
-
);
|
|
492
|
-
console.log(chalk.grey(`\n\tStarting Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
493
13
|
|
|
494
|
-
if (
|
|
495
|
-
|
|
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;
|
|
496
21
|
}
|
|
497
22
|
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const projectRoot = process.cwd();
|
|
505
|
-
|
|
506
|
-
if (!servers.has(projectRoot)) {
|
|
507
|
-
const name = require(path.join(projectRoot, 'package.json')).name;
|
|
508
|
-
console.log(chalk.red(`\n\nHolodeck was not running for project '${name}' at '${projectRoot}'\n\n`));
|
|
509
|
-
return;
|
|
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;
|
|
510
29
|
}
|
|
511
30
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
31
|
+
// use bun
|
|
32
|
+
// @ts-expect-error
|
|
33
|
+
options.useWorker = config.useWorker ?? true;
|
|
34
|
+
const nodeImpl = await import('./bun.js');
|
|
35
|
+
const program = await nodeImpl.launchProgram(options);
|
|
36
|
+
closeHandler = program.endProgram;
|
|
37
|
+
return program.config;
|
|
38
|
+
},
|
|
39
|
+
async endProgram() {
|
|
40
|
+
closeHandler();
|
|
516
41
|
},
|
|
517
42
|
};
|