@warp-drive/holodeck 0.0.0-alpha.126 → 0.0.0-alpha.127
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/dist/index.js.map +1 -1
- package/dist/mock.js.map +1 -1
- package/package.json +11 -21
- package/server/index.js +147 -95
- package/server/start-node.js +3 -0
- package/server/tsconfig.json +12 -0
- package/server/worker.js +3 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocument } from '@ember-data/request';\n\nimport type { ScaffoldGenerator } from './mock';\n\nconst TEST_IDS = new WeakMap<object, { id: string; request: number; mock: number }>();\n\nlet HOST = 'https://localhost:1135/';\nexport function setConfig({ host }: { host: string }) {\n HOST = host.endsWith('/') ? host : `${host}/`;\n}\n\nexport function setTestId(context: object, str: string | null) {\n if (str && TEST_IDS.has(context)) {\n throw new Error(`MockServerHandler is already configured with a testId.`);\n }\n if (str) {\n TEST_IDS.set(context, { id: str, request: 0, mock: 0 });\n } else {\n TEST_IDS.delete(context);\n }\n}\n\nlet IS_RECORDING = false;\nexport function setIsRecording(value: boolean) {\n IS_RECORDING = Boolean(value);\n}\nexport function getIsRecording() {\n return IS_RECORDING;\n}\n\nexport class MockServerHandler implements Handler {\n declare owner: object;\n constructor(owner: object) {\n this.owner = owner;\n }\n async request<T>(context: RequestContext, next: NextFn<T>): Promise<StructuredDataDocument<T>> {\n const test = TEST_IDS.get(this.owner);\n if (!test) {\n throw new Error(\n `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`\n );\n }\n\n const request: RequestInfo = Object.assign({}, context.request);\n const isRecording = request.url!.endsWith('/__record');\n const firstChar = request.url!.includes('?') ? '&' : '?';\n const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${\n isRecording ? test.mock++ : test.request++\n }`;\n request.url = request.url + queryForTest;\n\n request.mode = 'cors';\n request.credentials = 'omit';\n request.referrerPolicy = '';\n\n try {\n const future = next(request);\n context.setStream(future.getStream());\n return await future;\n } catch (e) {\n if (e instanceof Error && !(e instanceof DOMException)) {\n e.message = e.message.replace(queryForTest, '');\n }\n throw e;\n }\n }\n}\n\nexport async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean) {\n const test = TEST_IDS.get(owner);\n if (!test) {\n throw new Error(`Cannot call \"mock\" before configuring a testId. Use setTestId to set the testId for each test`);\n }\n const testMockNum = test.mock++;\n if (getIsRecording() || isRecording) {\n const port = window.location.port ? `:${window.location.port}` : '';\n const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;\n await fetch(url, {\n method: 'POST',\n body: JSON.stringify(generate()),\n mode: 'cors',\n credentials: 'omit',\n referrerPolicy: '',\n });\n }\n}\n"],"names":["TEST_IDS","WeakMap","HOST","setConfig","host","endsWith","setTestId","context","str","has","Error","set","id","request","mock","delete","IS_RECORDING","setIsRecording","value","Boolean","getIsRecording","MockServerHandler","constructor","owner","next","test","get","Object","assign","isRecording","url","firstChar","includes","queryForTest","mode","credentials","referrerPolicy","future","setStream","getStream","e","DOMException","message","replace","generate","testMockNum","fetch","method","body","JSON","stringify"],"mappings":"AAIA,MAAMA,QAAQ,GAAG,IAAIC,OAAO,EAAyD
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocument } from '@ember-data/request';\n\nimport type { ScaffoldGenerator } from './mock';\n\nconst TEST_IDS = new WeakMap<object, { id: string; request: number; mock: number }>();\n\nlet HOST = 'https://localhost:1135/';\nexport function setConfig({ host }: { host: string }) {\n HOST = host.endsWith('/') ? host : `${host}/`;\n}\n\nexport function setTestId(context: object, str: string | null) {\n if (str && TEST_IDS.has(context)) {\n throw new Error(`MockServerHandler is already configured with a testId.`);\n }\n if (str) {\n TEST_IDS.set(context, { id: str, request: 0, mock: 0 });\n } else {\n TEST_IDS.delete(context);\n }\n}\n\nlet IS_RECORDING = false;\nexport function setIsRecording(value: boolean) {\n IS_RECORDING = Boolean(value);\n}\nexport function getIsRecording() {\n return IS_RECORDING;\n}\n\nexport class MockServerHandler implements Handler {\n declare owner: object;\n constructor(owner: object) {\n this.owner = owner;\n }\n async request<T>(context: RequestContext, next: NextFn<T>): Promise<StructuredDataDocument<T>> {\n const test = TEST_IDS.get(this.owner);\n if (!test) {\n throw new Error(\n `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`\n );\n }\n\n const request: RequestInfo = Object.assign({}, context.request);\n const isRecording = request.url!.endsWith('/__record');\n const firstChar = request.url!.includes('?') ? '&' : '?';\n const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${\n isRecording ? test.mock++ : test.request++\n }`;\n request.url = request.url + queryForTest;\n\n request.mode = 'cors';\n request.credentials = 'omit';\n request.referrerPolicy = '';\n\n try {\n const future = next(request);\n context.setStream(future.getStream());\n return await future;\n } catch (e) {\n if (e instanceof Error && !(e instanceof DOMException)) {\n e.message = e.message.replace(queryForTest, '');\n }\n throw e;\n }\n }\n}\n\nexport async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean) {\n const test = TEST_IDS.get(owner);\n if (!test) {\n throw new Error(`Cannot call \"mock\" before configuring a testId. Use setTestId to set the testId for each test`);\n }\n const testMockNum = test.mock++;\n if (getIsRecording() || isRecording) {\n const port = window.location.port ? `:${window.location.port}` : '';\n const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;\n await fetch(url, {\n method: 'POST',\n body: JSON.stringify(generate()),\n mode: 'cors',\n credentials: 'omit',\n referrerPolicy: '',\n });\n }\n}\n"],"names":["TEST_IDS","WeakMap","HOST","setConfig","host","endsWith","setTestId","context","str","has","Error","set","id","request","mock","delete","IS_RECORDING","setIsRecording","value","Boolean","getIsRecording","MockServerHandler","constructor","owner","next","test","get","Object","assign","isRecording","url","firstChar","includes","queryForTest","mode","credentials","referrerPolicy","future","setStream","getStream","e","DOMException","message","replace","generate","testMockNum","fetch","method","body","JSON","stringify"],"mappings":"AAIA,MAAMA,QAAQ,GAAG,IAAIC,OAAO,EAAyD;AAErF,IAAIC,IAAI,GAAG,yBAAyB;AAC7B,SAASC,SAASA,CAAC;AAAEC,EAAAA;AAAuB,CAAC,EAAE;AACpDF,EAAAA,IAAI,GAAGE,IAAI,CAACC,QAAQ,CAAC,GAAG,CAAC,GAAGD,IAAI,GAAG,CAAGA,EAAAA,IAAI,CAAG,CAAA,CAAA;AAC/C;AAEO,SAASE,SAASA,CAACC,OAAe,EAAEC,GAAkB,EAAE;EAC7D,IAAIA,GAAG,IAAIR,QAAQ,CAACS,GAAG,CAACF,OAAO,CAAC,EAAE;AAChC,IAAA,MAAM,IAAIG,KAAK,CAAC,CAAA,sDAAA,CAAwD,CAAC;AAC3E;AACA,EAAA,IAAIF,GAAG,EAAE;AACPR,IAAAA,QAAQ,CAACW,GAAG,CAACJ,OAAO,EAAE;AAAEK,MAAAA,EAAE,EAAEJ,GAAG;AAAEK,MAAAA,OAAO,EAAE,CAAC;AAAEC,MAAAA,IAAI,EAAE;AAAE,KAAC,CAAC;AACzD,GAAC,MAAM;AACLd,IAAAA,QAAQ,CAACe,MAAM,CAACR,OAAO,CAAC;AAC1B;AACF;AAEA,IAAIS,YAAY,GAAG,KAAK;AACjB,SAASC,cAAcA,CAACC,KAAc,EAAE;AAC7CF,EAAAA,YAAY,GAAGG,OAAO,CAACD,KAAK,CAAC;AAC/B;AACO,SAASE,cAAcA,GAAG;AAC/B,EAAA,OAAOJ,YAAY;AACrB;AAEO,MAAMK,iBAAiB,CAAoB;EAEhDC,WAAWA,CAACC,KAAa,EAAE;IACzB,IAAI,CAACA,KAAK,GAAGA,KAAK;AACpB;AACA,EAAA,MAAMV,OAAOA,CAAIN,OAAuB,EAAEiB,IAAe,EAAsC;IAC7F,MAAMC,IAAI,GAAGzB,QAAQ,CAAC0B,GAAG,CAAC,IAAI,CAACH,KAAK,CAAC;IACrC,IAAI,CAACE,IAAI,EAAE;AACT,MAAA,MAAM,IAAIf,KAAK,CACb,CAAA,gGAAA,CACF,CAAC;AACH;AAEA,IAAA,MAAMG,OAAoB,GAAGc,MAAM,CAACC,MAAM,CAAC,EAAE,EAAErB,OAAO,CAACM,OAAO,CAAC;IAC/D,MAAMgB,WAAW,GAAGhB,OAAO,CAACiB,GAAG,CAAEzB,QAAQ,CAAC,WAAW,CAAC;AACtD,IAAA,MAAM0B,SAAS,GAAGlB,OAAO,CAACiB,GAAG,CAAEE,QAAQ,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG;IACxD,MAAMC,YAAY,GAAG,CAAGF,EAAAA,SAAS,aAAaN,IAAI,CAACb,EAAE,CACnDiB,sBAAAA,EAAAA,WAAW,GAAGJ,IAAI,CAACX,IAAI,EAAE,GAAGW,IAAI,CAACZ,OAAO,EAAE,CAC1C,CAAA;AACFA,IAAAA,OAAO,CAACiB,GAAG,GAAGjB,OAAO,CAACiB,GAAG,GAAGG,YAAY;IAExCpB,OAAO,CAACqB,IAAI,GAAG,MAAM;IACrBrB,OAAO,CAACsB,WAAW,GAAG,MAAM;IAC5BtB,OAAO,CAACuB,cAAc,GAAG,EAAE;IAE3B,IAAI;AACF,MAAA,MAAMC,MAAM,GAAGb,IAAI,CAACX,OAAO,CAAC;MAC5BN,OAAO,CAAC+B,SAAS,CAACD,MAAM,CAACE,SAAS,EAAE,CAAC;AACrC,MAAA,OAAO,MAAMF,MAAM;KACpB,CAAC,OAAOG,CAAC,EAAE;MACV,IAAIA,CAAC,YAAY9B,KAAK,IAAI,EAAE8B,CAAC,YAAYC,YAAY,CAAC,EAAE;AACtDD,QAAAA,CAAC,CAACE,OAAO,GAAGF,CAAC,CAACE,OAAO,CAACC,OAAO,CAACV,YAAY,EAAE,EAAE,CAAC;AACjD;AACA,MAAA,MAAMO,CAAC;AACT;AACF;AACF;AAEO,eAAe1B,IAAIA,CAACS,KAAa,EAAEqB,QAA2B,EAAEf,WAAqB,EAAE;AAC5F,EAAA,MAAMJ,IAAI,GAAGzB,QAAQ,CAAC0B,GAAG,CAACH,KAAK,CAAC;EAChC,IAAI,CAACE,IAAI,EAAE;AACT,IAAA,MAAM,IAAIf,KAAK,CAAC,CAAA,6FAAA,CAA+F,CAAC;AAClH;AACA,EAAA,MAAMmC,WAAW,GAAGpB,IAAI,CAACX,IAAI,EAAE;AAC/B,EAAA,IAAIM,cAAc,EAAE,IAAIS,WAAW,EAAE;IAEnC,MAAMC,GAAG,GAAG,CAAA,EAAG5B,IAAI,CAAA,mBAAA,EAAsBuB,IAAI,CAACb,EAAE,CAAyBiC,sBAAAA,EAAAA,WAAW,CAAE,CAAA;IACtF,MAAMC,KAAK,CAAChB,GAAG,EAAE;AACfiB,MAAAA,MAAM,EAAE,MAAM;MACdC,IAAI,EAAEC,IAAI,CAACC,SAAS,CAACN,QAAQ,EAAE,CAAC;AAChCV,MAAAA,IAAI,EAAE,MAAM;AACZC,MAAAA,WAAW,EAAE,MAAM;AACnBC,MAAAA,cAAc,EAAE;AAClB,KAAC,CAAC;AACJ;AACF;;;;"}
|
package/dist/mock.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock.js","sources":["../src/mock.ts"],"sourcesContent":["import { getIsRecording, mock } from '.';\n\nexport interface Scaffold {\n status: number;\n statusText?: string;\n headers: Record<string, string>;\n body: Record<string, string> | string | null;\n method: string;\n url: string;\n response: Record<string, unknown>;\n}\n\nexport type ScaffoldGenerator = () => Scaffold;\nexport type ResponseGenerator = () => Record<string, unknown>;\n\n/**\n * Sets up Mocking for a GET request on the mock server\n * for the supplied url.\n *\n * The response body is generated by the supplied response function.\n *\n * Available options:\n * - status: the status code to return (default: 200)\n * - headers: the headers to return (default: {})\n * - body: the body to match against for the request (default: null)\n * - RECORD: whether to record the request (default: false)\n *\n * @param url the url to mock, relative to the mock server host (e.g. `users/1`)\n * @param response a function which generates the response to return\n * @param options status, headers for the response, body to match against for the request, and whether to record the request\n * @return\n */\nexport function GET(\n owner: object,\n url: string,\n response: ResponseGenerator,\n options?: Partial<Omit<Scaffold, 'response' | 'url' | 'method'>> & { RECORD?: boolean }\n): Promise<void> {\n return mock(\n owner,\n () => ({\n status: options?.status ?? 200,\n statusText: options?.statusText ?? 'OK',\n headers: options?.headers ?? {},\n body: options?.body ?? null,\n method: 'GET',\n url,\n response: response(),\n }),\n getIsRecording() || (options?.RECORD ?? false)\n );\n}\nexport function POST() {}\nexport function PUT() {}\nexport function PATCH() {}\nexport function DELETE() {}\nexport function QUERY() {}\n"],"names":["GET","owner","url","response","options","mock","status","statusText","headers","body","method","getIsRecording","RECORD","POST","PUT","PATCH","DELETE","QUERY"],"mappings":";;AAeA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,GAAGA,CACjBC,KAAa,EACbC,GAAW,EACXC,QAA2B,EAC3BC,OAAuF,EACxE;AACf,EAAA,OAAOC,IAAI,CACTJ,KAAK,EACL,OAAO;AACLK,IAAAA,MAAM,EAAEF,OAAO,EAAEE,MAAM,IAAI,GAAG;AAC9BC,IAAAA,UAAU,EAAEH,OAAO,EAAEG,UAAU,IAAI,IAAI;AACvCC,IAAAA,OAAO,EAAEJ,OAAO,EAAEI,OAAO,IAAI,EAAE;AAC/BC,IAAAA,IAAI,EAAEL,OAAO,EAAEK,IAAI,IAAI,IAAI;AAC3BC,IAAAA,MAAM,EAAE,KAAK;IACbR,GAAG;IACHC,QAAQ,EAAEA,QAAQ
|
|
1
|
+
{"version":3,"file":"mock.js","sources":["../src/mock.ts"],"sourcesContent":["import { getIsRecording, mock } from '.';\n\nexport interface Scaffold {\n status: number;\n statusText?: string;\n headers: Record<string, string>;\n body: Record<string, string> | string | null;\n method: string;\n url: string;\n response: Record<string, unknown>;\n}\n\nexport type ScaffoldGenerator = () => Scaffold;\nexport type ResponseGenerator = () => Record<string, unknown>;\n\n/**\n * Sets up Mocking for a GET request on the mock server\n * for the supplied url.\n *\n * The response body is generated by the supplied response function.\n *\n * Available options:\n * - status: the status code to return (default: 200)\n * - headers: the headers to return (default: {})\n * - body: the body to match against for the request (default: null)\n * - RECORD: whether to record the request (default: false)\n *\n * @param url the url to mock, relative to the mock server host (e.g. `users/1`)\n * @param response a function which generates the response to return\n * @param options status, headers for the response, body to match against for the request, and whether to record the request\n * @return\n */\nexport function GET(\n owner: object,\n url: string,\n response: ResponseGenerator,\n options?: Partial<Omit<Scaffold, 'response' | 'url' | 'method'>> & { RECORD?: boolean }\n): Promise<void> {\n return mock(\n owner,\n () => ({\n status: options?.status ?? 200,\n statusText: options?.statusText ?? 'OK',\n headers: options?.headers ?? {},\n body: options?.body ?? null,\n method: 'GET',\n url,\n response: response(),\n }),\n getIsRecording() || (options?.RECORD ?? false)\n );\n}\nexport function POST() {}\nexport function PUT() {}\nexport function PATCH() {}\nexport function DELETE() {}\nexport function QUERY() {}\n"],"names":["GET","owner","url","response","options","mock","status","statusText","headers","body","method","getIsRecording","RECORD","POST","PUT","PATCH","DELETE","QUERY"],"mappings":";;AAeA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,GAAGA,CACjBC,KAAa,EACbC,GAAW,EACXC,QAA2B,EAC3BC,OAAuF,EACxE;AACf,EAAA,OAAOC,IAAI,CACTJ,KAAK,EACL,OAAO;AACLK,IAAAA,MAAM,EAAEF,OAAO,EAAEE,MAAM,IAAI,GAAG;AAC9BC,IAAAA,UAAU,EAAEH,OAAO,EAAEG,UAAU,IAAI,IAAI;AACvCC,IAAAA,OAAO,EAAEJ,OAAO,EAAEI,OAAO,IAAI,EAAE;AAC/BC,IAAAA,IAAI,EAAEL,OAAO,EAAEK,IAAI,IAAI,IAAI;AAC3BC,IAAAA,MAAM,EAAE,KAAK;IACbR,GAAG;IACHC,QAAQ,EAAEA,QAAQ;AACpB,GAAC,CAAC,EACFQ,cAAc,EAAE,KAAKP,OAAO,EAAEQ,MAAM,IAAI,KAAK,CAC/C,CAAC;AACH;AACO,SAASC,IAAIA,GAAG;AAChB,SAASC,GAAGA,GAAG;AACf,SAASC,KAAKA,GAAG;AACjB,SAASC,MAAMA,GAAG;AAClB,SAASC,KAAKA,GAAG;;;;"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warp-drive/holodeck",
|
|
3
3
|
"description": "⚡️ Simple, Fast HTTP Mocking for Tests",
|
|
4
|
-
"version": "0.0.0-alpha.
|
|
4
|
+
"version": "0.0.0-alpha.127",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chris Thoburn <runspired@users.noreply.github.com>",
|
|
7
7
|
"repository": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"homepage": "https://github.com/emberjs/data",
|
|
13
13
|
"bugs": "https://github.com/emberjs/data/issues",
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">= 18.20.
|
|
15
|
+
"node": ">= 18.20.7"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"http-mock"
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"extends": "../../package.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@hono/node-server": "^1.11.1",
|
|
25
24
|
"chalk": "^5.3.0",
|
|
26
|
-
"hono": "^4.7.
|
|
25
|
+
"hono": "^4.7.2",
|
|
26
|
+
"@hono/node-server": "^1.13.8"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"files": [
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"ensure-cert": "./server/ensure-cert.js"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@ember-data/request": "5.4.0-alpha.
|
|
43
|
-
"@warp-drive/core-types": "5.4.0-alpha.
|
|
42
|
+
"@ember-data/request": "5.4.0-alpha.141",
|
|
43
|
+
"@warp-drive/core-types": "5.4.0-alpha.141"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@babel/core": "^7.24.5",
|
|
@@ -48,12 +48,10 @@
|
|
|
48
48
|
"@babel/preset-env": "^7.24.5",
|
|
49
49
|
"@babel/preset-typescript": "^7.24.1",
|
|
50
50
|
"@babel/runtime": "^7.24.5",
|
|
51
|
-
"@ember-data/request": "5.4.0-alpha.
|
|
52
|
-
"@warp-drive/core-types": "5.4.0-alpha.
|
|
53
|
-
"@warp-drive/internal-config": "5.4.0-alpha.
|
|
54
|
-
"
|
|
55
|
-
"typescript": "^5.7.2",
|
|
56
|
-
"vite": "^5.2.11"
|
|
51
|
+
"@ember-data/request": "5.4.0-alpha.141",
|
|
52
|
+
"@warp-drive/core-types": "5.4.0-alpha.141",
|
|
53
|
+
"@warp-drive/internal-config": "5.4.0-alpha.141",
|
|
54
|
+
"vite": "^5.4.14"
|
|
57
55
|
},
|
|
58
56
|
"exports": {
|
|
59
57
|
".": {
|
|
@@ -72,17 +70,9 @@
|
|
|
72
70
|
"default": "./dist/mock.js"
|
|
73
71
|
}
|
|
74
72
|
},
|
|
75
|
-
"dependenciesMeta": {
|
|
76
|
-
"@ember-data/request": {
|
|
77
|
-
"injected": true
|
|
78
|
-
},
|
|
79
|
-
"@warp-drive/core-types": {
|
|
80
|
-
"injected": true
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
73
|
"scripts": {
|
|
84
74
|
"check:pkg-types": "tsc --noEmit",
|
|
85
75
|
"build:pkg": "vite build;",
|
|
86
|
-
"sync
|
|
76
|
+
"sync": "echo \"syncing\""
|
|
87
77
|
}
|
|
88
78
|
}
|
package/server/index.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
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';
|
|
7
|
+
import { HTTPException } from 'hono/http-exception';
|
|
8
|
+
import { cors } from 'hono/cors';
|
|
8
9
|
import crypto from 'node:crypto';
|
|
9
10
|
import fs from 'node:fs';
|
|
10
|
-
import http2 from 'node:http2';
|
|
11
11
|
import zlib from 'node:zlib';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
|
|
15
|
-
/** @type {import('bun-types')} */
|
|
16
15
|
const isBun = typeof Bun !== 'undefined';
|
|
17
|
-
const DEBUG =
|
|
18
|
-
|
|
16
|
+
const DEBUG =
|
|
17
|
+
process.env.DEBUG?.includes('wd:holodeck') || process.env.DEBUG === '*' || process.env.DEBUG?.includes('wd:*');
|
|
19
18
|
|
|
20
|
-
function getCertInfo() {
|
|
19
|
+
async function getCertInfo() {
|
|
21
20
|
let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
|
|
22
21
|
let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
|
|
23
22
|
|
|
@@ -39,16 +38,32 @@ function getCertInfo() {
|
|
|
39
38
|
);
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (isBun) {
|
|
42
|
+
const CERT = Bun.file(CERT_PATH);
|
|
43
|
+
const KEY = Bun.file(KEY_PATH);
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
if (!(await CERT.exists()) || !(await KEY.exists())) {
|
|
46
|
+
throw new Error('SSL certificate or key not found, you may need to run `pnpx @warp-drive/holodeck ensure-cert`');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
CERT_PATH,
|
|
51
|
+
KEY_PATH,
|
|
52
|
+
CERT: await CERT.text(),
|
|
53
|
+
KEY: await KEY.text(),
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
|
|
57
|
+
throw new Error('SSL certificate or key not found, you may need to run `pnpx @warp-drive/holodeck ensure-cert`');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
CERT_PATH,
|
|
62
|
+
KEY_PATH,
|
|
63
|
+
CERT: fs.readFileSync(CERT_PATH, 'utf8'),
|
|
64
|
+
KEY: fs.readFileSync(KEY_PATH, 'utf8'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
const DEFAULT_PORT = 1135;
|
|
@@ -85,7 +100,7 @@ function getNiceUrl(url) {
|
|
|
85
100
|
*/
|
|
86
101
|
function generateFilepath(options) {
|
|
87
102
|
const { body } = options;
|
|
88
|
-
const bodyHash = body ? crypto.createHash('md5').update(body).digest('hex') : null;
|
|
103
|
+
const bodyHash = body ? crypto.createHash('md5').update(JSON.stringify(body)).digest('hex') : null;
|
|
89
104
|
const cacheDir = generateFileDir(options);
|
|
90
105
|
return `${cacheDir}/${bodyHash ? `${bodyHash}-` : 'res'}`;
|
|
91
106
|
}
|
|
@@ -94,10 +109,14 @@ function generateFileDir(options) {
|
|
|
94
109
|
return `${projectRoot}/.mock-cache/${testId}/${method}-${testRequestNumber}-${url}`;
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
function replayRequest(context, cacheKey) {
|
|
98
|
-
let
|
|
112
|
+
async function replayRequest(context, cacheKey) {
|
|
113
|
+
let metaJson;
|
|
99
114
|
try {
|
|
100
|
-
|
|
115
|
+
if (isBun) {
|
|
116
|
+
metaJson = await Bun.file(`${cacheKey}.meta.json`).json();
|
|
117
|
+
} else {
|
|
118
|
+
metaJson = JSON.parse(fs.readFileSync(`${cacheKey}.meta.json`, 'utf8'));
|
|
119
|
+
}
|
|
101
120
|
} catch (e) {
|
|
102
121
|
context.header('Content-Type', 'application/vnd.api+json');
|
|
103
122
|
context.status(400);
|
|
@@ -108,18 +127,23 @@ function replayRequest(context, cacheKey) {
|
|
|
108
127
|
status: '400',
|
|
109
128
|
code: 'MOCK_NOT_FOUND',
|
|
110
129
|
title: 'Mock not found',
|
|
111
|
-
detail: `No
|
|
130
|
+
detail: `No meta was found for ${context.req.method} ${context.req.url}. You may need to record a mock for this request.`,
|
|
112
131
|
},
|
|
113
132
|
],
|
|
114
133
|
})
|
|
115
134
|
);
|
|
116
135
|
}
|
|
117
136
|
|
|
118
|
-
const metaJson = JSON.parse(meta);
|
|
119
137
|
const bodyPath = `${cacheKey}.body.br`;
|
|
138
|
+
const bodyInit =
|
|
139
|
+
metaJson.status !== 204 && metaJson.status < 500
|
|
140
|
+
? isBun
|
|
141
|
+
? Bun.file(bodyPath)
|
|
142
|
+
: fs.createReadStream(bodyPath)
|
|
143
|
+
: '';
|
|
120
144
|
|
|
121
145
|
const headers = new Headers(metaJson.headers || {});
|
|
122
|
-
|
|
146
|
+
// @ts-expect-error - createReadStream is supported in node
|
|
123
147
|
const response = new Response(bodyInit, {
|
|
124
148
|
status: metaJson.status,
|
|
125
149
|
statusText: metaJson.statusText,
|
|
@@ -191,12 +215,15 @@ function createTestHandler(projectRoot) {
|
|
|
191
215
|
body: body ? JSON.stringify(body) : null,
|
|
192
216
|
testRequestNumber,
|
|
193
217
|
});
|
|
218
|
+
const compressedResponse = compress(JSON.stringify(response));
|
|
194
219
|
// allow Content-Type to be overridden
|
|
195
220
|
headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
|
|
196
221
|
// We always compress and chunk the response
|
|
197
222
|
headers['Content-Encoding'] = 'br';
|
|
198
223
|
// we don't cache since tests will often reuse similar urls for different payload
|
|
199
224
|
headers['Cache-Control'] = 'no-store';
|
|
225
|
+
// streaming requires Content-Length
|
|
226
|
+
headers['Content-Length'] = compressedResponse.length;
|
|
200
227
|
|
|
201
228
|
const cacheDir = generateFileDir({
|
|
202
229
|
projectRoot,
|
|
@@ -207,21 +234,30 @@ function createTestHandler(projectRoot) {
|
|
|
207
234
|
});
|
|
208
235
|
|
|
209
236
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
237
|
+
|
|
238
|
+
if (isBun) {
|
|
239
|
+
const newMetaFile = Bun.file(`${cacheKey}.meta.json`);
|
|
240
|
+
await newMetaFile.write(JSON.stringify({ url, status, statusText, headers, method, requestBody: body }));
|
|
241
|
+
const newBodyFile = Bun.file(`${cacheKey}.body.br`);
|
|
242
|
+
await newBodyFile.write(compressedResponse);
|
|
243
|
+
} else {
|
|
244
|
+
fs.writeFileSync(
|
|
245
|
+
`${cacheKey}.meta.json`,
|
|
246
|
+
JSON.stringify({ url, status, statusText, headers, method, requestBody: body })
|
|
247
|
+
);
|
|
248
|
+
fs.writeFileSync(`${cacheKey}.body.br`, compressedResponse);
|
|
249
|
+
}
|
|
250
|
+
|
|
215
251
|
context.status(204);
|
|
216
252
|
return context.body(null);
|
|
217
253
|
} else {
|
|
218
|
-
const body =
|
|
254
|
+
const body = req.body;
|
|
219
255
|
const cacheKey = generateFilepath({
|
|
220
256
|
projectRoot,
|
|
221
257
|
testId,
|
|
222
258
|
url: niceUrl,
|
|
223
259
|
method: req.method,
|
|
224
|
-
body,
|
|
260
|
+
body: body ? JSON.stringify(body) : null,
|
|
225
261
|
testRequestNumber,
|
|
226
262
|
});
|
|
227
263
|
return replayRequest(context, cacheKey);
|
|
@@ -250,10 +286,68 @@ function createTestHandler(projectRoot) {
|
|
|
250
286
|
return TestHandler;
|
|
251
287
|
}
|
|
252
288
|
|
|
289
|
+
export function startNodeServer() {
|
|
290
|
+
const args = process.argv.slice();
|
|
291
|
+
|
|
292
|
+
if (!isBun && args.length) {
|
|
293
|
+
const options = JSON.parse(args[2]);
|
|
294
|
+
_createServer(options);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function startWorker() {
|
|
299
|
+
// listen for launch message
|
|
300
|
+
globalThis.onmessage = async (event) => {
|
|
301
|
+
const { options } = event.data;
|
|
302
|
+
|
|
303
|
+
const { server } = await _createServer(options);
|
|
304
|
+
|
|
305
|
+
// listen for messages
|
|
306
|
+
globalThis.onmessage = (event) => {
|
|
307
|
+
const message = event.data;
|
|
308
|
+
if (message === 'end') {
|
|
309
|
+
server.close();
|
|
310
|
+
globalThis.close();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
253
316
|
/*
|
|
254
317
|
{ port?: number, projectRoot: string }
|
|
255
318
|
*/
|
|
256
|
-
export function createServer(options) {
|
|
319
|
+
export async function createServer(options, useBun = false) {
|
|
320
|
+
if (!useBun) {
|
|
321
|
+
const CURRENT_FILE = new URL(import.meta.url).pathname;
|
|
322
|
+
const START_FILE = path.join(CURRENT_FILE, '../start-node.js');
|
|
323
|
+
const server = Bun.spawn(['node', '--experimental-default-type=module', START_FILE, JSON.stringify(options)], {
|
|
324
|
+
env: process.env,
|
|
325
|
+
cwd: process.cwd(),
|
|
326
|
+
stdin: 'inherit',
|
|
327
|
+
stdout: 'inherit',
|
|
328
|
+
stderr: 'inherit',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
terminate() {
|
|
333
|
+
server.kill();
|
|
334
|
+
// server.unref();
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
340
|
+
|
|
341
|
+
worker.postMessage({
|
|
342
|
+
type: 'launch',
|
|
343
|
+
options,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return worker;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function _createServer(options) {
|
|
350
|
+
const { CERT, KEY } = await getCertInfo();
|
|
257
351
|
const app = new Hono();
|
|
258
352
|
if (DEBUG) {
|
|
259
353
|
app.use('*', logger());
|
|
@@ -272,38 +366,26 @@ export function createServer(options) {
|
|
|
272
366
|
);
|
|
273
367
|
app.all('*', createTestHandler(options.projectRoot));
|
|
274
368
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
serve({
|
|
369
|
+
const server = serve({
|
|
370
|
+
overrideGlobalObjects: !isBun,
|
|
278
371
|
fetch: app.fetch,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
{
|
|
283
|
-
key: KEY,
|
|
284
|
-
cert: CERT,
|
|
285
|
-
},
|
|
286
|
-
requestListener
|
|
287
|
-
);
|
|
288
|
-
} catch (e) {
|
|
289
|
-
console.log(chalk.yellow(`Failed to create secure server, falling back to http server. Error: ${e.message}`));
|
|
290
|
-
return http2.createServer(requestListener);
|
|
291
|
-
}
|
|
372
|
+
serverOptions: {
|
|
373
|
+
key: KEY,
|
|
374
|
+
cert: CERT,
|
|
292
375
|
},
|
|
376
|
+
createServer: createSecureServer,
|
|
293
377
|
port: options.port ?? DEFAULT_PORT,
|
|
294
|
-
hostname: 'localhost',
|
|
295
|
-
// bun uses TLS options
|
|
296
|
-
// tls: {
|
|
297
|
-
// key: Bun.file(KEY_PATH),
|
|
298
|
-
// cert: Bun.file(CERT_PATH),
|
|
299
|
-
// },
|
|
378
|
+
hostname: options.hostname ?? 'localhost',
|
|
300
379
|
});
|
|
301
380
|
|
|
302
381
|
console.log(
|
|
303
|
-
`\tMock server running at ${chalk.
|
|
382
|
+
`\tMock server running at ${chalk.yellow('https://') + chalk.magenta((options.hostname ?? 'localhost') + ':') + chalk.yellow(options.port ?? DEFAULT_PORT)}`
|
|
304
383
|
);
|
|
384
|
+
|
|
385
|
+
return { app, server };
|
|
305
386
|
}
|
|
306
387
|
|
|
388
|
+
/** @type {Map<string, Awaited<ReturnType<typeof createServer>>>} */
|
|
307
389
|
const servers = new Map();
|
|
308
390
|
|
|
309
391
|
export default {
|
|
@@ -325,59 +407,29 @@ export default {
|
|
|
325
407
|
)}`
|
|
326
408
|
)
|
|
327
409
|
);
|
|
328
|
-
console.log(chalk.grey(`\n\tStarting Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
329
|
-
|
|
330
|
-
if (isBun) {
|
|
331
|
-
const serverProcess = Bun.spawn(
|
|
332
|
-
['node', '--experimental-default-type=module', CURRENT_FILE, JSON.stringify(options)],
|
|
333
|
-
{
|
|
334
|
-
env: process.env,
|
|
335
|
-
cwd: process.cwd(),
|
|
336
|
-
stdout: 'inherit',
|
|
337
|
-
stderr: 'inherit',
|
|
338
|
-
}
|
|
339
|
-
);
|
|
340
|
-
servers.set(projectRoot, serverProcess);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
410
|
+
console.log(chalk.grey(`\n\tStarting Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
343
411
|
|
|
344
412
|
if (servers.has(projectRoot)) {
|
|
345
413
|
throw new Error(`Holodeck is already running for project '${name}' at '${projectRoot}'`);
|
|
346
414
|
}
|
|
347
415
|
|
|
348
|
-
|
|
416
|
+
// toggle to true if Bun fixes CORS support for HTTP/2
|
|
417
|
+
const project = await createServer(options, false);
|
|
418
|
+
servers.set(projectRoot, project);
|
|
349
419
|
},
|
|
350
420
|
async endProgram() {
|
|
351
|
-
console.log(chalk.grey(`\n\tEnding Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
421
|
+
console.log(chalk.grey(`\n\tEnding Holodeck Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
|
|
352
422
|
const projectRoot = process.cwd();
|
|
353
423
|
|
|
354
424
|
if (!servers.has(projectRoot)) {
|
|
355
|
-
const name =
|
|
356
|
-
|
|
357
|
-
);
|
|
358
|
-
throw new Error(`Holodeck was not running for project '${name}' at '${projectRoot}'`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (isBun) {
|
|
362
|
-
const serverProcess = servers.get(projectRoot);
|
|
363
|
-
serverProcess.kill();
|
|
425
|
+
const name = require(path.join(projectRoot, 'package.json')).name;
|
|
426
|
+
console.log(chalk.red(`\n\nHolodeck was not running for project '${name}' at '${projectRoot}'\n\n`));
|
|
364
427
|
return;
|
|
365
428
|
}
|
|
366
429
|
|
|
367
|
-
servers.get(projectRoot)
|
|
430
|
+
const project = servers.get(projectRoot);
|
|
368
431
|
servers.delete(projectRoot);
|
|
432
|
+
project.terminate();
|
|
433
|
+
console.log(chalk.grey(`\n\tHolodeck program ended`));
|
|
369
434
|
},
|
|
370
435
|
};
|
|
371
|
-
|
|
372
|
-
function main() {
|
|
373
|
-
const args = process.argv.slice();
|
|
374
|
-
if (!isBun && args.length) {
|
|
375
|
-
if (args[1] !== CURRENT_FILE) {
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
const options = JSON.parse(args[2]);
|
|
379
|
-
createServer(options);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
main();
|
package/server/worker.js
ADDED