@furystack/rest-service 6.0.7 → 6.1.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.
Files changed (38) hide show
  1. package/dist/helpers.js +15 -1
  2. package/dist/helpers.js.map +1 -1
  3. package/dist/helpers.spec.js +18 -2
  4. package/dist/helpers.spec.js.map +1 -1
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/mime-types.js +339 -0
  8. package/dist/mime-types.js.map +1 -0
  9. package/dist/mime-types.spec.js +15 -0
  10. package/dist/mime-types.spec.js.map +1 -0
  11. package/dist/server-manager.js.map +1 -1
  12. package/dist/static-server-manager.js +66 -0
  13. package/dist/static-server-manager.js.map +1 -0
  14. package/dist/static-server-manager.spec.js +186 -0
  15. package/dist/static-server-manager.spec.js.map +1 -0
  16. package/package.json +11 -11
  17. package/src/helpers.spec.ts +20 -9
  18. package/src/helpers.ts +14 -0
  19. package/src/index.ts +2 -0
  20. package/src/mime-types.spec.ts +14 -0
  21. package/src/mime-types.ts +336 -0
  22. package/src/server-manager.ts +6 -1
  23. package/src/static-server-manager.spec.ts +226 -0
  24. package/src/static-server-manager.ts +89 -0
  25. package/types/helpers.d.ts +12 -0
  26. package/types/helpers.d.ts.map +1 -1
  27. package/types/index.d.ts +2 -0
  28. package/types/index.d.ts.map +1 -1
  29. package/types/mime-types.d.ts +305 -0
  30. package/types/mime-types.d.ts.map +1 -0
  31. package/types/mime-types.spec.d.ts +2 -0
  32. package/types/mime-types.spec.d.ts.map +1 -0
  33. package/types/server-manager.d.ts +5 -4
  34. package/types/server-manager.d.ts.map +1 -1
  35. package/types/static-server-manager.d.ts +20 -0
  36. package/types/static-server-manager.d.ts.map +1 -0
  37. package/types/static-server-manager.spec.d.ts +2 -0
  38. package/types/static-server-manager.spec.d.ts.map +1 -0
