@warp-drive/holodeck 0.0.0-beta.2 → 0.0.0-beta.20
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 +159 -8
- package/dist/index.js +11 -3
- 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 +24 -41
- package/server/ensure-cert.js +63 -0
- package/server/index.js +314 -103
- package/server/start-node.js +3 -0
- package/server/tsconfig.json +12 -0
- package/server/worker.js +3 -0
- package/bin/cmd/_start.js +0 -3
- package/bin/cmd/_stop.js +0 -3
- package/bin/cmd/pm2.js +0 -38
- package/bin/cmd/run.js +0 -50
- package/bin/cmd/spawn.js +0 -40
- package/bin/holodeck.js +0 -63
- package/server/CERT.md +0 -3
- package/server/localhost-key.pem +0 -28
- package/server/localhost.pem +0 -27
- /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,16 +1,74 @@
|
|
|
1
|
-
|
|
1
|
+
/* global Bun */
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { Hono } from 'hono';
|
|
3
|
-
import {
|
|
4
|
+
import { serve } from '@hono/node-server';
|
|
5
|
+
import { createSecureServer } from 'node:http2';
|
|
4
6
|
import { logger } from 'hono/logger';
|
|
7
|
+
import { HTTPException } from 'hono/http-exception';
|
|
8
|
+
import { cors } from 'hono/cors';
|
|
5
9
|
import crypto from 'node:crypto';
|
|
6
10
|
import fs from 'node:fs';
|
|
7
|
-
import http2 from 'node:http2';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
9
11
|
import zlib from 'node:zlib';
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import path from 'path';
|
|
12
14
|
|
|
13
|
-
const
|
|
15
|
+
const isBun = typeof Bun !== 'undefined';
|
|
16
|
+
const DEBUG =
|
|
17
|
+
process.env.DEBUG?.includes('wd:holodeck') || process.env.DEBUG === '*' || process.env.DEBUG?.includes('wd:*');
|
|
18
|
+
|
|
19
|
+
async function getCertInfo() {
|
|
20
|
+
let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
|
|
21
|
+
let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
|
|
22
|
+
|
|
23
|
+
if (!CERT_PATH) {
|
|
24
|
+
CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
|
|
25
|
+
process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
|
|
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
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!KEY_PATH) {
|
|
33
|
+
KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
|
|
34
|
+
process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
|
|
35
|
+
|
|
36
|
+
console.log(
|
|
37
|
+
`HOLODECK_SSL_KEY_PATH was not found in the current environment. Setting it to default value of ${KEY_PATH}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
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
|
+
}
|
|
71
|
+
}
|
|
14
72
|
|
|
15
73
|
const DEFAULT_PORT = 1135;
|
|
16
74
|
const BROTLI_OPTIONS = {
|
|
@@ -46,7 +104,7 @@ function getNiceUrl(url) {
|
|
|
46
104
|
*/
|
|
47
105
|
function generateFilepath(options) {
|
|
48
106
|
const { body } = options;
|
|
49
|
-
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;
|
|
50
108
|
const cacheDir = generateFileDir(options);
|
|
51
109
|
return `${cacheDir}/${bodyHash ? `${bodyHash}-` : 'res'}`;
|
|
52
110
|
}
|
|
@@ -55,10 +113,14 @@ function generateFileDir(options) {
|
|
|
55
113
|
return `${projectRoot}/.mock-cache/${testId}/${method}-${testRequestNumber}-${url}`;
|
|
56
114
|
}
|
|
57
115
|
|
|
58
|
-
function replayRequest(context, cacheKey) {
|
|
59
|
-
let
|
|
116
|
+
async function replayRequest(context, cacheKey) {
|
|
117
|
+
let metaJson;
|
|
60
118
|
try {
|
|
61
|
-
|
|
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
|
+
}
|
|
62
124
|
} catch (e) {
|
|
63
125
|
context.header('Content-Type', 'application/vnd.api+json');
|
|
64
126
|
context.status(400);
|
|
@@ -69,18 +131,23 @@ function replayRequest(context, cacheKey) {
|
|
|
69
131
|
status: '400',
|
|
70
132
|
code: 'MOCK_NOT_FOUND',
|
|
71
133
|
title: 'Mock not found',
|
|
72
|
-
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.`,
|
|
73
135
|
},
|
|
74
136
|
],
|
|
75
137
|
})
|
|
76
138
|
);
|
|
77
139
|
}
|
|
78
140
|
|
|
79
|
-
const metaJson = JSON.parse(meta);
|
|
80
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
|
+
: '';
|
|
81
148
|
|
|
82
149
|
const headers = new Headers(metaJson.headers || {});
|
|
83
|
-
|
|
150
|
+
// @ts-expect-error - createReadStream is supported in node
|
|
84
151
|
const response = new Response(bodyInit, {
|
|
85
152
|
status: metaJson.status,
|
|
86
153
|
statusText: metaJson.statusText,
|
|
@@ -96,107 +163,199 @@ function replayRequest(context, cacheKey) {
|
|
|
96
163
|
|
|
97
164
|
function createTestHandler(projectRoot) {
|
|
98
165
|
const TestHandler = async (context) => {
|
|
99
|
-
|
|
166
|
+
try {
|
|
167
|
+
const { req } = context;
|
|
100
168
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
169
|
+
const testId = req.query('__xTestId');
|
|
170
|
+
const testRequestNumber = req.query('__xTestRequestNumber');
|
|
171
|
+
const niceUrl = getNiceUrl(req.url);
|
|
104
172
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}
|
|
191
|
+
|
|
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
|
+
}
|
|
210
|
+
|
|
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 });
|
|
123
241
|
|
|
124
|
-
|
|
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
|
+
}
|
|
254
|
+
|
|
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
|
+
}
|
|
125
273
|
context.header('Content-Type', 'application/vnd.api+json');
|
|
126
|
-
context.status(
|
|
274
|
+
context.status(500);
|
|
127
275
|
return context.body(
|
|
128
276
|
JSON.stringify({
|
|
129
277
|
errors: [
|
|
130
278
|
{
|
|
131
|
-
status: '
|
|
132
|
-
code: '
|
|
133
|
-
title: '
|
|
134
|
-
detail:
|
|
135
|
-
"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.",
|
|
136
|
-
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,
|
|
137
283
|
},
|
|
138
284
|
],
|
|
139
285
|
})
|
|
140
286
|
);
|
|
141
287
|
}
|
|
142
|
-
|
|
143
|
-
if (req.method === 'POST' || niceUrl === '__record') {
|
|
144
|
-
const payload = await req.json();
|
|
145
|
-
const { url, headers, method, status, statusText, body, response } = payload;
|
|
146
|
-
const cacheKey = generateFilepath({
|
|
147
|
-
projectRoot,
|
|
148
|
-
testId,
|
|
149
|
-
url,
|
|
150
|
-
method,
|
|
151
|
-
body: body ? JSON.stringify(body) : null,
|
|
152
|
-
testRequestNumber,
|
|
153
|
-
});
|
|
154
|
-
// allow Content-Type to be overridden
|
|
155
|
-
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
|
|
156
|
-
// We always compress and chunk the response
|
|
157
|
-
headers['Content-Encoding'] = 'br';
|
|
158
|
-
// we don't cache since tests will often reuse similar urls for different payload
|
|
159
|
-
headers['Cache-Control'] = 'no-store';
|
|
160
|
-
|
|
161
|
-
const cacheDir = generateFileDir({
|
|
162
|
-
projectRoot,
|
|
163
|
-
testId,
|
|
164
|
-
url,
|
|
165
|
-
method,
|
|
166
|
-
testRequestNumber,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
170
|
-
fs.writeFileSync(
|
|
171
|
-
`${cacheKey}.meta.json`,
|
|
172
|
-
JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2)
|
|
173
|
-
);
|
|
174
|
-
fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response)));
|
|
175
|
-
context.status(204);
|
|
176
|
-
return context.body(null);
|
|
177
|
-
} else {
|
|
178
|
-
const body = await req.text();
|
|
179
|
-
const cacheKey = generateFilepath({
|
|
180
|
-
projectRoot,
|
|
181
|
-
testId,
|
|
182
|
-
url: niceUrl,
|
|
183
|
-
method: req.method,
|
|
184
|
-
body,
|
|
185
|
-
testRequestNumber,
|
|
186
|
-
});
|
|
187
|
-
return replayRequest(context, cacheKey);
|
|
188
|
-
}
|
|
189
288
|
};
|
|
190
289
|
|
|
191
290
|
return TestHandler;
|
|
192
291
|
}
|
|
193
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
|
+
|
|
194
320
|
/*
|
|
195
321
|
{ port?: number, projectRoot: string }
|
|
196
322
|
*/
|
|
197
|
-
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();
|
|
198
355
|
const app = new Hono();
|
|
199
|
-
|
|
356
|
+
if (DEBUG) {
|
|
357
|
+
app.use('*', logger());
|
|
358
|
+
}
|
|
200
359
|
app.use(
|
|
201
360
|
'*',
|
|
202
361
|
cors({
|
|
@@ -211,18 +370,70 @@ export function createServer(options) {
|
|
|
211
370
|
);
|
|
212
371
|
app.all('*', createTestHandler(options.projectRoot));
|
|
213
372
|
|
|
214
|
-
serve({
|
|
373
|
+
const server = serve({
|
|
374
|
+
overrideGlobalObjects: !isBun,
|
|
215
375
|
fetch: app.fetch,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
key: fs.readFileSync(`${__dirname}/localhost-key.pem`),
|
|
220
|
-
cert: fs.readFileSync(`${__dirname}/localhost.pem`),
|
|
221
|
-
},
|
|
222
|
-
requestListener
|
|
223
|
-
);
|
|
376
|
+
serverOptions: {
|
|
377
|
+
key: KEY,
|
|
378
|
+
cert: CERT,
|
|
224
379
|
},
|
|
380
|
+
createServer: createSecureServer,
|
|
225
381
|
port: options.port ?? DEFAULT_PORT,
|
|
226
|
-
hostname: 'localhost',
|
|
382
|
+
hostname: options.hostname ?? 'localhost',
|
|
227
383
|
});
|
|
384
|
+
|
|
385
|
+
console.log(
|
|
386
|
+
`\tMock server running at ${chalk.yellow('https://') + chalk.magenta((options.hostname ?? 'localhost') + ':') + chalk.yellow(options.port ?? DEFAULT_PORT)}`
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return { app, server };
|
|
228
390
|
}
|
|
391
|
+
|
|
392
|
+
/** @type {Map<string, Awaited<ReturnType<typeof createServer>>>} */
|
|
393
|
+
const servers = new Map();
|
|
394
|
+
|
|
395
|
+
export default {
|
|
396
|
+
async launchProgram(config = {}) {
|
|
397
|
+
const projectRoot = process.cwd();
|
|
398
|
+
const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then(
|
|
399
|
+
(pkg) => pkg.name
|
|
400
|
+
);
|
|
401
|
+
const options = { name, projectRoot, ...config };
|
|
402
|
+
console.log(
|
|
403
|
+
chalk.grey(
|
|
404
|
+
`\n\t@${chalk.greenBright('warp-drive')}/${chalk.magentaBright(
|
|
405
|
+
'holodeck'
|
|
406
|
+
)} 🌅\n\t=================================\n`
|
|
407
|
+
) +
|
|
408
|
+
chalk.grey(
|
|
409
|
+
`\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(
|
|
410
|
+
isBun ? 'bun@' + Bun.version : 'node'
|
|
411
|
+
)}`
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
console.log(chalk.grey(`\n\tStarting Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
415
|
+
|
|
416
|
+
if (servers.has(projectRoot)) {
|
|
417
|
+
throw new Error(`Holodeck is already running for project '${name}' at '${projectRoot}'`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// toggle to true if Bun fixes CORS support for HTTP/2
|
|
421
|
+
const project = await createServer(options, false);
|
|
422
|
+
servers.set(projectRoot, project);
|
|
423
|
+
},
|
|
424
|
+
async endProgram() {
|
|
425
|
+
console.log(chalk.grey(`\n\tEnding Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
426
|
+
const projectRoot = process.cwd();
|
|
427
|
+
|
|
428
|
+
if (!servers.has(projectRoot)) {
|
|
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`));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const project = servers.get(projectRoot);
|
|
435
|
+
servers.delete(projectRoot);
|
|
436
|
+
project.terminate();
|
|
437
|
+
console.log(chalk.grey(`\n\tHolodeck program ended`));
|
|
438
|
+
},
|
|
439
|
+
};
|
package/server/worker.js
ADDED
package/bin/cmd/_start.js
DELETED
package/bin/cmd/_stop.js
DELETED
package/bin/cmd/pm2.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
/* global Bun, globalThis */
|
|
3
|
-
const { process } = globalThis;
|
|
4
|
-
import pm2 from 'pm2';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
|
|
7
|
-
export default async function pm2Delegate(cmd, _args) {
|
|
8
|
-
const pkg = JSON.parse(fs.readFileSync('./package.json'), 'utf8');
|
|
9
|
-
|
|
10
|
-
return new Promise((resolve, reject) => {
|
|
11
|
-
pm2.connect((err) => {
|
|
12
|
-
if (err) {
|
|
13
|
-
console.log('not able to connect to pm2');
|
|
14
|
-
console.error(err);
|
|
15
|
-
process.exit(2);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const options = {
|
|
19
|
-
script: './holodeck.mjs',
|
|
20
|
-
name: pkg.name + '::holodeck',
|
|
21
|
-
cwd: process.cwd(),
|
|
22
|
-
args: cmd === 'start' ? '-f' : '',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
pm2[cmd](cmd === 'start' ? options : options.name, (err, apps) => {
|
|
26
|
-
pm2.disconnect(); // Disconnects from PM2
|
|
27
|
-
if (err) {
|
|
28
|
-
console.log(`not able to ${cmd} pm2 for ${options.name}`);
|
|
29
|
-
console.error(err);
|
|
30
|
-
reject(err);
|
|
31
|
-
} else {
|
|
32
|
-
console.log(`pm2 ${cmd} successful for ${options.name}`);
|
|
33
|
-
resolve();
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
}
|
package/bin/cmd/run.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
/* global Bun, globalThis */
|
|
3
|
-
const isBun = typeof Bun !== 'undefined';
|
|
4
|
-
const { process } = globalThis;
|
|
5
|
-
import { spawn } from './spawn.js';
|
|
6
|
-
import fs from 'fs';
|
|
7
|
-
|
|
8
|
-
export default async function run(args) {
|
|
9
|
-
const pkg = JSON.parse(fs.readFileSync('./package.json'), 'utf8');
|
|
10
|
-
const cmd = args[0];
|
|
11
|
-
const isPkgScript = pkg.scripts[cmd];
|
|
12
|
-
|
|
13
|
-
if (isBun) {
|
|
14
|
-
await spawn(['bun', 'run', 'holodeck:start-program']);
|
|
15
|
-
|
|
16
|
-
let exitCode = 0;
|
|
17
|
-
try {
|
|
18
|
-
await spawn(['bun', 'run', ...args]);
|
|
19
|
-
} catch (e) {
|
|
20
|
-
exitCode = e;
|
|
21
|
-
}
|
|
22
|
-
await spawn(['bun', 'run', 'holodeck:end-program']);
|
|
23
|
-
if (exitCode !== 0) {
|
|
24
|
-
process.exit(exitCode);
|
|
25
|
-
}
|
|
26
|
-
return;
|
|
27
|
-
} else {
|
|
28
|
-
await spawn(['pnpm', 'run', 'holodeck:start-program']);
|
|
29
|
-
|
|
30
|
-
let exitCode = 0;
|
|
31
|
-
try {
|
|
32
|
-
if (isPkgScript) {
|
|
33
|
-
const cmdArgs = pkg.scripts[cmd].split(' ');
|
|
34
|
-
if (args.length > 1) {
|
|
35
|
-
cmdArgs.push(...args.slice(1));
|
|
36
|
-
}
|
|
37
|
-
console.log({ cmdArgs });
|
|
38
|
-
await spawn(cmdArgs);
|
|
39
|
-
} else {
|
|
40
|
-
await spawn(['pnpm', 'exec', ...args]);
|
|
41
|
-
}
|
|
42
|
-
} catch (e) {
|
|
43
|
-
exitCode = e;
|
|
44
|
-
}
|
|
45
|
-
await spawn(['pnpm', 'run', 'holodeck:end-program']);
|
|
46
|
-
if (exitCode !== 0) {
|
|
47
|
-
process.exit(exitCode);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
package/bin/cmd/spawn.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
/* global Bun, globalThis */
|
|
3
|
-
const isBun = typeof Bun !== 'undefined';
|
|
4
|
-
|
|
5
|
-
export async function spawn(args, options) {
|
|
6
|
-
if (isBun) {
|
|
7
|
-
const proc = Bun.spawn(args, {
|
|
8
|
-
env: process.env,
|
|
9
|
-
cwd: process.cwd(),
|
|
10
|
-
stdout: 'inherit',
|
|
11
|
-
stderr: 'inherit',
|
|
12
|
-
});
|
|
13
|
-
await proc.exited;
|
|
14
|
-
if (proc.exitCode !== 0) {
|
|
15
|
-
throw proc.exitCode;
|
|
16
|
-
}
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const { spawn } = await import('node:child_process');
|
|
21
|
-
|
|
22
|
-
// eslint-disable-next-line no-inner-declarations
|
|
23
|
-
function pSpawn(cmd, args, opts) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
const proc = spawn(cmd, args, opts);
|
|
26
|
-
proc.on('exit', (code) => {
|
|
27
|
-
if (code === 0) {
|
|
28
|
-
resolve();
|
|
29
|
-
} else {
|
|
30
|
-
reject(code);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await pSpawn(args.shift(), args, {
|
|
37
|
-
stdio: 'inherit',
|
|
38
|
-
shell: true,
|
|
39
|
-
});
|
|
40
|
-
}
|