mockaton 8.7.4 → 8.7.6

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 CHANGED
@@ -9,27 +9,27 @@ Mockaton is an HTTP mock server with the goal of making
9
9
  your frontend development and testing easier—and a lot more fun.
10
10
 
11
11
  With Mockaton you don’t need to write code for wiring your mocks.
12
- Instead, just place your mocks in a directory, and it will scan it
13
- for filenames that follow a convention similar to the URL paths.
12
+ Instead, just place your mocks in a directory and it will be scanned
13
+ for filenames following a convention similar to the URLs.
14
14
 
15
- For example, for this route `/api/user/1234`, the mock filename would be:
15
+ For example, for `/api/user/1234` the mock filename would be:
16
16
  ```
17
17
  my-mocks-dir/api/user/[user-id].GET.200.json
18
18
  ```
19
19
 
20
20
  ## Scrapping Mocks from you Backend
21
21
 
22
- Mockaton can fallback to your real backend on routes you don’t have mocks for. That’s
23
- done by typing your backend address in the **Fallback Backend** field. And if you
24
- check **Save Mocks**, you can collect those responses that hit your backend.
22
+ Mockaton can fallback to your real backend on routes you don’t have mocks for. For that,
23
+ type your backend address in the **Fallback Backend** field. And if you
24
+ check **Save Mocks**, it will collect those responses that hit your backend.
25
25
  Those mocks will be saved to your `config.mocksDir` following the filename convention.
26
26
 
27
27
 
28
28
  ## Multiple Mock Variants
29
29
 
30
30
  ### Adding comments in filenames
31
- Want to mock a locked-out user or an invalid login attempt? You
32
- can just add a comment to the filename in parentheses. For example:
31
+ Want to mock a locked-out user or an invalid login attempt?
32
+ Add a comment to the filename in parentheses. For example:
33
33
 
34
34
  `api/login(locked out user).POST.423.json`
35
35
 
@@ -214,8 +214,8 @@ Response Status Code, and File Extension.
214
214
  api/user.GET.200.json
215
215
  ```
216
216
 
217
- You can also use `.empty` if you don’t want the response to have a
218
- `Content-Type` header.
217
+ You can also use `.empty` or `.unknown` if you don’t
218
+ want a `Content-Type` header in the response.
219
219
 
220
220
 
221
221
  ### Dynamic Parameters
@@ -305,10 +305,10 @@ URL, the filename will have `[id]` in their place. For example,
305
305
 
306
306
  ```
307
307
  /api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
308
- my-mocks-dir/api/user/[id]/likes.GET.200.json
308
+ my-mocks-dir/api/user/[id]/likes.GET.200.json
309
309
  ```
310
310
 
311
- Your existing mocks won’t be overwritten (they don’t hit the fallback server).
311
+ Your existing mocks won’t be overwritten (because they don’t hit the fallback server).
312
312
 
313
313
  <details>
314
314
  <summary>Extension Details</summary>
@@ -464,7 +464,7 @@ If you don’t want to open a browser, pass a noop:
464
464
  config.onReady = () => {}
465
465
  ```
466
466
 
467
- Nonetheless, you can trigger any command besides opening a browser.
467
+ At any rate, you can trigger any command besides opening a browser.
468
468
 
469
469
  ---
470
470
 
@@ -507,9 +507,14 @@ await mockaton.setProxyFallback('http://example.com')
507
507
  ```
508
508
  Pass an empty string to disable it.
509
509
 
510
+ ### Set Save Proxied Mocks
511
+ ```js
512
+ await mockaton.setCollectProxied(true)
513
+ ```
514
+
510
515
  ### Reset
511
516
  Re-initialize the collection. The selected mocks, cookies, and delays go back to
512
- default, but `config.proxyFallback` and `config.corsAllowed` are not affected.
517
+ default, but the `proxyFallback`, `colledProxied`, and `corsAllowed` are not affected.
513
518
  ```js
514
519
  await mockaton.reset()