@@ -0,0 +1,336 @@
1
+ import path from 'path'
2
+
3
+ export const mimeTypes = {
4
+ 'application/andrew-inset': ['ez'],
5
+ 'application/applixware': ['aw'],
6
+ 'application/atom+xml': ['atom'],
7
+ 'application/atomcat+xml': ['atomcat'],
8
+ 'application/atomdeleted+xml': ['atomdeleted'],
9
+ 'application/atomsvc+xml': ['atomsvc'],
10
+ 'application/atsc-dwd+xml': ['dwd'],
11
+ 'application/atsc-held+xml': ['held'],
12
+ 'application/atsc-rsat+xml': ['rsat'],
13
+ 'application/bdoc': ['bdoc'],
14
+ 'application/calendar+xml': ['xcs'],
15
+ 'application/ccxml+xml': ['ccxml'],
16
+ 'application/cdfx+xml': ['cdfx'],
17
+ 'application/cdmi-capability': ['cdmia'],
18
+ 'application/cdmi-container': ['cdmic'],
19
+ 'application/cdmi-domain': ['cdmid'],
20
+ 'application/cdmi-object': ['cdmio'],
21
+ 'application/cdmi-queue': ['cdmiq'],
22
+ 'application/cu-seeme': ['cu'],
23
+ 'application/dash+xml': ['mpd'],
24
+ 'application/davmount+xml': ['davmount'],
25
+ 'application/docbook+xml': ['dbk'],
26
+ 'application/dssc+der': ['dssc'],
27
+ 'application/dssc+xml': ['xdssc'],
28
+ 'application/ecmascript': ['es', 'ecma'],
29
+ 'application/emma+xml': ['emma'],
30
+ 'application/emotionml+xml': ['emotionml'],
31
+ 'application/epub+zip': ['epub'],
32
+ 'application/exi': ['exi'],
33
+ 'application/express': ['exp'],
34
+ 'application/fdt+xml': ['fdt'],
35
+ 'application/font-tdpfr': ['pfr'],
36
+ 'application/geo+json': ['geojson'],
37
+ 'application/gml+xml': ['gml'],
38
+ 'application/gpx+xml': ['gpx'],
39
+ 'application/gxf': ['gxf'],
40
+ 'application/gzip': ['gz'],
41
+ 'application/hjson': ['hjson'],
42
+ 'application/hyperstudio': ['stk'],
43
+ 'application/inkml+xml': ['ink', 'inkml'],
44
+ 'application/ipfix': ['ipfix'],
45
+ 'application/its+xml': ['its'],
46
+ 'application/java-archive': ['jar', 'war', 'ear'],
47
+ 'application/java-serialized-object': ['ser'],
48
+ 'application/java-vm': ['class'],
49
+ 'application/javascript': ['js', 'mjs'],
50
+ 'application/json': ['json', 'map'],
51
+ 'application/json5': ['json5'],
52
+ 'application/jsonml+json': ['jsonml'],
53
+ 'application/ld+json': ['jsonld'],
54
+ 'application/lgr+xml': ['lgr'],
55
+ 'application/lost+xml': ['lostxml'],
56
+ 'application/mac-binhex40': ['hqx'],
57
+ 'application/mac-compactpro': ['cpt'],
58
+ 'application/mads+xml': ['mads'],
59
+ 'application/manifest+json': ['webmanifest'],
60
+ 'application/marc': ['mrc'],
61
+ 'application/marcxml+xml': ['mrcx'],
62
+ 'application/mathematica': ['ma', 'nb', 'mb'],
63
+ 'application/mathml+xml': ['mathml'],
64
+ 'application/mbox': ['mbox'],
65
+ 'application/mediaservercontrol+xml': ['mscml'],
66
+ 'application/metalink+xml': ['metalink'],
67
+ 'application/metalink4+xml': ['meta4'],
68
+ 'application/mets+xml': ['mets'],
69
+ 'application/mmt-aei+xml': ['maei'],
70
+ 'application/mmt-usd+xml': ['musd'],
71
+ 'application/mods+xml': ['mods'],
72
+ 'application/mp21': ['m21', 'mp21'],
73
+ 'application/mp4': ['mp4s', 'm4p'],
74
+ 'application/msword': ['doc', 'dot'],
75
+ 'application/mxf': ['mxf'],
76
+ 'application/n-quads': ['nq'],
77
+ 'application/n-triples': ['nt'],
78
+ 'application/node': ['cjs'],
79
+ 'application/octet-stream': [
80
+ 'bin',
81
+ 'dms',
82
+ 'lrf',
83
+ 'mar',
84
+ 'so',
85
+ 'dist',
86
+ 'distz',
87
+ 'pkg',
88
+ 'bpk',
89
+ 'dump',
90
+ 'elc',
91
+ 'deploy',
92
+ 'exe',
93
+ 'dll',
94
+ 'deb',
95
+ 'dmg',
96
+ 'iso',
97
+ 'img',
98
+ 'msi',
99
+ 'msp',
100
+ 'msm',
101
+ 'buffer',
102
+ ],
103
+ 'application/oda': ['oda'],
104
+ 'application/oebps-package+xml': ['opf'],
105
+ 'application/ogg': ['ogx'],
106
+ 'application/omdoc+xml': ['omdoc'],
107
+ 'application/onenote': ['onetoc', 'onetoc2', 'onetmp', 'onepkg'],
108
+ 'application/oxps': ['oxps'],
109
+ 'application/p2p-overlay+xml': ['relo'],
110
+ 'application/patch-ops-error+xml': ['xer'],
111
+ 'application/pdf': ['pdf'],
112
+ 'application/pgp-encrypted': ['pgp'],
113
+ 'application/pgp-signature': ['asc', 'sig'],
114
+ 'application/pics-rules': ['prf'],
115
+ 'application/pkcs10': ['p10'],
116
+ 'application/pkcs7-mime': ['p7m', 'p7c'],
117
+ 'application/pkcs7-signature': ['p7s'],
118
+ 'application/pkcs8': ['p8'],
119
+ 'application/pkix-attr-cert': ['ac'],
120
+ 'application/pkix-cert': ['cer'],
121
+ 'application/pkix-crl': ['crl'],
122
+ 'application/pkix-pkipath': ['pkipath'],
123
+ 'application/pkixcmp': ['pki'],
124
+ 'application/pls+xml': ['pls'],
125
+ 'application/postscript': ['ai', 'eps', 'ps'],
126
+ 'application/provenance+xml': ['provx'],
127
+ 'application/pskc+xml': ['pskcxml'],
128
+ 'application/raml+yaml': ['raml'],
129
+ 'application/rdf+xml': ['rdf', 'owl'],
130
+ 'application/reginfo+xml': ['rif'],
131
+ 'application/relax-ng-compact-syntax': ['rnc'],
132
+ 'application/resource-lists+xml': ['rl'],
133
+ 'application/resource-lists-diff+xml': ['rld'],
134
+ 'application/rls-services+xml': ['rs'],
135
+ 'application/route-apd+xml': ['rapd'],
136
+ 'application/route-s-tsid+xml': ['sls'],
137
+ 'application/route-usd+xml': ['rusd'],
138
+ 'application/rpki-ghostbusters': ['gbr'],
139
+ 'application/rpki-manifest': ['mft'],
140
+ 'application/rpki-roa': ['roa'],
141
+ 'application/rsd+xml': ['rsd'],
142
+ 'application/rss+xml': ['rss'],
143
+ 'application/rtf': ['rtf'],
144
+ 'application/sbml+xml': ['sbml'],
145
+ 'application/scvp-cv-request': ['scq'],
146
+ 'application/scvp-cv-response': ['scs'],
147
+ 'application/scvp-vp-request': ['spq'],
148
+ 'application/scvp-vp-response': ['spp'],
149
+ 'application/sdp': ['sdp'],
150
+ 'application/senml+xml': ['senmlx'],
151
+ 'application/sensml+xml': ['sensmlx'],
152
+ 'application/set-payment-initiation': ['setpay'],
153
+ 'application/set-registration-initiation': ['setreg'],
154
+ 'application/shf+xml': ['shf'],
155
+ 'application/sieve': ['siv', 'sieve'],
156
+ 'application/smil+xml': ['smi', 'smil'],
157
+ 'application/sparql-query': ['rq'],
158
+ 'application/sparql-results+xml': ['srx'],
159
+ 'application/srgs': ['gram'],
160
+ 'application/srgs+xml': ['grxml'],
161
+ 'application/sru+xml': ['sru'],
162
+ 'application/ssdl+xml': ['ssdl'],
163
+ 'application/ssml+xml': ['ssml'],
164
+ 'application/swid+xml': ['swidtag'],
165
+ 'application/tei+xml': ['tei', 'teicorpus'],
166
+ 'application/thraud+xml': ['tfi'],
167
+ 'application/timestamped-data': ['tsd'],
168
+ 'application/toml': ['toml'],
169
+ 'application/trig': ['trig'],
170
+ 'application/ttml+xml': ['ttml'],
171
+ 'application/ubjson': ['ubj'],
172
+ 'application/urc-ressheet+xml': ['rsheet'],
173
+ 'application/urc-targetdesc+xml': ['td'],
174
+ 'application/voicexml+xml': ['vxml'],
175
+ 'application/wasm': ['wasm'],
176
+ 'application/widget': ['wgt'],
177
+ 'application/winhlp': ['hlp'],
178
+ 'application/wsdl+xml': ['wsdl'],
179
+ 'application/wspolicy+xml': ['wspolicy'],
180
+ 'application/xaml+xml': ['xaml'],
181
+ 'application/xcap-att+xml': ['xav'],
182
+ 'application/xcap-caps+xml': ['xca'],
183
+ 'application/xcap-diff+xml': ['xdf'],
184
+ 'application/xcap-el+xml': ['xel'],
185
+ 'application/xcap-ns+xml': ['xns'],
186
+ 'application/xenc+xml': ['xenc'],
187
+ 'application/xhtml+xml': ['xhtml', 'xht'],
188
+ 'application/xliff+xml': ['xlf'],
189
+ 'application/xml': ['xml', 'xsl', 'xsd', 'rng'],
190
+ 'application/xml-dtd': ['dtd'],
191
+ 'application/xop+xml': ['xop'],
192
+ 'application/xproc+xml': ['xpl'],
193
+ 'application/xslt+xml': ['(.)+xsl', 'xslt'],
194
+ 'application/xspf+xml': ['xspf'],
195
+ 'application/xv+xml': ['mxml', 'xhvml', 'xvml', 'xvm'],
196
+ 'application/yang': ['yang'],
197
+ 'application/yin+xml': ['yin'],
198
+ 'application/zip': ['zip'],
199
+ 'audio/3gpp': ['(.)+3gpp'],
200
+ 'audio/adpcm': ['adp'],
201
+ 'audio/amr': ['amr'],
202
+ 'audio/basic': ['au', 'snd'],
203
+ 'audio/midi': ['mid', 'midi', 'kar', 'rmi'],
204
+ 'audio/mobile-xmf': ['mxmf'],
205
+ 'audio/mp3': ['(.)+mp3'],
206
+ 'audio/mp4': ['m4a', 'mp4a'],
207
+ 'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'],
208
+ 'audio/ogg': ['oga', 'ogg', 'spx', 'opus'],
209
+ 'audio/s3m': ['s3m'],
210
+ 'audio/silk': ['sil'],
211
+ 'audio/wav': ['wav'],
212
+ 'audio/wave': ['(.)+wav'],
213
+ 'audio/webm': ['weba'],
214
+ 'audio/xm': ['xm'],
215
+ 'font/collection': ['ttc'],
216
+ 'font/otf': ['otf'],
217
+ 'font/ttf': ['ttf'],
218
+ 'font/woff': ['woff'],
219
+ 'font/woff2': ['woff2'],
220
+ 'image/aces': ['exr'],
221
+ 'image/apng': ['apng'],
222
+ 'image/avif': ['avif'],
223
+ 'image/bmp': ['bmp'],
224
+ 'image/cgm': ['cgm'],
225
+ 'image/dicom-rle': ['drle'],
226
+ 'image/emf': ['emf'],
227
+ 'image/fits': ['fits'],
228
+ 'image/g3fax': ['g3'],
229
+ 'image/gif': ['gif'],
230
+ 'image/heic': ['heic'],
231
+ 'image/heic-sequence': ['heics'],
232
+ 'image/heif': ['heif'],
233
+ 'image/heif-sequence': ['heifs'],
234
+ 'image/hej2k': ['hej2'],
235
+ 'image/hsj2': ['hsj2'],
236
+ 'image/ief': ['ief'],
237
+ 'image/jls': ['jls'],
238
+ 'image/jp2': ['jp2', 'jpg2'],
239
+ 'image/jpeg': ['jpeg', 'jpg', 'jpe'],
240
+ 'image/jph': ['jph'],
241
+ 'image/jphc': ['jhc'],
242
+ 'image/jpm': ['jpm'],
243
+ 'image/jpx': ['jpx', 'jpf'],
244
+ 'image/jxr': ['jxr'],
245
+ 'image/jxra': ['jxra'],
246
+ 'image/jxrs': ['jxrs'],
247
+ 'image/jxs': ['jxs'],
248
+ 'image/jxsc': ['jxsc'],
249
+ 'image/jxsi': ['jxsi'],
250
+ 'image/jxss': ['jxss'],
251
+ 'image/ktx': ['ktx'],
252
+ 'image/ktx2': ['ktx2'],
253
+ 'image/png': ['png'],
254
+ 'image/sgi': ['sgi'],
255
+ 'image/svg+xml': ['svg', 'svgz'],
256
+ 'image/t38': ['t38'],
257
+ 'image/tiff': ['tif', 'tiff'],
258
+ 'image/tiff-fx': ['tfx'],
259
+ 'image/webp': ['webp'],
260
+ 'image/wmf': ['wmf'],
261
+ 'message/disposition-notification': ['disposition-notification'],
262
+ 'message/global': ['u8msg'],
263
+ 'message/global-delivery-status': ['u8dsn'],
264
+ 'message/global-disposition-notification': ['u8mdn'],
265
+ 'message/global-headers': ['u8hdr'],
266
+ 'message/rfc822': ['eml', 'mime'],
267
+ 'model/3mf': ['3mf'],
268
+ 'model/gltf+json': ['gltf'],
269
+ 'model/gltf-binary': ['glb'],
270
+ 'model/iges': ['igs', 'iges'],
271
+ 'model/mesh': ['msh', 'mesh', 'silo'],
272
+ 'model/mtl': ['mtl'],
273
+ 'model/obj': ['obj'],
274
+ 'model/step+xml': ['stpx'],
275
+ 'model/step+zip': ['stpz'],
276
+ 'model/step-xml+zip': ['stpxz'],
277
+ 'model/stl': ['stl'],
278
+ 'model/vrml': ['wrl', 'vrml'],
279
+ 'model/x3d+binary': ['(.)+x3db', 'x3dbz'],
280
+ 'model/x3d+fastinfoset': ['x3db'],
281
+ 'model/x3d+vrml': ['(.)+x3dv', 'x3dvz'],
282
+ 'model/x3d+xml': ['x3d', 'x3dz'],
283
+ 'model/x3d-vrml': ['x3dv'],
284
+ 'text/cache-manifest': ['appcache', 'manifest'],
285
+ 'text/calendar': ['ics', 'ifb'],
286
+ 'text/coffeescript': ['coffee', 'litcoffee'],
287
+ 'text/css': ['css'],
288
+ 'text/csv': ['csv'],
289
+ 'text/html': ['html', 'htm', 'shtml'],
290
+ 'text/jade': ['jade'],
291
+ 'text/jsx': ['jsx'],
292
+ 'text/less': ['less'],
293
+ 'text/markdown': ['markdown', 'md'],
294
+ 'text/mathml': ['mml'],
295
+ 'text/mdx': ['mdx'],
296
+ 'text/n3': ['n3'],
297
+ 'text/plain': ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'],
298
+ 'text/richtext': ['rtx'],
299
+ 'text/rtf': ['(.)+rtf'],
300
+ 'text/sgml': ['sgml', 'sgm'],
301
+ 'text/shex': ['shex'],
302
+ 'text/slim': ['slim', 'slm'],
303
+ 'text/spdx': ['spdx'],
304
+ 'text/stylus': ['stylus', 'styl'],
305
+ 'text/tab-separated-values': ['tsv'],
306
+ 'text/troff': ['t', 'tr', 'roff', 'man', 'me', 'ms'],
307
+ 'text/turtle': ['ttl'],
308
+ 'text/uri-list': ['uri', 'uris', 'urls'],
309
+ 'text/vcard': ['vcard'],
310
+ 'text/vtt': ['vtt'],
311
+ 'text/xml': ['(.)+xml'],
312
+ 'text/yaml': ['yaml', 'yml'],
313
+ 'video/3gpp': ['3gp', '3gpp'],
314
+ 'video/3gpp2': ['3g2'],
315
+ 'video/h261': ['h261'],
316
+ 'video/h263': ['h263'],
317
+ 'video/h264': ['h264'],
318
+ 'video/iso.segment': ['m4s'],
319
+ 'video/jpeg': ['jpgv'],
320
+ 'video/jpm': ['(.)+jpm', 'jpgm'],
321
+ 'video/mj2': ['mj2', 'mjp2'],
322
+ 'video/mp2t': ['ts'],
323
+ 'video/mp4': ['mp4', 'mp4v', 'mpg4'],
324
+ 'video/mpeg': ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'],
325
+ 'video/ogg': ['ogv'],
326
+ 'video/quicktime': ['qt', 'mov'],
327
+ 'video/webm': ['webm'],
328
+ }
329
+
330
+ export const getMimeForFile = (file: string) => {
331
+ const ext = path.extname(file)
332
+ return (
333
+ Object.entries(mimeTypes).find(([, values]) => values.some((value) => ext.match(`^.${value}$`)))?.[0] ||
334
+ 'application/octet-stream'
335
+ )
336
+ }
@@ -15,9 +15,14 @@ export interface OnRequest {
15
15
  res: ServerResponse
16
16
  }
