mockaton 13.11.1 → 13.11.2

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/src/client/app.js CHANGED
@@ -3,14 +3,12 @@ import { createElement as r, t, restoreFocus, Fragment } from './utils/dom.js'
3
3
  import { store } from './app-store.js'
4
4
  import { API } from './ApiConstants.js'
5
5
  import { Header } from './app-header.js'
6
+ import { adoptSheet } from './utils/css.js'
6
7
  import { PayloadViewer, previewMock } from './app-payload-viewer.js'
7
- import { TimerIcon, CloudIcon, ChevronDownIcon } from './graphics.js'
8
+ import { MockList, initKeyboardNavigation, renderRow } from './app-mock-list.js'
8
9
 
9
10
  import CSS from './app.css' with { type: 'css' }
10
- import { extractClassNames, classNames } from './utils/css.js'
11
- document.adoptedStyleSheets.push(CSS)
12
- Object.assign(CSS, extractClassNames(CSS))
13
-
11
+ adoptSheet(CSS, './app.css')
14
12
 
15
13
  store.onError = onError
16
14
  store.render = render
@@ -19,22 +17,22 @@ store.renderRow = renderRow
19
17
  onRealTimeUpdate(store.fetchState)
20
18
  initKeyboardNavigation()
21
19
 
22
- let mounted = false
23
20
  function render() {
24
21
  restoreFocus(() => document.body.replaceChildren(App()))
25
22
  if (store.hasChosenLink) previewMock()
26
- if (!mounted) LeftSide.$('a')?.focus()
27
- mounted = true
23
+ if (!store.mounted) LeftSide.$('a')?.focus()
24
+ store.mounted = true
28
25
  }
29
26
 
30
27
  function App() {
31
- return Fragment(
32
- Header(),
33
- r('main', null,
34
- LeftSide(),
35
- r('div', { className: CSS.rightSide },
36
- Resizer(LeftSide.ref),
37
- PayloadViewer())))
28
+ return (
29
+ Fragment(
30
+ Header(),
31
+ r('main', null,
32
+ LeftSide(),
33
+ r('div', { className: CSS.rightSide },
34
+ Resizer(),
35
+ PayloadViewer()))))
38
36
  }
39
37
 
40
38
  function LeftSide() {
@@ -44,305 +42,20 @@ function LeftSide() {
44
42
  style: { width: LeftSide.ref.width },
45
43
  className: CSS.leftSide
46
44
  },
47
- r('div', { className: CSS.SubToolbar },
48
- GroupByMethod(),
49
- BulkSelector()),
50
- r('div', { className: CSS.Table }, MockList())))
45
+ MockList()))
51
46
  }
52
47
  LeftSide.ref = { width: undefined }
53
48
  LeftSide.$ = selector => LeftSide.ref.elem.querySelector(selector)
