mockaton 12.7.1 → 13.0.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.
@@ -29,29 +29,40 @@ export async function proxy(req, response, delay) {
29
29
  return
30
30
  }
31
31
 
32
- const headers = Object.fromEntries(proxyResponse.headers)
33
- headers['set-cookie'] = proxyResponse.headers.getSetCookie() // parses multiple into an array
34
- response.writeHead(proxyResponse.status, headers)
32
+ response.writeHead(proxyResponse.status, {
33
+ ...Object.fromEntries(proxyResponse.headers),
34
+ 'Set-Cookie': proxyResponse.headers.getSetCookie() // parses multiple into an array
35
+ })
35
36
  const body = await proxyResponse.text()
36
37
  setTimeout(() => response.end(body), delay) // TESTME
37
38
 
38
39
  if (config.collectProxied) {
39
40
  const ext = extFor(proxyResponse.headers.get('content-type'))
40
- let filename = makeMockFilename(req.url, req.method, proxyResponse.status, ext)
41
- if (isFile(join(config.mocksDir, filename))) // TESTME
42
- filename = makeMockFilename(req.url + `(${randomUUID()})`, req.method, proxyResponse.status, ext)
43
-
44
- let data = body
45
- if (config.formatCollectedJSON && ext === 'json') // TESTME
46
- try {
47
- data = JSON.stringify(JSON.parse(body), null, ' ')
48
- }
49
- catch {}
41
+ saveMockToDisk(req.url, req.method, proxyResponse.status, ext, body)
42
+ }
43
+ }
44
+
45
+ function saveMockToDisk(url, method, status, ext, body) {
46
+ if (config.formatCollectedJSON && ext === 'json')
50
47
  try {
51
- write(join(config.mocksDir, filename), data)
48
+ body = JSON.stringify(JSON.parse(body), null, ' ')
52
49
  }
53
50
  catch (err) {
54
- logger.warn('Write access denied', err)
51
+ logger.warn('Invalid JSON response', err)
55
52
  }
53
+
54
+ try {
55
+ write(makeUniqueMockFilename(url, method, status, ext), body)
56
56
  }
57
+ catch (err) {
58
+ logger.warn('Write access denied', err)
59
+ }
60
+ }
61
+
62
+ function makeUniqueMockFilename(url, method, status, ext) {
63
+ let file = makeMockFilename(url, method, status, ext)
64
+ if (isFile(join(config.mocksDir, file)))
65
+ file = makeMockFilename(url, method, status, ext, `(${randomUUID()})`)
66
+ return join(config.mocksDir, file)
57
67
  }
68
+
@@ -5,12 +5,10 @@ import { EventEmitter } from 'node:events'
5
5
  import { config } from './config.js'
6
6
  import { isFile, isDirectory } from './utils/fs.js'
7
7
 
8
- import * as staticCollection from './staticCollection.js'
9
8
  import * as mockBrokerCollection from './mockBrokersCollection.js'
10
9
 
11
10
 
12
11
  let mocksWatcher = null
13
- let staticWatcher = null
14
12
 
15
13
 
16
14
  /**
@@ -67,33 +65,6 @@ export function watchMocksDir() {
67
65
  }
68
66
 
69
67
 
70
- export function watchStaticDir() {
71
- const dir = config.staticDir
72
- if (!dir)
73
- return
74
-
75
- staticWatcher = staticWatcher || watch(dir, { recursive: true, persistent: false }, (_, file) => {
76
- if (!file)
77
- return
78
-
79
- if (isDirectory(join(dir, file))) {
80
- staticCollection.init()
81
- uiSyncVersion.increment()
82
- }
83
- else if (!isFile(join(dir, file))) { // file deleted
84
- staticCollection.unregisterMock(file)
85
- uiSyncVersion.increment()
86
- }
87
- else if (staticCollection.registerMock(file))
88
- uiSyncVersion.increment()
89
- else {
90
- // ignore file edits
91
- }
92
- })
93
- }
94
-
95
-
96
-
97
68
  /** Realtime notify ARR Events */