17
17
 
18
+ export interface ServerApi {
19
+ shouldExec: (options: OnRequest) => boolean
20
+ onRequest: (options: OnRequest) => void
21
+ }
22
+
18
23
  export interface ServerRecord {
19
24
  server: Server
20
- apis: Array<{ shouldExec: (options: OnRequest) => boolean; onRequest: (options: OnRequest) => void }>
25
+ apis: ServerApi[]
21
26
  }
22
27
 
23
28
  @Injectable({ lifetime: 'singleton' })
@@ -0,0 +1,226 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { sleepAsync, usingAsync } from '@furystack/utils'
3
+ import got, { RequestError } from 'got'
4
+ import { ServerManager } from './server-manager'
5
+ import { StaticServerManager } from './static-server-manager'
6
+
7
+ describe('StaticServerManager', () => {
8
+ describe('Top level routing', () => {
9
+ it('Should return a 404 without fallback', async () => {
10
+ await usingAsync(new Injector(), async (injector) => {
11
+ const staticServerManager = injector.getInstance(StaticServerManager)
12
+
13
+ await staticServerManager.addStaticSite({
14
+ baseUrl: '/',
15
+ path: '.',
16
+ port: 1234,
17
+ })
18
+
19
+ try {
20
+ await got.get('http://localhost:1234/not-found.html')
21
+ } catch (error) {
22
+ expect(error).toBeInstanceOf(RequestError)
23
+ const requestError: RequestError = error as RequestError
24
+
25
+ expect(requestError.response?.statusCode).toBe(404)
26
+ expect(requestError.response?.headers['content-type']).toBe('text/plain')
27
+ expect(requestError.response?.body).toBe('Not found')
28
+ }
29
+ })
30
+ })
31
+
32
+ it('Should return a fallback', async () => {
33
+ await usingAsync(new Injector(), async (injector) => {
34
+ const staticServerManager = injector.getInstance(StaticServerManager)
35
+
36
+ await staticServerManager.addStaticSite({
37
+ baseUrl: '/',
38
+ path: '.',
39
+ fallback: 'package.json',
40
+ port: 1234,
41
+ headers: {
42
+ 'custom-header': 'custom-value',
43
+ },
44
+ })
45
+
46
+ const result = await got.get('http://localhost:1234/not-found.html')
47
+
48
+ expect(result.headers['content-type']).toBe('application/json')
49
+ expect(result.headers['custom-header']).toBe('custom-value')
50
+ })
51
+ })
52
+
53
+ it('Should return a defined file from a root directory', async () => {
54
+ await usingAsync(new Injector(), async (injector) => {
55
+ const staticServerManager = injector.getInstance(StaticServerManager)
56
+
57
+ await staticServerManager.addStaticSite({
58
+ baseUrl: '/',
59
+ path: '.',
60
+ port: 1234,
61
+ headers: {
62
+ 'custom-header': 'custom-value',
63
+ },
64
+ })
65
+
66
+ const result = await got.get('http://localhost:1234/README.md')
67
+
68
+ expect(result.headers['content-type']).toBe('text/markdown')
69
+ expect(result.headers['custom-header']).toBe('custom-value')
70
+ })
71
+ })
72
+
73
+ it('Should return a defined file from a subdirectory', async () => {
74
+ await usingAsync(new Injector(), async (injector) => {
75
+ const staticServerManager = injector.getInstance(StaticServerManager)
76
+
77
+ await staticServerManager.addStaticSite({
78
+ baseUrl: '/',
79
+ path: '.',
80
+ port: 1234,
81
+ })
82
+
83
+ const result = await got.get('http://localhost:1234/packages/utils/README.md')
84
+
85
+ expect(result.headers['content-type']).toBe('text/markdown')
86
+ })
87
+ })
88
+ })
89
+
90
+ describe('Non-top level routing', () => {
91
+ it('Should not handle a request when the path is not matching', async () => {
92
+ await usingAsync(new Injector(), async (injector) => {
93
+ const staticServerManager = injector.getInstance(StaticServerManager)
94
+
95
+ await staticServerManager.addStaticSite({
96
+ baseUrl: '/bundle',
97
+ path: '.',
98
+ port: 1234,
99
+ })
100
+
101
+ const server = [...injector.getInstance(ServerManager).servers.values()][0]
102
+
103
+ server.apis[0].onRequest = jest.fn()
104
+
105
+ got.get('http://localhost:1234/bundleToAnotherFolder/not-found.html')
106
+
107
+ await sleepAsync(100)
108
+
109
+ expect(server.apis[0].onRequest).not.toHaveBeenCalled()
110
+ })
111
+ })
112
+
113
+ it('Should return a 404 without fallback', async () => {
114
+ await usingAsync(new Injector(), async (injector) => {
115
+ const staticServerManager = injector.getInstance(StaticServerManager)
116
+
117
+ await staticServerManager.addStaticSite({
118
+ baseUrl: '/bundle',
119
+ path: '.',
120
+ port: 1234,
121
+ })
122
+
123
+ try {
124
+ await got.get('http://localhost:1234/bundle/not-found.html')
125
+ } catch (error) {
126
+ expect(error).toBeInstanceOf(RequestError)
127
+ const requestError: RequestError = error as RequestError
128
+
129
+ expect(requestError.response?.statusCode).toBe(404)
130
+ expect(requestError.response?.headers['content-type']).toBe('text/plain')
131
+ expect(requestError.response?.body).toBe('Not found')
132
+ }
133
+ })
134
+ })
135
+
136
+ it('Should return a fallback', async () => {
137
+ await usingAsync(new Injector(), async (injector) => {
138
+ const staticServerManager = injector.getInstance(StaticServerManager)
139
+
140
+ await staticServerManager.addStaticSite({
141
+ baseUrl: '/bundle',
142
+ path: '.',
143
+ fallback: 'package.json',
144
+ port: 1234,
145
+ })
146
+
147
+ const result = await got.get('http://localhost:1234/bundle/not-found.html')
148
+
149
+ expect(result.headers['content-type']).toBe('application/json')
150
+ })
151
+ })
152
+
153
+ it('Should return a defined file from a root directory', async () => {
154
+ await usingAsync(new Injector(), async (injector) => {
155
+ const staticServerManager = injector.getInstance(StaticServerManager)
156
+
157
+ await staticServerManager.addStaticSite({
158
+ baseUrl: '/',
159
+ path: '.',
160
+ port: 1234,
161
+ })
162
+
163
+ const result = await got.get('http://localhost:1234/README.md')
164
+
165
+ expect(result.headers['content-type']).toBe('text/markdown')
166
+ })
167
+ })
168
+
169
+ it('Should return a defined file from a subdirectory', async () => {
170
+ await usingAsync(new Injector(), async (injector) => {
171
+ const staticServerManager = injector.getInstance(StaticServerManager)
172
+
173
+ await staticServerManager.addStaticSite({
174
+ baseUrl: '/',
175
+ path: '.',
176
+ port: 1234,
177
+ })
178
+
179
+ const result = await got.get('http://localhost:1234/packages/utils/README.md')
180
+
181
+ expect(result.headers['content-type']).toBe('text/markdown')
182
+ })
183
+ })
184
+ })
185
+
186
+ describe('shouldExec', () => {
187
+ it('Should NOT match for requests with not defined method', () => {
188
+ const staticServerManager = new StaticServerManager()
189
+ expect(staticServerManager.shouldExec('/')({ req: { url: '/' } })).toBe(false)
190
+ })
191
+
192
+ it('Should NOT match for non-GET requests', () => {
193
+ const staticServerManager = new StaticServerManager()
194
+ expect(staticServerManager.shouldExec('/')({ req: { method: 'POST', url: '/' } })).toBe(false)
195
+ })
196
+ ;[
197
+ ['/', '/'],
198
+ ['/', '/index.html'],
199
+ ['/', '/subdir'],
200
+ ['/', '/subdir/'],
201
+ ['/', '/subdir/file.js'],
202
+ ['/subdir', '/subdir'],
203
+ ['/subdir', '/subdir/'],
204
+ ['/subdir', '/subdir/file.js'],
205
+ ['/subdir', '/subdir/s2/file.js'],
206
+ ].forEach(([root, url]) =>
207
+ it(`Should match for '${root}' root and '${url}' url`, () => {
208
+ const staticServerManager = new StaticServerManager()
209
+ const shouldExec = staticServerManager.shouldExec(root)({ req: { method: 'GET', url } })
210
+ expect(shouldExec).toBe(true)
211
+ }),
212
+ )
213
+
214
+ it('Should not exec different paths for non-top level root directory', () => {
215
+ const staticServerManager = new StaticServerManager()
216
+ expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/' } })).toBe(false)
217
+ expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/other/index.html' } })).toBe(
218
+ false,
219
+ )
220
+ expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/subdir2' } })).toBe(false)
221
+ expect(staticServerManager.shouldExec('/subdir')({ req: { method: 'GET', url: '/subdir2/index.html' } })).toBe(
222
+ false,
223
+ )
224
+ })
225
+ })
226
+ })
@@ -0,0 +1,89 @@
1
+ import { Injectable, Injected } from '@furystack/inject'
2
+ import { createReadStream } from 'fs'
3
+ import { stat } from 'fs/promises'
4
+ import { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'http'
5
+ import { getMimeForFile } from './mime-types'
6
+ import { join, normalize, sep } from 'path'
7
+ import { ServerManager } from './server-manager'
8
+
9
+ export interface StaticServerOptions {
10
+ baseUrl: string
11
+ path: string
12
+ hostName?: string
13
+ port: number
14
+ fallback?: string
15
+ headers?: OutgoingHttpHeaders
16
+ }
17
+
18
+ @Injectable({ lifetime: 'singleton' })
19
+ export class StaticServerManager {
20
+ @Injected(ServerManager)
21
+ private readonly serverManager!: ServerManager
22
+
23
+ private async sendFile({
24
+ fullPath,
25
+ headers,
26
+ res,
27
+ }: {
28
+ fullPath: string
29
+ res: ServerResponse
30
+ headers?: OutgoingHttpHeaders
31
+ }) {
32
+ const { size } = await stat(fullPath)
33
+
34
+ const head = {
35
+ ...headers,
36
+ 'Content-Length': size,
37
+ 'Content-Type': getMimeForFile(fullPath),
38
+ }
39
+
40
+ res.writeHead(200, head)
41
+ createReadStream(fullPath, { autoClose: true }).pipe(res)
42
+ }
43
+
44
+ public shouldExec =
45
+ (baseUrl: string) =>
46
+ ({ req }: { req: Pick<IncomingMessage, 'url' | 'method'> }) =>
47
+ req.url &&
48
+ req.method?.toUpperCase() === 'GET' &&
49
+ (req.url === baseUrl || req.url.startsWith(baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`))
50
+ ? true
51
+ : false
52
+
53
+ private onRequest = ({
54
+ path,
55
+ baseUrl,
56
+ fallback,
57
+ headers,
58
+ }: {
59
+ path: string
60
+ baseUrl: string
61
+ fallback?: string
62
+ headers?: OutgoingHttpHeaders
63
+ }) => {
64
+ return async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => {
65
+ const filePath = (req.url as string).substring(baseUrl.length - 1).replaceAll('/', sep)
66
+ const fullPath = normalize(join(path, filePath))
67
+
68
+ try {
69
+ await this.sendFile({ fullPath, res, headers })
70
+ } catch (error) {
71
+ if (fallback) {
72
+ await this.sendFile({ fullPath: join(path, fallback), res, headers })
73
+ } else {
74
+ res.writeHead(404, { 'Content-Type': 'text/plain' })
75
+ res.end('Not found')
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ public async addStaticSite(options: StaticServerOptions) {
82
+ const server = await this.serverManager.getOrCreate({ hostName: options.hostName, port: options.port })
83
+
84
+ server.apis.push({
85
+ shouldExec: this.shouldExec(options.baseUrl),
86
+ onRequest: this.onRequest({ ...options }),
87
+ })
88
+ }
89
+ }