54
- LeftSide.$$ = selector => LeftSide.ref.elem.querySelectorAll(selector)
55
-
56
-
57
- function GroupByMethod() {
58
- return (
59
- r('label', { className: CSS.GroupByMethod },
60
- r('input', {
61
- type: 'checkbox',
62
- checked: store.groupByMethod,
63
- onChange: store.toggleGroupByMethod
64
- }),
65
- r('span', { className: CSS.checkboxBody }, t`Group by Method`)))
66
- }
67
-
68
-
69
- function BulkSelector() {
70
- const { comments } = store
71
- const firstOption = t`Pick Comment…`
72
- function onChange() {
73
- const value = this.value
74
- this.value = firstOption // hack so it’s always selected
75
- store.bulkSelectByComment(value)
76
- }
77
- const disabled = !comments.length
78
- return (
79
- r('label', { className: CSS.BulkSelector },
80
- r('span', null, t`Bulk Select`),
81
- r('select', {
82
- disabled,
83
- autocomplete: 'off',
84
- title: disabled
85
- ? t`No mock files have comments which are anything within parentheses on the filename.`
86
- : undefined,
87
- onChange
88
- },
89
- r('option', { value: firstOption }, firstOption),
90
- r('hr'),
91
- comments.map(value => r('option', { value }, value)))))
92
- }
93
-
94
-
95
- function MockList() {
96
- if (!Object.keys(store.brokersByMethod).length)
97
- return r('div', null, t`No mocks found`)
98
-
99
- if (store.groupByMethod)
100
- return Object.keys(store.brokersByMethod).map(method => Fragment(
101
- r('div', {
102
- className: classNames(CSS.TableHeading, store.canProxy && CSS.canProxy)
103
- }, method),
104
- FolderGroups(store.folderGroupsByMethod(method))))
105
-
106
- return FolderGroups(store.folderGroupsByMethod('*'))
107
- }
108
-
109
- function FolderGroups(brokersTree) {
110
- const res = []
111
- for (const b of brokersTree) {
112
- if (!b.children.length)
113
- res.push(Row(b))
114
- else
115
- res.push(FolderGroup(b))
116
- }
117
- return res
118
- }
119
-
120
- function FolderGroup(broker) {
121
- const folder = broker.urlMask
122
- const children = broker.children
123
- return (
124
- r('details', {
125
- className: CSS.FolderGroup,
126
- open: !store.collapsedFolders.has(folder),
127
- onToggle() {
128
- store.setFolderCollapsed(folder, !this.open)
129
- }
130
- },
131
- r('summary', null,
132
- r('span', { className: CSS.FolderChevron }, ChevronDownIcon()),
133
- r('span', {
134
- className: classNames(
135
- CSS.FolderName,
136
- store.groupByMethod && CSS.groupedByMethod,
137
- store.canProxy && CSS.canProxy)
138
- },
139
- folder + '…')),
140
- Row(broker),
141
- children.map(c => c.children.length
142
- ? FolderGroup(c)
143
- : Row(c))))
144
- }
145
-
146
- /** @param {BrokerRowModel} row */
147
- function Row(row) {
148
- const { method, urlMask } = row
149
- return (
150
- r('div', {
151
- key: row.key,
152
- className: classNames(CSS.TableRow, mounted && row.isNew && CSS.animIn)
153
- },
154
-
155
- store.canProxy && ClickDragToggler({
156
- className: CSS.ProxyToggler,
157
- title: t`Proxy Toggler`,
158
- body: CloudIcon(),
159
- checked: row.proxied,
160
- commit(checked) {
161
- store.setProxied(method, urlMask, checked)
162
- }
163
- }),
164
-
165
- ClickDragToggler({
166
- className: CSS.DelayToggler,
167
- title: t`Delay`,
168
- body: TimerIcon(),
169
- checked: row.delayed,
170
- commit(checked) {
171
- store.setDelayed(method, urlMask, checked)
172
- }
173
- }),
174
-
175
- ClickDragToggler(
176
- row.isStatic
177
- ? {
178
- className: CSS.StatusCodeToggler,
179
- title: t`Not Found`,
180
- body: t`404`,
181
- disabled: row.opts.length === 1 && row.status === 404,
182
- checked: !row.proxied && row.status === 404,
183
- commit() { store.toggleStatus(method, urlMask, 404) }
184
- }
185
- : {
186
- className: CSS.StatusCodeToggler,
187
- title: t`Internal Server Error`,
188
- body: t`500`,
189
- disabled: row.opts.length === 1 && row.status === 500,
190
- checked: !row.proxied && row.status === 500,
191
- commit() { store.toggleStatus(method, urlMask, 500) }
192
- }),
193
-
194
- !store.groupByMethod && r('span', { className: CSS.Method }, method),
195
-
196
- PreviewLink(method, urlMask, row.urlMaskDittoed),
197
-
198
- MockSelector(row)))
199
- }
200
-
201
-
202
- function renderRow(method, urlMask) {
203
- unChooseOld()
204
- const row = store.brokerAsRow(method, urlMask)
205
- const tr = LeftSide.$(`.${CSS.TableRow}[key="${row.key}"]`)
206
- mergeTableRow(tr, Row(row))
207
- previewMock()
208
-
209
- function unChooseOld() {
210
- return LeftSide.$(`a.${CSS.chosen}`)
211
- ?.classList.remove(CSS.chosen)
212
- }
213
-
214
- function mergeTableRow(oldRow, newRow) {
215
- for (let i = 0; i < newRow.children.length; i++) {
216
- const oldEl = oldRow.children[i]
217
- const newEl = newRow.children[i]
218
- switch (newEl.tagName) {
219
- case 'LABEL': {
220
- const oldInput = oldEl.querySelector('[type="checkbox"]')
221
- const newInput = newEl.querySelector('[type="checkbox"]')
222
- oldInput.checked = newInput.checked
223
- oldInput.disabled = newInput.disabled
224
- break
225
- }
226
- case 'A':
227
- oldEl.className = newEl.className
228
- break
229
- case 'SELECT':
230
- oldEl.replaceChildren(...newEl.cloneNode(true).children)
231
- oldEl.className = newEl.className
232
- oldEl.disabled = newEl.disabled
233
- oldEl.value = newEl.value
234
- break
235
- }
236
- }
237
- }
238
- }
239
49
 