515
520
  ```
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "A deterministic server-side for developing and testing frontend clients",
4
4
  "type": "module",
5
- "version": "8.7.4",
5
+ "version": "8.7.6",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "license": "MIT",
package/src/Api.js CHANGED
@@ -10,17 +10,22 @@ import { DF, API } from './ApiConstants.js'
10
10
  import { parseJSON } from './utils/http-request.js'
11
11
  import { listFilesRecursively } from './utils/fs.js'
12
12
  import * as mockBrokersCollection from './mockBrokersCollection.js'
13
- import { sendOK, sendBadRequest, sendJSON, sendFile, sendUnprocessableContent } from './utils/http-response.js'
13
+ import { sendOK, sendBadRequest, sendJSON, sendUnprocessableContent, sendDashboardFile, sendForbidden } from './utils/http-response.js'
14
14
 
15
15
 
16
+ const dashboardAssets = [
17
+ '/ApiConstants.js',
18
+ '/Commander.js',
19
+ '/Dashboard.css',
20
+ '/Dashboard.js',
21
+ '/Filename.js',
22
+ '/mockaton-logo.svg'
23
+ ]
24
+
16
25
  export const apiGetRequests = new Map([
17
26
  [API.dashboard, serveDashboard],
18
- [API.dashboard + '/ApiConstants.js', serveDashboardAsset],
19
- [API.dashboard + '/Commander.js', serveDashboardAsset],
20
- [API.dashboard + '/Dashboard.css', serveDashboardAsset],
21
- [API.dashboard + '/Dashboard.js', serveDashboardAsset],
22
- [API.dashboard + '/Filename.js', serveDashboardAsset],
23
- [API.dashboard + '/mockaton-logo.svg', serveDashboardAsset],
27
+ ...dashboardAssets.map(f =>
28
+ [API.dashboard + f, serveDashboardAsset]),
24
29
  [API.mocks, listMockBrokers],
25
30
  [API.cookies, listCookies],
26
31
  [API.comments, listComments],
@@ -44,10 +49,14 @@ export const apiPatchRequests = new Map([
44
49
  /* === GET === */
45
50
 
46
51
  function serveDashboard(_, response) {
47
- sendFile(response, join(import.meta.dirname, 'Dashboard.html'))
52
+ sendDashboardFile(response, join(import.meta.dirname, 'Dashboard.html'))
48
53
  }
49
54
  function serveDashboardAsset(req, response) {
50
- sendFile(response, join(import.meta.dirname, req.url.replace(API.dashboard, '')))
55
+ const f = req.url.replace(API.dashboard, '')
56
+ if (dashboardAssets.includes(f))
57
+ sendDashboardFile(response, join(import.meta.dirname, f))
58
+ else
59
+ sendForbidden(response)
51
60
  }
52
61
 
53
62
  function listCookies(_, response) { sendJSON(response, cookie.list()) }
@@ -95,9 +104,11 @@ async function selectMock(req, response) {
95
104
  const file = await parseJSON(req)
96
105
  const broker = mockBrokersCollection.getBrokerByFilename(file)
97
106
  if (!broker || !broker.hasMock(file))
98
- throw `Missing Mock: ${file}`
99
- broker.updateFile(file)
100
- sendOK(response)
107
+ sendUnprocessableContent(response, `Missing Mock: ${file}`)
108
+ else {
109
+ broker.updateFile(file)
110
+ sendOK(response)
111
+ }
101
112
  }
102
113
  catch (error) {
103
114
  sendBadRequest(response, error)
package/src/Dashboard.css CHANGED
@@ -1,17 +1,18 @@
1
1
  :root {
2
2
  --boxShadow1: 0 2px 1px -1px rgba(0, 0, 0, 0.1), 0 1px 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.08);
3
+ --radius: 6px
3
4
  }
4
5
 
5
6
  @media (prefers-color-scheme: light) {
6
7
  :root {
7
8
  --color4xxBackground: #ffedd1;
8
- --colorAccent: #0078e1;
9
- --colorAccentAlt: #009c71;
9
+ --colorAccent: #0075db;
10
+ --colorAccentAlt: #008664;
10
11
  --colorBackground: #fff;
11
- --colorHeaderBackground: #f7f7f7;
12
- --colorComboBoxBackground: #f7f7f7;
13
- --colorSecondaryButtonBackground: #f5f5f5;
14
12
  --colorComboBoxHeaderBackground: #fff;
13
+ --colorComboBoxBackground: #f7f7f7;
14
+ --colorHeaderBackground: #f3f3f3;
15
+ --colorSecondaryButtonBackground: #f3f3f3;
15
16
  --colorDisabled: #444;
16
17
  --colorHover: #dfefff;
17
18
  --colorLabel: #444;
@@ -64,13 +65,21 @@ select, a, input, button, summary {
64
65
  }
65
66
  }
66
67
 
68
+ a, button, input[type=checkbox] {
69
+ cursor: pointer;
70
+
71
+ &:active {
72
+ cursor: grabbing;
73
+ }
74
+ }
75
+
67
76
  select {
68
77
  font-size: 100%;
69
78
  background: var(--colorComboBoxBackground);
70
79
  color: var(--colorText);
71
80
  cursor: pointer;
72
81
  outline: 0;
73
- border-radius: 6px;
82
+ border-radius: var(--radius);
74
83
 
75
84
  &:enabled {
76
85
  box-shadow: var(--boxShadow1);
@@ -122,7 +131,11 @@ select {
122
131
  margin-top: 4px;
123
132
  font-size: 11px;
124
133
  background: var(--colorComboBoxHeaderBackground);
125
- border-radius: 6px;
134
+ border-radius: var(--radius);
135
+ }
136
+
137
+ select:enabled:hover {
138
+ background: var(--colorHover);
126
139
  }
127
140
 
128
141
  &.FallbackBackend {
@@ -165,7 +178,6 @@ select {
165
178
  background: transparent;
166
179
  color: var(--colorRed);
167
180
  border-radius: 50px;
168
- cursor: pointer;
169
181
 
170
182
  &:hover {
171
183
  background: var(--colorRed);
@@ -234,7 +246,7 @@ select {
234
246
  display: inline-block;
235
247
  width: 280px;
236
248
  padding: 8px 6px;
237
- border-radius: 6px;
249
+ border-radius: var(--radius);
238
250
  color: var(--colorAccent);
239
251
  text-decoration: none;
240
252
 
@@ -292,8 +304,8 @@ select {
292
304
  }
293
305
 
294
306
  > svg {
295
- width: 16px;
296
- height: 16px;
307
+ width: 20px;
308
+ height: 20px;
297
309
  vertical-align: bottom;
298
310
  fill: var(--colorText);
299
311
  border-radius: 50%;
@@ -329,7 +341,7 @@ select {
329
341
  }
330
342
 
331
343
  > span {
332
- padding: 4px;
344
+ padding: 5px 4px;
333
345
  box-shadow: var(--boxShadow1);
334
346
  font-size: 10px;
335
347
  color: var(--colorText);
@@ -374,13 +386,10 @@ select {
374
386
  }
375
387
 
376
388
  .StaticFilesList {
377
- margin-top: 40px;
389
+ margin-top: 20px;
378
390
 
379
- summary {
380
- width: max-content;
391
+ h2 {
381
392
  margin-bottom: 8px;
382
- cursor: pointer;
383
- font-weight: bold;
384
393
  }
385
394
 
386
395
  ul {
@@ -395,7 +404,7 @@ select {
395
404
  a {
396
405
  display: inline-block;
397
406
  padding: 6px;
398
- border-radius: 6px;
407
+ border-radius: var(--radius);
399
408
  color: var(--colorAccentAlt);
400
409
  text-decoration: none;
401
410
 
package/src/Dashboard.js CHANGED
@@ -3,6 +3,14 @@ import { Commander } from './Commander.js'
3
3
  import { DEFAULT_500_COMMENT } from './ApiConstants.js'
4
4
 
5
5
 
6
+ function syntaxHighlightJson(textBody) {
7
+ const prism = window.Prism
8
+ return prism?.highlight && prism?.languages?.json
9
+ ? prism.highlight(textBody, prism.languages.json, 'json')
10
+ : false
11
+ }
12
+
13
+
6
14
  const Strings = {
7
15
  bulk_select_by_comment: 'Bulk Select by Comment',
8
16
  bulk_select_by_comment_disabled_title: 'No mock files have comments, which are anything within parentheses on the filename.',
@@ -19,7 +27,7 @@ const Strings = {
19
27
  pick: 'Pick…',
20
28
  reset: 'Reset',
21
29
  save_proxied: 'Save Mocks',
22
- static: 'Static'
30
+ static_get: 'Static GET'
23
31
  }
24
32
 
25
33
  const CSS = {
@@ -397,12 +405,15 @@ async function updatePayloadViewer(method, urlMask, response) {
397
405
  }))
398
406
  }
399
407
  else {
400
- const prism = window.Prism
401
408
  const body = await response.text() || Strings.empty_response_body
402
- if (mime === 'application/json' && prism?.highlight && prism?.languages)
403
- payloadViewerRef.current.innerHTML = prism.highlight(body, prism.languages.json, 'json')
404
- else
405
- payloadViewerRef.current.innerText = body
409
+ if (mime === 'application/json') {
410
+ const hBody = syntaxHighlightJson(body)
411
+ if (hBody) {
412
+ payloadViewerRef.current.innerHTML = hBody
413
+ return
414
+ }
415
+ }
416
+ payloadViewerRef.current.innerText = body
406
417
  }
407
418
  }
408
419
 
@@ -428,11 +439,11 @@ function StaticFilesList({ staticFiles }) {
428
439
  if (!staticFiles.length)
429
440
  return null
430
441
  return (
431
- r('details', {
442
+ r('section', {
432
443
  open: true,
433
444
  className: CSS.StaticFilesList
434
445
  },
435
- r('summary', null, Strings.static),
446
+ r('h2', null, Strings.static_get),
436
447
  r('ul', null, staticFiles.map(f =>
437
448
  r('li', null,
438
449
  r('a', { href: f, target: '_blank' }, f))))))
package/src/Filename.js CHANGED
@@ -20,6 +20,7 @@ export function filenameIsValid(file) {
20
20
  return !error
21
21
  }
22
22
 
23
+ // TODO ThinkAbout 206 (reject, handle, or send in full?)
23
24
  function validateFilename(file) {
24
25
  const tokens = file.replace(reComments, '').split('.')
25
26
  if (tokens.length < 4)
@@ -20,7 +20,7 @@ export async function dispatchMock(req, response) {
20
20
  return
21
21
  }
22
22
 
23
- console.log(decodeURIComponent(req.url), ' → ', broker.file)
23
+ console.log('%s → %s', decodeURIComponent(req.url), broker.file)
24
24
  response.statusCode = broker.status
25
25
 
26
26
  if (cookie.getCurrent())
package/src/Mockaton.js CHANGED
@@ -12,6 +12,8 @@ import { setCorsHeaders, isPreflight } from './utils/http-cors.js'
12
12
  import { apiPatchRequests, apiGetRequests } from './Api.js'
13
13
 
14
14
 
15
+ process.on('unhandledRejection', error => { throw error })
16
+
15
17
  export function Mockaton(options) {
16
18
  setup(options)
17
19
  mockBrokerCollection.init()
@@ -21,7 +23,7 @@ export function Mockaton(options) {
21
23
  if (!file)
22
24
  return
23
25
  if (existsSync(join(config.mocksDir, file)))
24
- mockBrokerCollection.registerMock(file, 'ensureItHas500')
26
+ mockBrokerCollection.registerMock(file, 'isFromWatcher')
25
27
  else
26
28
  mockBrokerCollection.unregisterMock(file)
27
29
  })
@@ -39,6 +41,8 @@ export function Mockaton(options) {
39
41
  }
40
42
 
41
43
  async function onRequest(req, response) {
44
+ req.on('error', console.error)
45
+ response.on('error', console.error)
42
46
  response.setHeader('Server', 'Mockaton')
43
47
 
44
48
  if (config.corsAllowed)
@@ -5,7 +5,7 @@ import { createServer } from 'node:http'
5
5
  import { dirname, join } from 'node:path'
6
6
  import { randomUUID } from 'node:crypto'
7
7
  import { equal, deepEqual, match } from 'node:assert/strict'
8
- import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync } from 'node:fs'
8
+ import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs'
9
9
 
10
10
  import { config } from './config.js'
11
11
  import { mimeFor } from './utils/mime.js'
@@ -14,7 +14,7 @@ import { readBody } from './utils/http-request.js'
14
14
  import { Commander } from './Commander.js'
15
15
  import { CorsHeader } from './utils/http-cors.js'
16
16
  import { parseFilename } from './Filename.js'
17
- import { listFilesRecursively, read } from './utils/fs.js'
17
+ import { listFilesRecursively } from './utils/fs.js'
18
18
  import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
19
19
 
20
20
 
@@ -351,7 +351,7 @@ async function testRegistering() {
351
351
  fixtureForRegisteringPutA500[1]
352
352
  ])
353
353
  deepEqual(currentMock, {
354
- file: fixtureForRegisteringPutA500[1],
354
+ file: fixtureForRegisteringPutA[1],
355
355
  delay: 0
356
356
  })
357
357
  })
@@ -409,6 +409,7 @@ async function testItUpdatesRouteDelay(url, file, expectedBody) {
409
409
  await describe('url: ' + url, () => {
410
410
  it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
411
411
  it('delay', () => equal((new Date()).getTime() - now.getTime() > config.delay, true))
412
+ // TODO flaky test ^
412
413
  })
413
414
  }
414
415
 
@@ -416,7 +417,7 @@ async function testBadRequestWhenUpdatingNonExistingMockAlternative() {
416
417
  await it('There are mocks for /api/the-route but not this one', async () => {
417
418
  const missingFile = 'api/the-route(non-existing-variant).GET.200.json'
418
419
  const res = await commander.select(missingFile)
419
- equal(res.status, 400)
420
+ equal(res.status, 422)
420
421
  equal(await res.text(), `Missing Mock: ${missingFile}`)
421
422
  })
422
423
  }
@@ -499,6 +500,11 @@ export default function (req, response) {
499
500
 
500
501
  async function testStaticFileServing() {
501
502
  await describe('Static File Serving', () => {
503
+ it('404 path traversal', async () => {
504
+ const res = await request('/../../../etc/passwd')
505
+ equal(res.status, 404)
506
+ })
507
+
502
508
  it('Defaults to index.html', async () => {
503
509
  const res = await request('/')
504
510
  const body = await res.text()
@@ -549,7 +555,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
549
555
  const fallbackServer = createServer(async (req, response) => {
550
556
  response.writeHead(423, {
551
557
  'custom_header': 'my_custom_header',
552
- 'content-type': mimeFor('txt'),
558
+ 'content-type': mimeFor('.txt'),
553
559
  'set-cookie': [
554
560
  'cookieA=A',
555
561
  'cookieB=B'
@@ -573,7 +579,7 @@ async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
573
579
  equal(res.headers.get('set-cookie'), ['cookieA=A', 'cookieB=B'].join(', '))
574
580
  equal(await res.text(), reqBodyPayload)
575
581
 
576
- const savedBody = read(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'))
582
+ const savedBody = readFileSync(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'), 'utf8')
577
583
  equal(savedBody, reqBodyPayload)
578
584
 
579
585
  fallbackServer.close()
@@ -1,14 +1,22 @@
1
- import { join } from 'node:path'
1
+ import { join, resolve } from 'node:path'
2
+ import fs, { readFileSync } from 'node:fs'
3
+
2
4
  import { config } from './config.js'
5
+ import { mimeFor } from './utils/mime.js'
3
6
  import { isDirectory, isFile } from './utils/fs.js'
4
- import { sendFile, sendPartialContent, sendNotFound } from './utils/http-response.js'
7
+ import { sendNotFound, sendInternalServerError } from './utils/http-response.js'
5
8
 
6
9
 
7
10
  export function isStatic(req) {
8
- if (!config.staticDir)
11
+ if (!config.staticDir || !isWithinStaticDir(req.url))
9
12
  return false
10
13
  const f = resolvePath(req.url)
11
- return !config.ignore.test(f) && Boolean(f)
14
+ return f && !config.ignore.test(f)
15
+ }
16
+
17
+ function isWithinStaticDir(url) {
18
+ const candidate = resolve(join(config.staticDir, url))
19
+ return candidate.startsWith(config.staticDir)
12
20
  }
13
21
 
14
22
  export async function dispatchStatic(req, response) {
@@ -24,9 +32,46 @@ export async function dispatchStatic(req, response) {
24
32
  function resolvePath(url) {
25
33
  let candidate = join(config.staticDir, url)
26
34
  if (isDirectory(candidate))
27
- candidate += '/index.html'
35
+ candidate = join(candidate, 'index.html')
28
36
  if (isFile(candidate))
29
37
  return candidate
30
38
  }
31
39
 
40
+ function sendFile(response, file) {
41
+ if (!isFile(file))
42
+ sendNotFound(response)
43
+ else {
44
+ response.setHeader('Content-Type', mimeFor(file))
45
+ response.end(readFileSync(file, 'utf8'))
46
+ }
47
+ }
48
+
49
+ async function sendPartialContent(response, range, file) {
50
+ const { size } = await fs.promises.lstat(file)
51
+ let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
52
+ if (isNaN(end)) end = size - 1
53
+ if (isNaN(start)) start = size - end
54
+
55
+ if (start < 0 || start > end || start >= size || end >= size) {
56
+ response.statusCode = 416 // Range Not Satisfiable
57
+ response.setHeader('Content-Range', `bytes */${size}`)
58
+ response.end()
59
+ }
60
+ else {
61
+ response.statusCode = 206 // Partial Content
62
+ response.setHeader('Accept-Ranges', 'bytes')
63
+ response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
64
+ response.setHeader('Content-Type', mimeFor(file))
65
+ const reader = fs.createReadStream(file, { start, end })
66
+ reader.on('open', function () {
67
+ this.pipe(response)
68
+ })
69
+ reader.on('error', function (error) {
70
+ sendInternalServerError(response, error)
71
+ })
72
+ }
73
+ }
74
+
75
+
76
+
32
77
 
package/src/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { resolve } from 'node:path'
1
2
  import { isDirectory } from './utils/fs.js'
2
3
  import { openInBrowser } from './utils/openInBrowser.js'
3
4
  import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
@@ -66,6 +67,9 @@ export function setup(options) {
66
67
 
67
68
  onReady: is(Function)
68
69
  })
70
+
71
+ config.mocksDir = resolve(config.mocksDir)
72
+ config.staticDir = resolve(config.staticDir)
69
73
  }
70
74
 
71
75
 
@@ -35,7 +35,7 @@ export function init() {
35
35
  })
36
36
  }
37
37
 
38
- export function registerMock(file, shouldEnsure500) {
38
+ export function registerMock(file, isFromWatcher) {
39
39
  if (getBrokerByFilename(file)?.hasMock(file)
40
40
  || config.ignore.test(file)
41
41
  || !filenameIsValid(file))
@@ -48,8 +48,11 @@ export function registerMock(file, shouldEnsure500) {
48
48
  else
49
49
  collection[method][urlMask].register(file)
50
50
 
51
- if (shouldEnsure500)
51
+ if (isFromWatcher) {
52
+ if (!this.file)
53
+ collection[method][urlMask].selectDefaultFile()
52
54
  collection[method][urlMask].ensureItHas500()
55
+ }
53
56
  }
54
57
 
55
58
  export function unregisterMock(file) {
package/src/utils/fs.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import { join, dirname, sep, posix } from 'node:path'
2
- import { lstatSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
2
+ import { lstatSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
3
3
 
4
4
 
5
5
  export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
6
6
  export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.isDirectory()
7
7
 
8
- export const read = path => readFileSync(path, 'utf8')
9
-
10
8
  /** @returns {Array<string>} paths relative to `dir` */
11
9
  export const listFilesRecursively = dir => {
12
10
  const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
@@ -1,6 +1,5 @@
1
- import fs from 'node:fs'
1
+ import { readFileSync } from 'node:fs'
2
2
  import { mimeFor } from './mime.js'
3
- import { isFile, read } from './fs.js'
4
3
 
5
4
 
6
5
  export function sendOK(response) {
@@ -17,46 +16,20 @@ export function sendJSON(response, payload) {
17
16
  response.end(JSON.stringify(payload))
18
17
  }
19
18
 
20
- export function sendFile(response, filePath) {
21
- if (!isFile(filePath))
22
- sendNotFound(response)
23
- else {
24
- response.setHeader('Content-Type', mimeFor(filePath))
25
- response.end(read(filePath))
26
- }
19
+ export function sendForbidden(response) {
20
+ response.statusCode = 403
21
+ response.end()
27
22
  }
28
23
 
29
- export async function sendPartialContent(response, range, file) {
30
- const { size } = await fs.promises.lstat(file)
31
- let [start, end] = range.replace(/bytes=/, '').split('-').map(n => parseInt(n, 10))
32
- if (isNaN(end)) end = size - 1
33
- if (isNaN(start)) start = size - end
34
-
35
- if (start < 0 || start > end || start >= size || end >= size) {
36
- response.statusCode = 416 // Range Not Satisfiable
37
- response.setHeader('Content-Range', `bytes */${size}`)
38
- response.end()
39
- }
40
- else {
41
- response.statusCode = 206 // Partial Content
42
- response.setHeader('Accept-Ranges', 'bytes')
43
- response.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
44
- response.setHeader('Content-Type', mimeFor(file))
45
- const reader = fs.createReadStream(file, { start, end })
46
- reader.on('open', function () {
47
- this.pipe(response)
48
- })
49
- reader.on('error', function (error) {
50
- sendInternalServerError(response, error)
51
- })
52
- }
24
+ export function sendDashboardFile(response, file) {
25
+ response.setHeader('Content-Type', mimeFor(file))
26
+ response.end(readFileSync(file, 'utf8'))
53
27
  }
54
28
 
55
-
56
29
  export function sendBadRequest(response, error) {
57
30
  console.error(error)
58
31
  response.statusCode = 400
59
- response.end(error)
32
+ response.end()
60
33
  }
61
34
 
62
35
  export function sendNotFound(response) {
@@ -73,5 +46,5 @@ export function sendUnprocessableContent(response, error) {
73
46
  export function sendInternalServerError(response, error) {
74
47
  console.error(error)
75
48
  response.statusCode = 500
76
- response.end(error?.code || '')
49
+ response.end()
77
50
  }
package/src/utils/mime.js CHANGED
@@ -88,16 +88,22 @@ const mimes = {
88
88
  }
89
89
 
90
90
  export function mimeFor(filename) {
91
- const ext = filename.replace(/.*\./, '').toLowerCase()
91
+ const ext = extname(filename).toLowerCase()
92
92
  return config.extraMimes[ext] || mimes[ext] || ''
93
93
  }
94
+ function extname(filename) {
95
+ const i = filename.lastIndexOf('.')
96
+ return i >= 0
97
+ ? filename.substring(i + 1)
98
+ : ''
99
+ }
100
+
94
101
 
95
102
  export function extFor(mime) {
96
103
  return mime
97
104
  ? findExt(mime)
98
105
  : 'empty'
99
106
  }
100
-
101
107
  function findExt(targetMime) {
102
108
  for (const [ext, mime] of Object.entries(config.extraMimes))
103
109
  if (targetMime === mime)