mockaton 12.4.0 → 12.5.1

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/index.d.ts CHANGED
@@ -61,6 +61,8 @@ export const jsToJsonPlugin: Plugin
61
61
  export function jwtCookie(cookieName: string, payload: any, path?: string): string
62
62
 
63
63
  export function parseJSON(request: IncomingMessage): Promise<any>
64
+ export function parseSplats(reqUrl: string, filename: string): Record<string, string>
65
+ export function parseQueryParams(reqUrl: string): URLSearchParams
64
66
 
65
67
  export type JsonPromise<T> = Promise<Response & { json(): Promise<T> }>
66
68
 
package/index.js CHANGED
@@ -4,5 +4,6 @@ export { Mockaton } from './src/server/Mockaton.js'
4
4
  export { jwtCookie } from './src/server/utils/jwt.js'
5
5
  export { jsToJsonPlugin } from './src/server/MockDispatcher.js'
6
6
  export { parseJSON, BodyReaderError } from './src/server/utils/HttpIncomingMessage.js'
7
+ export { parseSplats, parseQueryParams } from './src/server/utils/UrlParsers.js'
7
8
 
8
9
  export const defineConfig = opts => opts
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "12.4.0",
5
+ "version": "12.5.1",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -44,13 +44,17 @@ export function parseFilename(file) {
44
44
  }
45
45
  }
46
46
 
47
- function removeTrailingSlash(url = '') {
47
+ export function removeTrailingSlash(url = '') {
48
48
  return url
49
49
  .replace(/\/$/, '')
50
50
  .replace('/?', '?')
51
51
  .replace('/#', '#')
52
52
  }
53
53
 
54
+ export function removeQueryStringAndFragment(url = '') {
55
+ return new URL(url, 'http://_').pathname
56
+ }
57
+
54
58
  function responseStatusIsValid(status) {
55
59
  return Number.isInteger(status)
56
60
  && status >= 100
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MOCK_COMMENT } from '../client/ApiConstants.js'
2
- import { parseFilename, includesComment, extractComments } from '../client/Filename.js'
2
+ import { parseFilename, includesComment, extractComments, removeQueryStringAndFragment } from '../client/Filename.js'
3
3
 
4
4
 
5
5
  /**
@@ -105,15 +105,11 @@ class UrlMatcher {
105
105
 
106
106
  #buildUrlRegex(file) {
107
107
  let { urlMask } = parseFilename(file)
108
- urlMask = this.#removeQueryStringAndFragment(urlMask)
108
+ urlMask = removeQueryStringAndFragment(urlMask)
109
109
  urlMask = this.#disregardVariables(urlMask)
110
110
  return new RegExp('^' + urlMask + '/*$')
111
111
  }
112
112
 
113
- #removeQueryStringAndFragment(str) {
114
- return str.replace(/[?#].*/, '')
115
- }
116
-
117
113
  #disregardVariables(str) { // Stars out all parts that are in square brackets
118
114
  return str.replace(/\[.*?]/g, '[^/]+')
119
115
  }
@@ -127,7 +123,7 @@ class UrlMatcher {
127
123
  // slashes. For instance, for routing api/foo/[id]?qs…
128
124
  urlMaskMatches = (url) => {
129
125
  let u = decodeURIComponent(url)
130
- u = this.#removeQueryStringAndFragment(u)
126
+ u = removeQueryStringAndFragment(u)
131
127
  u += '/'
132
128
  return this.#urlRegex.test(u)
133
129
  }
@@ -6,7 +6,7 @@ import { write, isFile } from './utils/fs.js'
6
6
  import { readBody, BodyReaderError } from './utils/HttpIncomingMessage.js'
7
7
 
8
8
  import { config } from './config.js'
9
-
9
+ import { logger } from './utils/logger.js'
10
10
  import { makeMockFilename } from '../client/Filename.js'
11
11
 
12
12
 
@@ -47,6 +47,11 @@ export async function proxy(req, response, delay) {
47
47
  data = JSON.stringify(JSON.parse(body), null, ' ')
48
48
  }
49
49
  catch {}
50
- write(join(config.mocksDir, filename), data)
50
+ try {
51
+ write(join(config.mocksDir, filename), data)
52
+ }
53
+ catch (err) {
54
+ logger.warn('Write access denied', err)
55
+ }
51
56
  }
52
57
  }
@@ -1,9 +1,12 @@
1
+ import { resolve as r } from 'node:path'
2
+
3
+ const mockatonSrcRoot = `file://${r(import.meta.dirname, '..')}`
4
+
1
5
  // We register this hook at runtime so it doesn’t interfere with non-dynamic imports.
