mockaton 8.24.0 → 8.26.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/README.md +35 -10
- package/index.d.ts +3 -1
- package/package.json +2 -2
- package/src/Dashboard.css +6 -9
- package/src/Dashboard.js +28 -8
- package/src/Filename.js +1 -8
- package/src/MockDispatcher.js +3 -2
- package/src/Mockaton.js +7 -6
- package/src/StaticDispatcher.js +3 -1
- package/src/cli.js +14 -8
- package/src/config.js +9 -5
- package/src/mockBrokersCollection.js +9 -1
- package/src/utils/fs.js +2 -1
- package/src/utils/http-response.js +4 -3
- package/src/utils/log.js +34 -0
package/README.md
CHANGED
|
@@ -55,9 +55,9 @@ Nonetheless, there’s a programmatic API, which is handy
|
|
|
55
55
|
for setting up tests (see **Commander API** section below).
|
|
56
56
|
|
|
57
57
|
<picture>
|
|
58
|
-
<source media="(prefers-color-scheme: light)" srcset="pixaton-tests/macos/pic-for-readme.
|
|
59
|
-
<source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/macos/pic-for-readme.
|
|
60
|
-
<img alt="Mockaton Dashboard" src="pixaton-tests/macos/pic-for-readme.
|
|
58
|
+
<source media="(prefers-color-scheme: light)" srcset="pixaton-tests/macos/pic-for-readme.vp880x768.light.gold.png">
|
|
59
|
+
<source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/macos/pic-for-readme.vp880x768.dark.gold.png">
|
|
60
|
+
<img alt="Mockaton Dashboard" src="pixaton-tests/macos/pic-for-readme.vp880x768.light.gold.png">
|
|
61
61
|
</picture>
|
|
62
62
|
|
|
63
63
|
|
|
@@ -132,13 +132,17 @@ npx mockaton --port 2345
|
|
|
132
132
|
CLI options override their counterparts in `mockaton.config.js`
|
|
133
133
|
|
|
134
134
|
```txt
|
|
135
|
-
-c, --config <file>
|
|
135
|
+
-c, --config <file> (default: ./mockaton.config.js)
|
|
136
136
|
|
|
137
|
-
-
|
|
138
|
-
-
|
|
137
|
+
-m, --mocks-dir <dir> (default: ./mockaton-mocks/)
|
|
138
|
+
-s, --static-dir <dir> (default: ./mockaton-static-mocks/)
|
|
139
139
|
|
|
140
|
-
-
|
|
141
|
-
-
|
|
140
|
+
-H, --host <host> (default: 127.0.0.1)
|
|
141
|
+
-p, --port <port> (default: 0) which means auto-assigned
|
|
142
|
+
|
|
143
|
+
-q, --quiet Errors only
|
|
144
|
+
-h, --help Show this help
|
|
145
|
+
-v, --version Show version
|
|
142
146
|
```
|
|
143
147
|
|
|
144
148
|
|
|
@@ -152,7 +156,6 @@ import {
|
|
|
152
156
|
SUPPORTED_METHODS
|
|
153
157
|
} from 'mockaton'
|
|
154
158
|
|
|
155
|
-
|
|
156
159
|
export default defineConfig({
|
|
157
160
|
mocksDir: 'mockaton-mocks',
|
|
158
161
|
staticDir: 'mockaton-static-mocks',
|
|
@@ -186,7 +189,9 @@ export default defineConfig({
|
|
|
186
189
|
corsCredentials: true,
|
|
187
190
|
corsMaxAge: 0,
|
|
188
191
|
|
|
189
|
-
onReady: await openInBrowser
|
|
192
|
+
onReady: await openInBrowser,
|
|
193
|
+
|
|
194
|
+
logLevel: 'normal'
|
|
190
195
|
})
|
|
191
196
|
```
|
|
192
197
|
|
|
@@ -421,9 +426,29 @@ config.onReady = () => {}
|
|
|
421
426
|
|
|
422
427
|
At any rate, you can trigger any command besides opening a browser.
|
|
423
428
|
|
|
429
|
+
<br/>
|
|
430
|
+
|
|
431
|
+
### `logLevel?: 'quiet' | 'normal'`
|
|
432
|
+
Defaults to `'normal'`.
|
|
433
|
+
|
|
434
|
+
- `quiet`: only errors (stderr)
|
|
435
|
+
- `normal`: info, access, warnings, and errors
|
|
436
|
+
|
|
437
|
+
</details>
|
|
438
|
+
|
|
424
439
|
|
|
440
|
+
<details>
|
|
441
|
+
<summary>Programmatic Launch (Optional)</summary>
|
|
425
442
|
|
|
443
|
+
```js
|
|
444
|
+
import { Mockaton } from 'mockaton'
|
|
445
|
+
import mockatonConfig from './mockaton.config.js'
|
|
426
446
|
|
|
447
|
+
Mockaton({
|
|
448
|
+
...mockatonConfig, // Not required, but it’s not read by default.
|
|
449
|
+
port: 3333, // etc.
|
|
450
|
+
})
|
|
451
|
+
```
|
|
427
452
|
</details>
|
|
428
453
|
|
|
429
454
|
|
package/index.d.ts
CHANGED
|
@@ -41,10 +41,12 @@ interface Config {
|
|
|
41
41
|
corsMaxAge?: number
|
|
42
42
|
|
|
43
43
|
onReady?: (address: string) => void
|
|
44
|
+
|
|
45
|
+
logLevel?: 'normal' | 'quiet'
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
export function Mockaton(options: Partial<Config>): Server
|
|
49
|
+
export function Mockaton(options: Partial<Config>): Server | undefined
|
|
48
50
|
export function defineConfig(options: Partial<Config>): Config
|
|
49
51
|
|
|
50
52
|
export const jsToJsonPlugin: Plugin
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "mockaton",
|
|
3
3
|
"description": "HTTP Mock Server",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.
|
|
5
|
+
"version": "8.26.0",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "node --test \"src/**/*.test.js\"",
|
|
22
22
|
"coverage": "node --test --test-reporter=lcov --test-reporter-destination=.coverage/lcov.info --experimental-test-coverage \"src/**/*.test.js\"",
|
|
23
|
-
"start": "node --watch src/cli.js
|
|
23
|
+
"start": "node --watch src/cli.js",
|
|
24
24
|
"pixaton": "node --test --import=./pixaton-tests/_setup.js --experimental-test-isolation=none \"pixaton-tests/**/*.test.js\"",
|
|
25
25
|
"outdated": "npm outdated --parseable | awk -F: '{ printf \"npm i %-30s ;# %s\\n\", $4, $2 }'"
|
|
26
26
|
},
|
package/src/Dashboard.css
CHANGED
|
@@ -305,6 +305,10 @@ main {
|
|
|
305
305
|
table {
|
|
306
306
|
border-collapse: collapse;
|
|
307
307
|
|
|
308
|
+
tr {
|
|
309
|
+
border-top: 2px solid transparent;
|
|
310
|
+
}
|
|
311
|
+
|
|
308
312
|
th {
|
|
309
313
|
padding-bottom: 2px;
|
|
310
314
|
padding-left: 4px;
|
|
@@ -314,12 +318,6 @@ table {
|
|
|
314
318
|
tbody {
|
|
315
319
|
border-bottom: 20px solid transparent;
|
|
316
320
|
}
|
|
317
|
-
|
|
318
|
-
td:nth-child(3),
|
|
319
|
-
td:nth-child(4),
|
|
320
|
-
td:nth-child(5) {
|
|
321
|
-
max-width: 280px;
|
|
322
|
-
}
|
|
323
321
|
}
|
|
324
322
|
|
|
325
323
|
.empty {
|
|
@@ -332,7 +330,7 @@ table {
|
|
|
332
330
|
left: -8px;
|
|
333
331
|
display: inline-block;
|
|
334
332
|
width: 100%;
|
|
335
|
-
padding: 8px;
|
|
333
|
+
padding: 6px 8px;
|
|
336
334
|
margin-left: 4px;
|
|
337
335
|
border-radius: var(--radius);
|
|
338
336
|
color: var(--colorAccent);
|
|
@@ -349,9 +347,8 @@ table {
|
|
|
349
347
|
}
|
|
350
348
|
|
|
351
349
|
.MockSelector {
|
|
352
|
-
width: 100%;
|
|
353
350
|
height: 26px;
|
|
354
|
-
padding-right:
|
|
351
|
+
padding-right: 20px;
|
|
355
352
|
padding-left: 8px;
|
|
356
353
|
text-overflow: ellipsis;
|
|
357
354
|
font-size: 12px;
|
package/src/Dashboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_500_COMMENT, HEADER_FOR_502 } from './ApiConstants.js'
|
|
2
|
-
import { parseFilename } from './Filename.js'
|
|
2
|
+
import { parseFilename, extractComments } from './Filename.js'
|
|
3
3
|
import { Commander } from './ApiCommander.js'
|
|
4
4
|
|
|
5
5
|
|
|
@@ -394,11 +394,15 @@ function MockSelector({ broker }) {
|
|
|
394
394
|
CSS.MockSelector,
|
|
395
395
|
selected !== files[0] && CSS.nonDefault,
|
|
396
396
|
status >= 400 && status < 500 && CSS.status4xx)
|
|
397
|
-
}, files.map(file =>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
397
|
+
}, files.map(file => {
|
|
398
|
+
const { status, ext } = parseFilename(file)
|
|
399
|
+
return (
|
|
400
|
+
r('option', {
|
|
401
|
+
value: file,
|
|
402
|
+
selected: file === selected
|
|
403
|
+
}, `${status} ${ext} ${extractComments(file).join(' ')}`)
|
|
404
|
+
)
|
|
405
|
+
})))
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
/** @param {{ broker: ClientMockBroker }} props */
|
|
@@ -582,7 +586,7 @@ function PayloadViewerTitle({ file, status, statusText }) {
|
|
|
582
586
|
const { urlMask, method, ext } = parseFilename(file)
|
|
583
587
|
return (
|
|
584
588
|
r('span', null,
|
|
585
|
-
urlMask + '.' + method + '.',
|
|
589
|
+
urlMask.replace(/^\//, '') + '.' + method + '.',
|
|
586
590
|
r('abbr', { title: statusText }, status),
|
|
587
591
|
'.' + ext))
|
|
588
592
|
}
|
|
@@ -851,9 +855,12 @@ function dittoSplitPaths(paths) {
|
|
|
851
855
|
|
|
852
856
|
|
|
853
857
|
function syntaxJSON(json) {
|
|
858
|
+
const MAX_NODES = 1000
|
|
859
|
+
let nNodes = 0
|
|
854
860
|
const frag = document.createDocumentFragment()
|
|
855
861
|
|
|
856
862
|
function span(className, textContent) {
|
|
863
|
+
nNodes++
|
|
857
864
|
const s = document.createElement('span')
|
|
858
865
|
s.className = className
|
|
859
866
|
s.textContent = textContent
|
|
@@ -861,13 +868,17 @@ function syntaxJSON(json) {
|
|
|
861
868
|
}
|
|
862
869
|
|
|
863
870
|
function text(t) {
|
|
871
|
+
nNodes++
|
|
864
872
|
frag.appendChild(document.createTextNode(t))
|
|
865
873
|
}
|
|
866
874
|
|
|
867
875
|
let match
|
|
868
876
|
let lastIndex = 0
|
|
869
|
-
syntaxJSON.regex.lastIndex = 0
|
|
877
|
+
syntaxJSON.regex.lastIndex = 0 // resets regex
|
|
870
878
|
while ((match = syntaxJSON.regex.exec(json)) !== null) {
|
|
879
|
+
if (nNodes > MAX_NODES)
|
|
880
|
+
break
|
|
881
|
+
|
|
871
882
|
if (match.index > lastIndex)
|
|
872
883
|
text(json.slice(lastIndex, match.index))
|
|
873
884
|
|
|
@@ -882,6 +893,7 @@ function syntaxJSON(json) {
|
|
|
882
893
|
else if (str) span(CSS.syntaxStr, str)
|
|
883
894
|
else span(CSS.syntaxVal, full)
|
|
884
895
|
}
|
|
896
|
+
frag.normalize()
|
|
885
897
|
text(json.slice(lastIndex))
|
|
886
898
|
return frag
|
|
887
899
|
}
|
|
@@ -891,9 +903,12 @@ syntaxJSON.regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s
|
|
|
891
903
|
|
|
892
904
|
|
|
893
905
|
function syntaxXML(xml) {
|
|
906
|
+
const MAX_NODES = 1000
|
|
907
|
+
let nNodes = 0
|
|
894
908
|
const frag = document.createDocumentFragment()
|
|
895
909
|
|
|
896
910
|
function span(className, textContent) {
|
|
911
|
+
nNodes++
|
|
897
912
|
const s = document.createElement('span')
|
|
898
913
|
s.className = className
|
|
899
914
|
s.textContent = textContent
|
|
@@ -901,6 +916,7 @@ function syntaxXML(xml) {
|
|
|
901
916
|
}
|
|
902
917
|
|
|
903
918
|
function text(t) {
|
|
919
|
+
nNodes++
|
|
904
920
|
frag.appendChild(document.createTextNode(t))
|
|
905
921
|
}
|
|
906
922
|
|
|
@@ -908,6 +924,9 @@ function syntaxXML(xml) {
|
|
|
908
924
|
let lastIndex = 0
|
|
909
925
|
syntaxXML.regex.lastIndex = 0
|
|
910
926
|
while ((match = syntaxXML.regex.exec(xml)) !== null) {
|
|
927
|
+
if (nNodes > MAX_NODES)
|
|
928
|
+
break
|
|
929
|
+
|
|
911
930
|
if (match.index > lastIndex)
|
|
912
931
|
text(xml.slice(lastIndex, match.index))
|
|
913
932
|
|
|
@@ -919,6 +938,7 @@ function syntaxXML(xml) {
|
|
|
919
938
|
else if (match[4]) span(CSS.syntaxAttrVal, match[4])
|
|
920
939
|
}
|
|
921
940
|
text(xml.slice(lastIndex))
|
|
941
|
+
frag.normalize()
|
|
922
942
|
return frag
|
|
923
943
|
}
|
|
924
944
|
syntaxXML.regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
|
package/src/Filename.js
CHANGED
|
@@ -22,15 +22,8 @@ export const includesComment = (filename, search) =>
|
|
|
22
22
|
extractComments(filename).some(comment => comment.includes(search))
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
export function filenameIsValid(file) {
|
|
26
|
-
const error = validateFilename(file)
|
|
27
|
-
if (error)
|
|
28
|
-
console.error(error, file)
|
|
29
|
-
return !error
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
// TODO ThinkAbout 206 (reject, handle, or send in full?)
|
|
33
|
-
function validateFilename(file) {
|
|
26
|
+
export function validateFilename(file) {
|
|
34
27
|
const tokens = file.replace(reComments, '').split('.')
|
|
35
28
|
if (tokens.length < 4)
|
|
36
29
|
return 'Invalid Filename Convention'
|
package/src/MockDispatcher.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path'
|
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
|
|
5
|
+
import { log } from './utils/log.js'
|
|
5
6
|
import { proxy } from './ProxyRelay.js'
|
|
6
7
|
import { cookie } from './cookie.js'
|
|
7
8
|
import { mimeFor } from './utils/mime.js'
|
|
@@ -22,7 +23,7 @@ export async function dispatchMock(req, response) {
|
|
|
22
23
|
return
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
log.access(req.url, broker.file)
|
|
26
27
|
response.statusCode = broker.status
|
|
27
28
|
|
|
28
29
|
if (cookie.getCurrent())
|
|
@@ -45,7 +46,7 @@ export async function dispatchMock(req, response) {
|
|
|
45
46
|
sendNotFound(response)
|
|
46
47
|
else if (error.code === 'ERR_UNKNOWN_FILE_EXTENSION') {
|
|
47
48
|
if (error.toString().includes('Unknown file extension ".ts'))
|
|
48
|
-
|
|
49
|
+
log.warn('\nLooks like you need a TypeScript compiler\n')
|
|
49
50
|
sendInternalServerError(response, error)
|
|
50
51
|
}
|
|
51
52
|
else
|
package/src/Mockaton.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http'
|
|
2
2
|
|
|
3
|
+
import { log } from './utils/log.js'
|
|
3
4
|
import { API } from './ApiConstants.js'
|
|
4
5
|
import { config, setup } from './config.js'
|
|
5
6
|
import { dispatchMock } from './MockDispatcher.js'
|
|
@@ -18,7 +19,7 @@ process.on('unhandledRejection', error => { throw error })
|
|
|
18
19
|
export function Mockaton(options) {
|
|
19
20
|
const error = setup(options)
|
|
20
21
|
if (error) {
|
|
21
|
-
|
|
22
|
+
log.error(error)
|
|
22
23
|
process.exitCode = 1
|
|
23
24
|
return
|
|
24
25
|
}
|
|
@@ -32,19 +33,19 @@ export function Mockaton(options) {
|
|
|
32
33
|
|
|
33
34
|
server.listen(config.port, config.host, function (error) {
|
|
34
35
|
if (error) {
|
|
35
|
-
|
|
36
|
+
log.error(error)
|
|
36
37
|
process.exit(1)
|
|
37
38
|
return
|
|
38
39
|
}
|
|
39
40
|
const { address, port } = this.address()
|
|
40
41
|
const url = `http://${address}:${port}`
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
log.info('Listening', url)
|
|
43
|
+
log.info('Dashboard', url + API.dashboard)
|
|
43
44
|
config.onReady(url + API.dashboard)
|
|
44
45
|
})
|
|
45
46
|
|
|
46
47
|
server.on('error', error => {
|
|
47
|
-
|
|
48
|
+
log.error(error.message)
|
|
48
49
|
process.exit(1)
|
|
49
50
|
})
|
|
50
51
|
|
|
@@ -53,7 +54,7 @@ export function Mockaton(options) {
|
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
async function onRequest(req, response) {
|
|
56
|
-
response.on('error',
|
|
57
|
+
response.on('error', log.warn)
|
|
57
58
|
|
|
58
59
|
try {
|
|
59
60
|
response.setHeader('Server', 'Mockaton')
|
package/src/StaticDispatcher.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
|
|
4
|
+
import { log } from './utils/log.js'
|
|
4
5
|
import { mimeFor } from './utils/mime.js'
|
|
5
6
|
import { brokerByRoute } from './staticCollection.js'
|
|
6
7
|
import { config, calcDelay } from './config.js'
|
|
@@ -12,11 +13,12 @@ export async function dispatchStatic(req, response) {
|
|
|
12
13
|
|
|
13
14
|
setTimeout(async () => {
|
|
14
15
|
if (!broker || broker.status === 404) { // TESTME
|
|
16
|
+
log.access(req.url, 'static404')
|
|
15
17
|
sendNotFound(response)
|
|
16
18
|
return
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
log.access(req.url, 'static200')
|
|
20
22
|
|
|
21
23
|
const file = join(config.staticDir, broker.route)
|
|
22
24
|
if (req.headers.range)
|
package/src/cli.js
CHANGED
|
@@ -19,7 +19,10 @@ const args = parseArgs({
|
|
|
19
19
|
'static-dir': { short: 's', type: 'string' },
|
|
20
20
|
|
|
21
21
|
help: { short: 'h', type: 'boolean' },
|
|
22
|
-
version: { short: 'v', type: 'boolean' }
|
|
22
|
+
version: { short: 'v', type: 'boolean' },
|
|
23
|
+
|
|
24
|
+
quiet: { short: 'q', type: 'boolean' },
|
|
25
|
+
debug: { type: 'boolean' }
|
|
23
26
|
}
|
|
24
27
|
}).values
|
|
25
28
|
|
|
@@ -32,16 +35,17 @@ else if (args.help)
|
|
|
32
35
|
Usage: mockaton [options]
|
|
33
36
|
|
|
34
37
|
Options:
|
|
35
|
-
-c, --config <file>
|
|
38
|
+
-c, --config <file> (default: ./mockaton.config.js)
|
|
36
39
|
|
|
37
|
-
-m, --mocks-dir <dir>
|
|
38
|
-
-s, --static-dir <dir>
|
|
40
|
+
-m, --mocks-dir <dir> (default: ./mockaton-mocks/)
|
|
41
|
+
-s, --static-dir <dir> (default: ./mockaton-static-mocks/)
|
|
39
42
|
|
|
40
|
-
-H, --host <host>
|
|
41
|
-
-p, --port <port>
|
|
43
|
+
-H, --host <host> (default: 127.0.0.1)
|
|
44
|
+
-p, --port <port> (default: 0) which means auto-assigned
|
|
42
45
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
46
|
+
-q, --quiet Errors only
|
|
47
|
+
-h, --help Show this help
|
|
48
|
+
-v, --version Show version
|
|
45
49
|
|
|
46
50
|
Notes:
|
|
47
51
|
* mockaton.config.js supports more options, see:
|
|
@@ -64,5 +68,7 @@ else {
|
|
|
64
68
|
if (args['mocks-dir']) opts.mocksDir = args['mocks-dir']
|
|
65
69
|
if (args['static-dir']) opts.staticDir = args['static-dir']
|
|
66
70
|
|
|
71
|
+
if (args.quiet) opts.logLevel = 'quiet'
|
|
72
|
+
|
|
67
73
|
Mockaton(opts)
|
|
68
74
|
}
|
package/src/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, isAbsolute } from 'node:path'
|
|
2
2
|
|
|
3
|
+
import { log } from './utils/log.js'
|
|
3
4
|
import { isDirectory } from './utils/fs.js'
|
|
4
5
|
import { openInBrowser } from './utils/openInBrowser.js'
|
|
5
6
|
import { jsToJsonPlugin } from './MockDispatcher.js'
|
|
@@ -48,7 +49,9 @@ const schema = {
|
|
|
48
49
|
corsCredentials: [true, is(Boolean)],
|
|
49
50
|
corsMaxAge: [0, is(Number)],
|
|
50
51
|
|
|
51
|
-
onReady: [await openInBrowser, is(Function)]
|
|
52
|
+
onReady: [await openInBrowser, is(Function)],
|
|
53
|
+
|
|
54
|
+
logLevel: ['normal', val => ['normal', 'quiet'].includes(val)]
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
|
|
@@ -68,18 +71,19 @@ export const ConfigValidator = Object.freeze(validators)
|
|
|
68
71
|
|
|
69
72
|
/** @param {Partial<Config>} options */
|
|
70
73
|
export function setup(options) {
|
|
71
|
-
if (options.mocksDir && !isAbsolute(options.mocksDir))
|
|
74
|
+
if (options.mocksDir && !isAbsolute(options.mocksDir))
|
|
72
75
|
options.mocksDir = join(process.cwd(), options.mocksDir)
|
|
73
|
-
|
|
76
|
+
|
|
74
77
|
if (options.staticDir && !isAbsolute(options.staticDir))
|
|
75
78
|
options.staticDir = join(process.cwd(), options.staticDir)
|
|
76
|
-
|
|
77
|
-
if (!options.staticDir && !isDirectory(defaults.staticDir))
|
|
79
|
+
|
|
80
|
+
if (!options.staticDir && !isDirectory(defaults.staticDir))
|
|
78
81
|
options.staticDir = ''
|
|
79
82
|
|
|
80
83
|
try {
|
|
81
84
|
Object.assign(config, options)
|
|
82
85
|
validate(config, ConfigValidator)
|
|
86
|
+
log.setLevel(config.logLevel)
|
|
83
87
|
}
|
|
84
88
|
catch (err) {
|
|
85
89
|
return err.message
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { basename } from 'node:path'
|
|
2
2
|
|
|
3
|
+
import { log } from './utils/log.js'
|
|
3
4
|
import { cookie } from './cookie.js'
|
|
4
5
|
import { MockBroker } from './MockBroker.js'
|
|
5
6
|
import { listFilesRecursively } from './utils/fs.js'
|
|
6
7
|
import { config, isFileAllowed } from './config.js'
|
|
7
|
-
import { parseFilename,
|
|
8
|
+
import { parseFilename, validateFilename } from './Filename.js'
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -64,6 +65,13 @@ export function registerMock(file, isFromWatcher = false) {
|
|
|
64
65
|
return true
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function filenameIsValid(file) {
|
|
69
|
+
const error = validateFilename(file)
|
|
70
|
+
if (error)
|
|
71
|
+
log.warn(error, file)
|
|
72
|
+
return !error
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
export function unregisterMock(file) {
|
|
68
76
|
const broker = brokerByFilename(file)
|
|
69
77
|
if (!broker)
|
package/src/utils/fs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, dirname, sep, posix } from 'node:path'
|
|
2
2
|
import { lstatSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
3
|
+
import { log } from './log.js'
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
|
|
@@ -24,6 +25,6 @@ export const write = (path, body) => {
|
|
|
24
25
|
writeFileSync(path, body)
|
|
25
26
|
}
|
|
26
27
|
catch (err) {
|
|
27
|
-
|
|
28
|
+
log.warn('Write access denied', err)
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs, { readFileSync } from 'node:fs'
|
|
2
|
+
import { log } from './log.js'
|
|
2
3
|
import { mimeFor } from './mime.js'
|
|
3
4
|
import { HEADER_FOR_502 } from '../ApiConstants.js'
|
|
4
5
|
|
|
@@ -28,19 +29,19 @@ export function sendNotFound(response) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function sendUnprocessableContent(response, error) {
|
|
31
|
-
|
|
32
|
+
log.warn(error)
|
|
32
33
|
response.statusCode = 422
|
|
33
34
|
response.end(error)
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function sendInternalServerError(response, error) {
|
|
37
|
-
|
|
38
|
+
log.error(error)
|
|
38
39
|
response.statusCode = 500
|
|
39
40
|
response.end()
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function sendBadGateway(response, error) {
|
|
43
|
-
|
|
44
|
+
log.warn('Fallback Proxy Error:', error.cause.message)
|
|
44
45
|
response.statusCode = 502
|
|
45
46
|
response.setHeader(HEADER_FOR_502, 1)
|
|
46
47
|
response.end()
|
package/src/utils/log.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const log = new class {
|
|
2
|
+
#level = 'normal'
|
|
3
|
+
|
|
4
|
+
setLevel(level) {
|
|
5
|
+
this.#level = level
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
info(...msg) {
|
|
9
|
+
if (this.#level !== 'quiet')
|
|
10
|
+
console.info([this.#date, 'INFO', ...msg].join('::'))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
access(url, ...msg) {
|
|
14
|
+
if (this.#level !== 'quiet')
|
|
15
|
+
console.log([this.#date, 'ACCESS', this.#sanitizeURL(url), ...msg].join('::'))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
warn(...msg) {
|
|
19
|
+
console.warn([this.#date, 'WARN', ...msg].join('::'))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
error(...msg) {
|
|
23
|
+
console.error([this.#date, 'ERROR', ...msg].join('::'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
get #date() {
|
|
28
|
+
return new Date().toISOString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#sanitizeURL(url) {
|
|
32
|
+
return decodeURIComponent(url).replace(/[\x00-\x1F\x7F\x9B]/g, '')
|
|
33
|
+
}
|
|
34
|
+
}
|