98
69
  export function sseClientSyncVersion(req, response) {
99
70
  response.writeHead(200, {
@@ -113,7 +84,7 @@ export function sseClientSyncVersion(req, response) {
113
84
  const keepAlive = setInterval(() => {
114
85
  response.write(': ping\n\n')
115
86
  }, 10_000)
116
-
87
+
117
88
  req.on('close', cleanup)
118
89
  req.on('error', cleanup)
119
90
  function cleanup() {
@@ -125,12 +96,9 @@ export function sseClientSyncVersion(req, response) {
125
96
 
126
97
  export function startWatchers() {
127
98
  watchMocksDir()
128
- watchStaticDir()
129
99
  }
130
100
 
131
101
  export function stopWatchers() {
132
102
  mocksWatcher?.close()
133
- staticWatcher?.close()
134
103
  mocksWatcher = null
135
- staticWatcher = null
136
104
  }
package/src/server/cli.js CHANGED
@@ -11,25 +11,25 @@ import pkgJSON from '../../package.json' with { type: 'json' }
11
11
 
12
12
  process.on('unhandledRejection', error => { throw error })
13
13
 
14
- let args
14
+ let args, positionals
15
15
  try {
16
- args = parseArgs({
16
+ const result = parseArgs({
17
17
  options: {
18
18
  config: { short: 'c', type: 'string' },
19
19
 
20
20
  port: { short: 'p', type: 'string' },
21
21
  host: { short: 'H', type: 'string' },
22
22
 
23
- 'mocks-dir': { short: 'm', type: 'string' },
24
- 'static-dir': { short: 's', type: 'string' },
25
-
26
23
  quiet: { short: 'q', type: 'boolean' },
27
24
  'no-open': { short: 'n', type: 'boolean' },
28
25
 
29
26
  help: { short: 'h', type: 'boolean' },
30
27
  version: { short: 'v', type: 'boolean' }
31
- }
32
- }).values
28
+ },
29
+ allowPositionals: true
30
+ })
31
+ args = result.values
32
+ positionals = result.positionals
33
33
  }
34
34
  catch (error) {
35
35
  console.error(error.message)
@@ -45,14 +45,11 @@ if (args.version)
45
45
 
46
46
  else if (args.help)
47
47
  console.log(`
48
- Usage: mockaton [options]
48
+ Usage: mockaton [mocks-dir] [options]
49
49
 
50
50
  Options:
51
51
  -c, --config <file> (default: ./mockaton.config.js)
52
52
 
53
- -m, --mocks-dir <dir> (default: ./mockaton-mocks/)
54
- -s, --static-dir <dir> (default: ./mockaton-static-mocks/)
55
-
56
53
  -H, --host <host> (default: 127.0.0.1)
57
54
  -p, --port <port> (default: 0) which means auto-assigned
58
55
 
@@ -80,8 +77,7 @@ else {
80
77
  if (args.host) opts.host = args.host
81
78
  if (args.port) opts.port = Number.isNaN(Number(args.port)) ? args.port : Number(args.port)
82
79
 
83
- if (args['mocks-dir']) opts.mocksDir = args['mocks-dir']
84
- if (args['static-dir']) opts.staticDir = args['static-dir']
80
+ if (positionals[0]) opts.mocksDir = positionals[0]
85
81
 
86
82
  if (args.quiet) opts.logLevel = 'quiet'
87
83
  if (args['no-open']) opts.onReady = () => {}
@@ -12,7 +12,7 @@ const cli = (...args) => spawnSync(rel('cli.js'), args, { encoding: 'utf8' })
12
12
  describe('CLI', () => {
13
13
  test('invalid flag', () => {
14
14
  const { stderr, status } = cli('--invalid-flag')
15
- equal(stderr.trim(), `Unknown option '--invalid-flag'`)
15
+ equal(stderr.startsWith(`Unknown option '--invalid-flag'`), true)
16
16
  equal(status, 1)
17
17
  })
18
18
 
@@ -24,9 +24,8 @@ describe('CLI', () => {
24
24
 
25
25
  test('invalid port', () => {
26
26
  const { stderr, status } = cli(
27
- '--mocks-dir', rel('../../mockaton-mocks'),
28
- '--port', 'not-a-number',
29
- )
27
+ rel('../../mockaton-mocks'),
28
+ '--port', 'not-a-number')
30
29
  equal(stderr.trim(), `port="not-a-number" is invalid`)
31
30
  equal(status, 1)
32
31
  })
@@ -39,7 +38,7 @@ describe('CLI', () => {
39
38
 
40
39
  test('-h outputs usage message', () => {
41
40
  const { stdout, status } = cli('-h')
42
- equal(stdout.split('\n')[0], 'Usage: mockaton [options]')
41
+ equal(stdout.split('\n')[0], 'Usage: mockaton [mocks-dir] [options]')
43
42
  equal(status, 0)
44
43
  })
45
44
 
@@ -7,7 +7,6 @@ import { openInBrowser } from './utils/openInBrowser.js'
7
7
  import { optional, is, validate } from './utils/validate.js'
8
8
  import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
9
9
 
10
-
11
10
  import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
12
11
 
13
12
 
@@ -19,7 +18,6 @@ import { jsToJsonPlugin } from './MockDispatcherPlugins.js'
19
18
  * }} */
20
19
  const schema = {
21
20
  mocksDir: [resolve('mockaton-mocks'), isDirectory],
22
- staticDir: [resolve('mockaton-static-mocks'), optional(isDirectory)],
23
21
  ignore: [/(\.DS_Store|~)$/, is(RegExp)],
24
22
  watcherEnabled: [true, is(Boolean)],
25
23
  watcherDebounceMs: [80, ms => Number.isInteger(ms) && ms >= 0],
@@ -78,11 +76,6 @@ export function setup(opts) {
78
76
  if (opts.mocksDir)
79
77
  opts.mocksDir = resolve(opts.mocksDir)
80
78
 
81
- if (opts.staticDir)
82
- opts.staticDir = resolve(opts.staticDir)
83
- else if (!isDirectory(defaults.staticDir))
84
- opts.staticDir = ''
85
-
86
79
  Object.assign(config, opts)
87
80
  validate(config, ConfigValidator)
88
81
  logger.setLevel(config.logLevel)
@@ -1,12 +1,10 @@
1
1
  import { basename } from 'node:path'
2
2
 
3
- import { logger } from './utils/logger.js'
4
- import { listFilesRecursively } from './utils/fs.js'
5
-
6
3
  import { cookie } from './cookie.js'
7
4
  import { MockBroker } from './MockBroker.js'
5
+ import { parseFilename } from '../client/Filename.js'
6
+ import { listFilesRecursively } from './utils/fs.js'
8
7
  import { config, isFileAllowed } from './config.js'
9
- import { parseFilename, validateFilename } from '../client/Filename.js'
10
8
 
11
9
 
12
10
  /**
@@ -42,8 +40,7 @@ export function init() {
42
40
  /** @returns {boolean} registered */
43
41
  export function registerMock(file, isFromWatcher = false) {
44
42
  if (brokerByFilename(file)?.hasMock(file)
45
- || !isFileAllowed(basename(file))
46
- || !filenameIsValid(file))
43
+ || !isFileAllowed(basename(file)))
47
44
  return false
48
45
 
49
46
  const { method, urlMask } = parseFilename(file)
@@ -62,13 +59,6 @@ export function registerMock(file, isFromWatcher = false) {
62
59
  return true
63
60
  }
64
61
 
65
- function filenameIsValid(file) {
66
- const error = validateFilename(file)
67
- if (error)
68
- logger.warn(error, file)
69
- return !error
70
- }
71
-
72
62
  export function unregisterMock(file) {
73
63
  const broker = brokerByFilename(file)
74
64
  const hasNoMoreMocks = broker?.unregister(file)
@@ -99,6 +89,14 @@ export function brokerByRoute(method, url) {
99
89
  for (let i = brokers.length - 1; i >= 0; i--)
100
90
  if (brokers[i].urlMaskMatches(url))
101
91
  return brokers[i]
92
+
93
+ // TODO Verify
94
+ if (method === 'GET') {
95
+ const indexUrl = url.endsWith('/') ? url + 'index.html' : url + '/index.html'
96
+ for (let i = brokers.length - 1; i >= 0; i--)
97
+ if (brokers[i].urlMaskMatches(indexUrl))
98
+ return brokers[i]
99
+ }
102
100
  }
103
101
 
104
102
  export function extractAllComments() {
@@ -61,7 +61,6 @@ const extToMime = {
61
61
  jsonld: 'application/ld+json',
62
62
  lz: 'application/x-lzip',
63
63
  m4a: 'audio/mp4',
64
- map: 'application/json',
65
64
  md: 'text/markdown',
66
65
  mid: 'audio/midi',
67
66
  midi: 'audio/midi',
@@ -1,36 +0,0 @@
1
- import { join } from 'node:path'
2
- import { readFileSync } from 'node:fs'
3
-
4
- import { isFile } from './utils/fs.js'
5
- import { logger } from './utils/logger.js'
6
- import { mimeFor } from './utils/mime.js'
7
-
8
- import { brokerByRoute } from './staticCollection.js'
9
- import { config, calcDelay } from './config.js'
10
-
11
-
12
- // TODO HEAD
13
- export async function dispatchStatic(req, response) {
14
- const broker = brokerByRoute(req.url)
15
-
16
- setTimeout(async () => {
17
- if (!broker || broker.status === 404) {
18
- response.mockNotFound()
19
- return
20
- }
21
-
22
- const file = join(config.staticDir, broker.route)
23
- if (!isFile(file)) {
24
- response.mockNotFound()
25
- return
26
- }
27
-
28
- logger.accessMock(req.url, 'static200')
29
- if (req.headers.range)
30
- await response.partialContent(req.headers.range, file)
31
- else {
32
- response.setHeader('Content-Type', mimeFor(file))
33
- response.end(readFileSync(file))
34
- }
35
- }, Number(broker.delayed && calcDelay()))
36
- }
@@ -1,56 +0,0 @@
1
- import { join, basename } from 'node:path'
2
- import { listFilesRecursively } from './utils/fs.js'
3
- import { config, isFileAllowed } from './config.js'
4
-
5
-
6
- class StaticBroker {
7
- constructor(route) {
8
- this.route = route
9
- this.delayed = false
10
- this.status = 200 // 200 or 404
11
- }
12
-
13
- setDelayed(value) { this.delayed = value }
14
- setStatus(value) { this.status = value }
15
- }
16
-
17
- /** @type {{ [route:string]: StaticBroker }} */
18
- let collection = {}
19
-
20
- export const all = () => collection
21
-
22
- export function init() {
23
- collection = {}
24
- listFilesRecursively(config.staticDir)
25
- .sort()
26
- .forEach(registerMock)
27
- }
28
-
29
-
30
- /** @returns {boolean} registered */
31
- export function registerMock(relativeFile) {
32
- if (!isFileAllowed(basename(relativeFile)))
33
- return false
34
-
35
- const route = '/' + relativeFile
36
- if (brokerByRoute(route))
37
- return false
38
-
39
- collection[route] = new StaticBroker(route)
40
- return true
41
- }
42
-
43
-
44
- export function unregisterMock(relativeFile) {
45
- delete collection['/' + relativeFile]
46
- }
47
-
48
-
49
- /** @returns {StaticBroker | undefined} */
50
- export function brokerByRoute(route) {
51
- return collection[route] || collection[join(route, 'index.html')]
52
- }
53
-
54
-
55
-
56
-