6
+ // Excluding src/ is only needed for DEV.
2
7
  export async function resolve(specifier, context, nextResolve) {
3
8
  const result = await nextResolve(specifier, context)
4
- if (specifier === 'debug')
5
- return result
6
- if (result.url?.startsWith('file:')) {
9
+ if (result.url?.startsWith('file://') && !result.url.startsWith(mockatonSrcRoot)) {
7
10
  const url = new URL(result.url)
8
11
  url.searchParams.set('t', performance.now())
9
12
  return {
@@ -1,10 +1,10 @@
1
1
  import { resolve } from 'node:path'
2
+ import { METHODS } from 'node:http'
2
3
 
3
4
  import { logger } from './utils/logger.js'
4
5
  import { isDirectory } from './utils/fs.js'
5
6
  import { openInBrowser } from './utils/openInBrowser.js'
6
7
  import { optional, is, validate } from './utils/validate.js'
7
- import { SUPPORTED_METHODS } from './utils/HttpIncomingMessage.js'
8
8
  import { validateCorsAllowedMethods, validateCorsAllowedOrigins } from './utils/http-cors.js'
9
9
 
10
10
  import { jsToJsonPlugin } from './MockDispatcher.js'
@@ -41,7 +41,7 @@ const schema = {
41
41
 
42
42
  corsAllowed: [true, is(Boolean)],
43
43
  corsOrigins: [['*'], validateCorsAllowedOrigins],
44
- corsMethods: [SUPPORTED_METHODS, validateCorsAllowedMethods],
44
+ corsMethods: [METHODS, validateCorsAllowedMethods],
45
45
  corsHeaders: [['content-type', 'authorization'], Array.isArray],
46
46
  corsExposedHeaders: [[], Array.isArray],
47
47
  corsCredentials: [true, is(Boolean)],
@@ -1,8 +1,7 @@
1
1
  import http, { METHODS } from 'node:http'
2
2
 
3
3
 
4
- export const SUPPORTED_METHODS = METHODS
5
- export const methodIsSupported = method => SUPPORTED_METHODS.includes(method)
4
+ export const methodIsSupported = method => METHODS.includes(method)
6
5
 
7
6
  export class BodyReaderError extends Error {
8
7
  name = 'BodyReaderError'
@@ -73,3 +72,4 @@ export function decode(url) {
73
72
  ? candidate
74
73
  : '' // reject multiple encodings
75
74
  }
75
+
@@ -0,0 +1,32 @@
1
+ import { relative } from 'node:path'
2
+ import { config } from '../config.js'
3
+ import { decode } from './HttpIncomingMessage.js'
4
+ import { parseFilename, removeTrailingSlash, removeQueryStringAndFragment } from '../../client/Filename.js'
5
+
6
+
7
+ export function parseQueryParams(url) {
8
+ return new URL(url, 'http://_').searchParams
9
+ }
10
+
11
+ export function parseSplats(url, filename) {
12
+ const { urlMask } = parseFilename(relative(config.mocksDir, filename))
13
+
14
+ const splats = []
15
+ const pattern = removeQueryStringAndFragment(decode(urlMask))
16
+ .replace(/\[(.+?)]/g, (_, name) => {
17
+ splats.push(name)
18
+ return '([^/]+)'
19
+ })
20
+
21
+ let u = removeQueryStringAndFragment(url)
22
+ u = removeTrailingSlash(u)
23
+
24
+ const match = u.match(new RegExp(`^${pattern}$`))
25
+ if (!match)
26
+ return {}
27
+
28
+ return splats.reduce((acc, name, i) => {
29
+ acc[name] = match[i + 1]
30
+ return acc
31
+ }, {})
32
+ }
@@ -0,0 +1,55 @@
1
+ import { test, describe } from 'node:test'
2
+ import { deepEqual, equal } from 'node:assert/strict'
3
+ import { parseSplats, parseQueryParams } from './UrlParsers.js'
4
+ import { config } from '../config.js'
5
+
6
+ test('parseQueryParams', () => {
7
+ const searchParams = parseQueryParams('/api/foo?limit=123')
8
+ equal(searchParams.get('limit'), '123')
9
+ })
10
+
11
+
12
+ describe('parseSplats', () => {
13
+ test('one splat', () => {
14
+ const splats = parseSplats(
15
+ '/api/company/123',
16
+ `${config.mocksDir}/api/company/[companyId].GET.200.js`
17
+ )
18
+ deepEqual(splats, {
19
+ companyId: '123'
20
+ })
21
+ })
22
+
23
+ test('one splat with trailing slash', () => {
24
+ const splats = parseSplats(
25
+ '/api/company/123/',
26
+ `${config.mocksDir}/api/company/[companyId].GET.200.js`
27
+ )
28
+ deepEqual(splats, {
29
+ companyId: '123'
30
+ })
31
+ })
32
+
33
+ test('two splats and comment', () => {
34
+ const splats = parseSplats(
35
+ '/api/company/123/user/456',
36
+ `${config.mocksDir}/api/company/[companyId]/user/[userId](comments).GET.200.js`
37
+ )
38
+ deepEqual(splats, {
39
+ companyId: '123',
40
+ userId: '456',
41
+ })
42
+ })
43
+
44
+ test('ignores query string', () => {
45
+ const splats = parseSplats(
46
+ '/api/company/123?foo=456',
47
+ `${config.mocksDir}/api/company/[companyId]?foo=[fooId].GET.200.js`
48
+ )
49
+ deepEqual(splats, {
50
+ companyId: '123'
51
+ })
52
+ })
53
+ })
54
+
55
+
@@ -1,14 +1,13 @@
1
1
  import { join, dirname, sep, posix } from 'node:path'
2
2
  import { lstatSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs'
3
3
 
4
- import { logger } from './logger.js'
5
-
6
4
 
7
5
  export const isFile = path => lstatSync(path, { throwIfNoEntry: false })?.isFile()
8
6
  export const isDirectory = path => lstatSync(path, { throwIfNoEntry: false })?.isDirectory()
9
7
 
8
+
10
9
  /** @returns {Array<string>} paths relative to `dir` */
11
- export const listFilesRecursively = dir => {
10
+ export function listFilesRecursively(dir) {
12
11
  try {
13
12
  const files = readdirSync(dir, { recursive: true }).filter(f => isFile(join(dir, f)))
14
13
  return process.platform === 'win32'
@@ -20,12 +19,7 @@ export const listFilesRecursively = dir => {
20
19
  }
21
20
  }
22
21
 
23
- export const write = (path, body) => {
24
- try {
25
- mkdirSync(dirname(path), { recursive: true })
26
- writeFileSync(path, body)
27
- }
28
- catch (err) {
29
- logger.warn('Write access denied', err)
30
- }
22
+ export function write(path, body) {
23
+ mkdirSync(dirname(path), { recursive: true })
24
+ writeFileSync(path, body)
31
25
  }