240
50
 
241
- function PreviewLink(method, urlMask, urlMaskDittoed) {
242
- function onClick(event) {
243
- event.preventDefault()
244
- store.previewLink(method, urlMask)
245
- }
246
- const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
247
- const [ditto, tail] = urlMaskDittoed
248
- return (
249
- r('a', {
250
- className: classNames(CSS.PreviewLink, isChosen && CSS.chosen),
251
- href: urlMask,
252
- onClick
253
- }, ditto
254
- ? [r('span', { className: CSS.dittoDir }, ditto), tail]
255
- : tail))
256
- }
257
-
258
-
259
- /** @param {BrokerRowModel} row */
260
- function MockSelector(row) {
261
- return (
262
- r('select', {
263
- onChange() {
264
- store.selectFile(this.value)
265
- },
266
- onKeyDown(event) {
267
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft')
268
- event.preventDefault()
269
- // Because in Firefox they change the select.option, and
270
- // we use those keys for spreadsheet-like navigation.
271
- },
272
- 'aria-label': t`Mock Selector`,
273
- disabled: row.opts.length < 2,
274
- className: classNames(
275
- CSS.MockSelector,
276
- row.selectedIdx > 0 && CSS.nonDefault,
277
- row.selectedFileIs4xx && CSS.status4xx)
278
- }, row.opts.map(([value, label, selected]) =>
279
- r('option', { value, selected }, label))))
280
- }
281
-
282
-
283
- function ClickDragToggler({ checked, commit, className, title, body }) {
284
- function onPointerEnter(event) {
285
- if (event.buttons === 1)
286
- onPointerDown.call(this, event)
287
- }
288
-
289
- function onPointerDown(event) {
290
- if (event.altKey) {
291
- onExclusiveClick.call(this)
292
- return
293
- }
294
- this.checked = !this.checked
295
- this.focus()
296
- commit(this.checked)
297
- }
298
-
299
- function onExclusiveClick() {
300
- const selector = selectorForColumnOf(this)
301
- if (!selector)
302
- return
303
-
304
- // Uncheck all other in the column.
305
- for (const elem of LeftSide.$$(selector))
306
- if (elem !== this && elem.checked && !elem.disabled) {
307
- elem.checked = false
308
- elem.dispatchEvent(new Event('change'))
309
- }
310
-
311
- if (!this.checked) {
312
- this.checked = true
313
- this.dispatchEvent(new Event('change'))
314
- }
315
- this.focus()
316
- }
317
-
318
- function onClick(event) {
319
- if (event.pointerType === 'mouse')
320
- event.preventDefault()
321
- }
322
- function onChange() {
323
- commit(this.checked)
324
- }
325
- return (
326
- r('label', { className: classNames(CSS.Toggler, className), title },
327
- r('input', {
328
- type: 'checkbox',
329
- checked,
330
- onPointerEnter,
331
- onPointerDown,
332
- onClick,
333
- onChange
334
- }),
335
- r('span', { className: CSS.checkboxBody }, body)))
336
- }
337
-
338
- function Resizer(ref) {
51
+ function Resizer() {
339
52
  let raf = 0
340
53
  let initialX = 0
341
54
  let initialWidth = 0
342
55
 
343
56
  function onPointerDown(event) {
344
57
  initialX = event.clientX
345
- initialWidth = ref.elem.clientWidth
58
+ initialWidth = LeftSide.ref.elem.clientWidth
346
59
  addEventListener('pointerup', onUp, { once: true })
347
60
  addEventListener('pointermove', onMove)
348
61
  Object.assign(document.body.style, {
@@ -355,8 +68,8 @@ function Resizer(ref) {
355
68
  function onMove(event) {
356
69
  const MIN_LEFT_WIDTH = 350
357
70
  raf = raf || requestAnimationFrame(() => {
358
- ref.width = Math.max(initialWidth - (initialX - event.clientX), MIN_LEFT_WIDTH) + 'px'
359
- ref.elem.style.width = ref.width
71
+ LeftSide.ref.width = Math.max(initialWidth - (initialX - event.clientX), MIN_LEFT_WIDTH) + 'px'
72
+ LeftSide.ref.elem.style.width = LeftSide.ref.width
360
73
  raf = 0
361
74
  })
362
75
  }
@@ -468,58 +181,3 @@ function onRealTimeUpdate(onUpdate) {
468
181
  conn = null
469
182
  }
470
183
  }
471
-
472
-
473
-
474
- function selectorForColumnOf(elem) {
475
- return columnSelectors().find(s => elem?.matches(s))
476
- }
477
-
478
- function columnSelectors() {
479
- return [
480
- `.${CSS.TableRow} .${CSS.ProxyToggler} input`,
481
- `.${CSS.TableRow} .${CSS.DelayToggler} input`,
482
- `.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
483
- `.${CSS.TableRow} .${CSS.PreviewLink}`,
484
- // No .MockSelector because down/up arrows have native behavior on them
485
- ]
486
- }
487
-
488
- function initKeyboardNavigation() {
489
- const rowSelectors = [
490
- ...columnSelectors(),
491
- `.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
492
- ]
493
-
494
- addEventListener('keydown', function ({ key }) {
495
- switch (key) {
496
- case 'ArrowDown':
497
- case 'ArrowUp': {
498
- const pivot = document.activeElement
499
- const sel = selectorForColumnOf(pivot)
500
- if (sel) {
501
- const offset = key === 'ArrowDown' ? +1 : -1
502
- const siblings = LeftSide.$$(sel)
503
- circularAdjacent(offset, siblings, pivot).focus()
504
- }
505
- break
506
- }
507
- case 'ArrowRight':
508
- case 'ArrowLeft': {
509
- const pivot = document.activeElement
510
- const sel = rowSelectors.find(s => pivot?.matches(s))
511
- if (sel) {
512
- const offset = key === 'ArrowRight' ? +1 : -1
513
- const siblings = pivot.closest(`.${CSS.TableRow}`).querySelectorAll(rowSelectors.join(','))
514
- circularAdjacent(offset, siblings, pivot).focus()
515
- }
516
- break
517
- }
518
- }
519
- })
520
-
521
- function circularAdjacent(step, siblings, pivot) {
522
- const arr = Array.from(siblings)
523
- return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
524
- }
525
- }
@@ -3,6 +3,13 @@ export function classNames(...args) {
3
3
  }
4
4
 
5
5
 
6
+ export function adoptSheet(sheet, url) {
7
+ sheet.__url = url.replace(/^\.\//, '')
8
+ document.adoptedStyleSheets.push(sheet)
9
+ Object.assign(sheet, extractClassNames(sheet))
10
+ }
11
+
12
+
6
13
  export function extractClassNames({ cssRules }) {
7
14
  // Class names must begin with _ or a letter, then it can have numbers and hyphens
8
15
  // TODO think about tag.className selectors
@@ -41,6 +41,9 @@ function init() {
41
41
 
42
42
  async function hotReloadCSS(file) {
43
43
  const mod = await import(`${document.baseURI}${file}?${Date.now()}`, { with: { type: 'css' } })
44
- document.adoptedStyleSheets = [mod.default]
44
+ mod.default.__url = file
45
+ for (let i = 0; i < document.adoptedStyleSheets.length; i++)
46
+ if (document.adoptedStyleSheets[i].__url === file)
47
+ document.adoptedStyleSheets[i] = mod.default
45
48
  }
46
49
  }
package/src/server/Api.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * selecting a specific mock-file for a particular route.
4
4
  */
5
5
 
6
- import { join } from 'node:path'
7
- import { write, rm, isFile, resolveIn, listFilesRecursively } from './utils/fs.js'
6
+ import { join, relative } from 'node:path'
7
+ import { write, rm, isFile, resolveIn } from './utils/fs.js'
8
8
 
9
9
  import openapi from '../../www/src/assets/openapi.json' with { type: 'json' }
10
10
  import pkgJSON from '../../package.json' with { type: 'json' }
@@ -23,14 +23,14 @@ import { removeQueryStringAndFragment } from './utils/HttpIncomingMessage.js'
23
23
 
24
24
  export const CLIENT_ASSETS = join(import.meta.dirname, '../client')
25
25
 
26
- const getReqs = new Map([
27
- [API.dashboard, serveDashboard],
26
+ const headReqs = new Map([
27
+ [API.health, (_, response) => response.ok()]
28
+ ])
28
29
 
29
- ...listFilesRecursively(CLIENT_ASSETS).map(f => [
30
- API.dashboard + '/' + f,
31
- serveDashboardAsset(f)
32
- ]),
30
+ const getReqs = new Map([
31
+ ...headReqs.entries(),
33
32
 
33
+ [API.root, serveDashboard],
34
34
  [API.state, getState],
35
35
  [API.syncVersion, sseClientSyncVersion],
36
36
 
@@ -62,20 +62,25 @@ const patchReqs = new Map([
62
62
  ])
63
63
 
64
64
  export async function handleApiRequest(req, response) {
65
- const url = removeQueryStringAndFragment(req.url)
65
+ if (!req.url.startsWith(API.root))
66
+ return false
66
67
 
67
- if ((req.method === 'GET' || req.method === 'HEAD') && url === API.health) {
68
- response.ok()
69
- return
70
- }
68
+ const url = removeQueryStringAndFragment(req.url)
71
69
 
72
70
  const handler = (
73
71
  req.method === 'GET' && getReqs.get(url) ||
72
+ req.method === 'HEAD' && headReqs.get(url) ||
74
73
  req.method === 'PATCH' && patchReqs.get(url))
75
74
  if (handler) {
76
75
  await handler(req, response)
77
76
  return true
78
77
  }
78
+
79
+ if (req.method === 'GET') { // serve static dashboard assets dir
80
+ const f = await resolveIn(CLIENT_ASSETS, relative(API.root, url))
81
+ await response.file(f)
82
+ return true
83
+ }
79
84
  }
80
85
 
81
86
 
@@ -85,11 +90,6 @@ function serveDashboard(_, response) {
85
90
  response.html(IndexHtml(config.hotReload, pkgJSON.version), CSP)
86
91
  }
87
92
 
88
- function serveDashboardAsset(f) {
89
- return async (_, response) => {
90
- await response.file(join(CLIENT_ASSETS, f))
91
- }
92
- }
93
93
 
94
94
  function getState(_, response) {
95
95
  response.json({
@@ -41,7 +41,7 @@ export function Mockaton(options) {
41
41
  server.on('error', reject)
42
42
  server.listen(config.port, config.host, () => {
43
43
  const url = `http://${server.address().address}:${server.address().port}`
44
- const dashboardUrl = url + API.dashboard
44
+ const dashboardUrl = url + API.root
45
45
  logger.info('Listening', url)
46
46
  logger.info('Dashboard', dashboardUrl)
47
47
  config.onReady(dashboardUrl)
@@ -10,8 +10,9 @@ import { describe, test, before, beforeEach, after } from 'node:test'
10
10
  import { unlink, mkdir, readFile, rename, readdir, writeFile, rm } from 'node:fs/promises'
11
11
 
12
12
  import { mimeFor } from './utils/mime.js'
13
+ import { API } from '../client/ApiConstants.js'
14
+ import { Commander } from '../client/ApiCommander.js'
13
15
  import { parseFilename } from '../client/Filename.js'
14
- import { API, Commander } from '../../index.js'
15
16
 
16
17
  import CONFIG from './Mockaton.test.config.js'
17
18
  import { config } from './config.js'
@@ -224,17 +225,17 @@ describe('CORS', () => {
224
225
 
225
226
  describe('Dashboard', () => {
226
227
  test('renders', async () => {
227
- const r = await request(API.dashboard)
228
+ const r = await request(API.root)
228
229
  match(await r.text(), new RegExp('<!DOCTYPE html>'))
229
230
  })
230
231
 
231
232
  test('query string is accepted', async () => {
232
- const r = await request(API.dashboard + '?foo=bar')
233
+ const r = await request(API.root + '?foo=bar')
233
234
  match(await r.text(), new RegExp('<!DOCTYPE html>'))
234
235
  })
235
236
 
236
237
  test('serves assets', async () => {
237
- const r = await request(API.dashboard + '/app.css')
238
+ const r = await request(API.root + '/app.css')
238
239
  match(await r.text(), new RegExp(':root {'))
239
240
  })
240
241
  })
package/src/server/cli.js CHANGED
@@ -4,16 +4,42 @@ import { pathToFileURL } from 'node:url'
4
4
  import { resolve, join } from 'node:path'
5
5
  import { parseArgs } from 'node:util'
6
6
 
7
- import { isFile } from './utils/fs.js'
7
+ import { isFile, isDirectory } from './utils/fs.js'
8
8
  import { Mockaton } from '../../index.js'
9
+ import { config } from './config.js'
9
10
  import pkgJSON from '../../package.json' with { type: 'json' }
10
11
 
11
12
 
12
- process.on('unhandledRejection', error => { throw error })
13
-
14
13
  const DEFAULT_CONFIG_FILE = 'mockaton.config.js'
15
14
  const SKILLS_PATH = join(import.meta.dirname, '../../skills/mockaton/SKILL.md')
16
15
 
16
+ const HELP = `
17
+ SYNOPSIS
18
+ mockaton [options] [mocks-dir]
19
+
20
+ OPTIONS
21
+ -c, --config <file> (default: ./${DEFAULT_CONFIG_FILE})
22
+
23
+ -H, --host <host> (default: 127.0.0.1)
24
+ -p, --port <port> (default: 0) which means auto-assigned
25
+
26
+ -q, --quiet Show errors only
27
+ --no-open Don't open dashboard in a browser
28
+ --no-read-only Allow writing and deleting mocks via API
29
+
30
+ --skills Show AI agent SKILL.md file path
31
+ -h, --help
32
+ -v, --version
33
+
34
+ NOTES
35
+ * mockaton.config.js supports more options.
36
+ * CLI options override their ${DEFAULT_CONFIG_FILE} counterparts.
37
+ * https://mockaton.com/config
38
+ `.trim()
39
+
40
+
41
+ process.on('unhandledRejection', error => { throw error })
42
+
17
43
  let args, positionals
18
44
  try {
19
45
  const result = parseArgs({
@@ -44,38 +70,12 @@ catch (error) {
44
70
  process.on('SIGUSR2', () => process.exit(0)) // For clean exit when collecting code-coverage
45
71
 
46
72
 
47
- if (args.version)
48
- console.log(pkgJSON.version)
49
-
50
- else if (args.skills)
51
- console.log(SKILLS_PATH)
52
-
53
- else if (args.help)
54
- console.log(`
55
- Usage: mockaton [mocks-dir] [options]
56
-
57
- Options:
58
- -c, --config <file> (default: ./${DEFAULT_CONFIG_FILE})
59
-
60
- -H, --host <host> (default: 127.0.0.1)
61
- -p, --port <port> (default: 0) which means auto-assigned
62
-
63
- -q, --quiet Show errors only
64
- --no-open Don't open dashboard in a browser
65
- --no-read-only Allow writing and deleting mocks via API
66
-
67
- --skills Show AI agent SKILL.md file path
68
- -h, --help
69
- -v, --version
70
-
71
- Notes:
72
- * mockaton.config.js supports more options, see: https://mockaton.com/config
73
- * CLI options override their ${DEFAULT_CONFIG_FILE}} counterparts
74
- `.trim())
75
-
73
+ if (args.version) console.log(pkgJSON.version)
74
+ else if (args.help) console.log(HELP)
75
+ else if (args.skills) console.log(SKILLS_PATH)
76
76
  else if (args.config && !isFile(args.config)) {
77
77
  console.error(`Invalid config file: ${args.config}`)
78
- process.exitCode = 1
78
+ process.exit(1)
79
79
  }
80
80
  else {
81
81
  const userConf = resolve(args.config ?? DEFAULT_CONFIG_FILE)
@@ -87,6 +87,10 @@ else {
87
87
  if (args.port) opts.port = Number.isNaN(Number(args.port)) ? args.port : Number(args.port)
88
88
 
89
89
  if (positionals[0]) opts.mocksDir = positionals[0]
90
+ else if (!opts.mocksDir && !isDirectory(config.mocksDir)) {
91
+ console.log(HELP)
92
+ process.exit(0)
93
+ }
90
94
 
91
95
  if (args.quiet) opts.logLevel = 'quiet'
92
96
  if (args['no-open']) opts.onReady = () => {}
@@ -23,9 +23,7 @@ describe('CLI', () => {
23
23
  })
24
24
 
25
25
  test('invalid port', () => {
26
- const { stderr, status } = cli(
27
- rel('../../mockaton-mocks'),
28
- '--port', 'not-a-number')
26
+ const { stderr, status } = cli(rel('../../mockaton-mocks'), '--port', 'not-a-number')
29
27
  equal(stderr.trim(), `port="not-a-number"\nExpected an integer between 0 and 65535`)
30
28
  equal(status, 1)
31
29
  })
@@ -38,7 +36,7 @@ describe('CLI', () => {
38
36
 
39
37
  test('-h outputs usage message', () => {
40
38
  const { stdout, status } = cli('-h')
41
- equal(stdout.split('\n')[0], 'Usage: mockaton [mocks-dir] [options]')
39
+ equal(stdout.split('\n')[1], ' mockaton [options] [mocks-dir]')
42
40
  equal(status, 0)
43
41
  })
44
42