@fastify/static 6.11.0 → 6.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/README.md CHANGED
@@ -21,7 +21,7 @@ Plugin for serving static files as fast as possible. Supports Fastify version `4
21
21
 
22
22
  ```js
23
23
  const fastify = require('fastify')({logger: true})
24
- const path = require('path')
24
+ const path = require('node:path')
25
25
 
26
26
  fastify.register(require('@fastify/static'), {
27
27
  root: path.join(__dirname, 'public'),
@@ -57,7 +57,7 @@ fastify.listen({ port: 3000 }, (err, address) => {
57
57
  ```js
58
58
  const fastify = require('fastify')()
59
59
  const fastifyStatic = require('@fastify/static')
60
- const path = require('path')
60
+ const path = require('node:path')
61
61
  // first plugin
62
62
  fastify.register(fastifyStatic, {
63
63
  root: path.join(__dirname, 'public')
@@ -76,7 +76,7 @@ fastify.register(fastifyStatic, {
76
76
 
77
77
  ```js
78
78
  const fastify = require('fastify')()
79
- const path = require('path')
79
+ const path = require('node:path')
80
80
 
81
81
  fastify.register(require('@fastify/static'), {
82
82
  root: path.join(__dirname, 'public'),
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
3
+ const path = require('node:path')
4
4
  const fastify = require('fastify')({ logger: { level: 'trace' } })
5
5
 
6
6
  fastify
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
3
+ const path = require('node:path')
4
4
  const Handlebars = require('handlebars')
5
5
 
6
6
  const fastify = require('fastify')({ logger: { level: 'trace' } })
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
3
+ const path = require('node:path')
4
4
  const fastify = require('fastify')({ logger: { level: 'trace' } })
5
5
 
6
6
  fastify
package/example/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
3
+ const path = require('node:path')
4
4
  const fastify = require('fastify')({ logger: { level: 'trace' } })
5
5
 
6
6
  fastify
package/index.js CHANGED
@@ -1,27 +1,34 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
4
- const url = require('url')
5
- const statSync = require('fs').statSync
6
- const { PassThrough } = require('readable-stream')
3
+ const { PassThrough } = require('node:stream')
4
+ const path = require('node:path')
5
+ const { fileURLToPath } = require('node:url')
6
+ const { statSync } = require('node:fs')
7
+ const { promisify } = require('node:util')
7
8
  const glob = require('glob')
8
- const send = require('@fastify/send')
9
- const contentDisposition = require('content-disposition')
9
+ const globPromise = promisify(glob)
10
10
  const fp = require('fastify-plugin')
11
- const util = require('util')
12
- const globPromise = util.promisify(glob)
11
+ const send = require('@fastify/send')
13
12
  const encodingNegotiator = require('@fastify/accept-negotiator')
14
-
15
- send.mime.default_type = 'application/octet-stream'
13
+ const contentDisposition = require('content-disposition')
16
14
 
17
15
  const dirList = require('./lib/dirList')
18
16
 
17
+ const winSeparatorRegex = new RegExp(`\\${path.win32.sep}`, 'g')
18
+ const backslashRegex = /\\/g
19
+ const startForwardSlashRegex = /^\//
20
+ const endForwardSlashRegex = /\/$/
21
+ const doubleForwardSlashRegex = /\/\//g
22
+ const asteriskRegex = /\*/g
23
+
24
+ const supportedEncodings = ['br', 'gzip', 'deflate']
25
+ send.mime.default_type = 'application/octet-stream'
26
+
19
27
  async function fastifyStatic (fastify, opts) {
20
28
  opts.root = normalizeRoot(opts.root)
21
29
  checkRootPathForErrors(fastify, opts.root)
22
30
 
23
31
  const setHeaders = opts.setHeaders
24
-
25
32
  if (setHeaders !== undefined && typeof setHeaders !== 'function') {
26
33
  throw new TypeError('The `setHeaders` option must be a function')
27
34
  }
@@ -48,19 +55,124 @@ async function fastifyStatic (fastify, opts) {
48
55
  maxAge: opts.maxAge
49
56
  }
50
57
 
51
- const allowedPath = opts.allowedPath
52
-
53
- if (opts.prefix === undefined) opts.prefix = '/'
54
-
55
- let prefix = opts.prefix
58
+ let prefix = opts.prefix ?? (opts.prefix = '/')
56
59
 
57
60
  if (!opts.prefixAvoidTrailingSlash) {
58
61
  prefix =
59
- opts.prefix[opts.prefix.length - 1] === '/'
60
- ? opts.prefix
61
- : opts.prefix + '/'
62
+ prefix[prefix.length - 1] === '/'
63
+ ? prefix
64
+ : prefix + '/'
65
+ }
66
+
67
+ // Set the schema hide property if defined in opts or true by default
68
+ const routeOpts = {
69
+ constraints: opts.constraints,
70
+ schema: {
71
+ hide: opts.schemaHide !== undefined ? opts.schemaHide : true
72
+ },
73
+ errorHandler (error, request, reply) {
74
+ if (error?.code === 'ERR_STREAM_PREMATURE_CLOSE') {
75
+ reply.request.raw.destroy()
76
+ return
77
+ }
78
+
79
+ fastify.errorHandler(error, request, reply)
80
+ }
81
+ }
82
+
83
+ if (opts.decorateReply !== false) {
84
+ fastify.decorateReply('sendFile', function (filePath, rootPath, options) {
85
+ const opts = typeof rootPath === 'object' ? rootPath : options
86
+ const root = typeof rootPath === 'string' ? rootPath : opts && opts.root
87
+ pumpSendToReply(
88
+ this.request,
89
+ this,
90
+ filePath,
91
+ root || sendOptions.root,
92
+ 0,
93
+ opts
94
+ )
95
+ return this
96
+ })
97
+
98
+ fastify.decorateReply(
99
+ 'download',
100
+ function (filePath, fileName, options = {}) {
101
+ const { root, ...opts } =
102
+ typeof fileName === 'object' ? fileName : options
103
+ fileName = typeof fileName === 'string' ? fileName : filePath
104
+
105
+ // Set content disposition header
106
+ this.header('content-disposition', contentDisposition(fileName))
107
+
108
+ pumpSendToReply(this.request, this, filePath, root, 0, opts)
109
+
110
+ return this
111
+ }
112
+ )
113
+ }
114
+
115
+ if (opts.serve !== false) {
116
+ if (opts.wildcard && typeof opts.wildcard !== 'boolean') {
117
+ throw new Error('"wildcard" option must be a boolean')
118
+ }
119
+ if (opts.wildcard === undefined || opts.wildcard === true) {
120
+ fastify.head(prefix + '*', routeOpts, function (req, reply) {
121
+ pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
122
+ })
123
+ fastify.get(prefix + '*', routeOpts, function (req, reply) {
124
+ pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
125
+ })
126
+ if (opts.redirect === true && prefix !== opts.prefix) {
127
+ fastify.get(opts.prefix, routeOpts, function (req, reply) {
128
+ reply.redirect(301, getRedirectUrl(req.raw.url))
129
+ })
130
+ }
131
+ } else {
132
+ const globPattern = '**/**'
133
+ const indexDirs = new Map()
134
+ const routes = new Set()
135
+
136
+ const roots = Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]
137
+ for (let i = 0; i < roots.length; ++i) {
138
+ const rootPath = roots[i]
139
+ const files = await globPromise(path.join(rootPath, globPattern).replace(winSeparatorRegex, path.posix.sep), { nodir: true, dot: opts.serveDotFiles })
140
+ const indexes = opts.index === undefined ? ['index.html'] : [].concat(opts.index)
141
+
142
+ for (let i = 0; i < files.length; ++i) {
143
+ const file = files[i].replace(rootPath.replace(backslashRegex, '/'), '')
144
+ .replace(startForwardSlashRegex, '')
145
+ const route = (prefix + file).replace(doubleForwardSlashRegex, '/')
146
+
147
+ if (routes.has(route)) {
148
+ continue
149
+ }
150
+
151
+ routes.add(route)
152
+
153
+ setUpHeadAndGet(routeOpts, route, '/' + file, rootPath)
154
+
155
+ const key = path.posix.basename(route)
156
+ if (indexes.includes(key) && !indexDirs.has(key)) {
157
+ indexDirs.set(path.posix.dirname(route), rootPath)
158
+ }
159
+ }
160
+ }
161
+
162
+ for (const [dirname, rootPath] of indexDirs.entries()) {
163
+ const pathname = dirname + (dirname.endsWith('/') ? '' : '/')
164
+ const file = '/' + pathname.replace(prefix, '')
165
+ setUpHeadAndGet(routeOpts, pathname, file, rootPath)
166
+
167
+ if (opts.redirect === true) {
168
+ setUpHeadAndGet(routeOpts, pathname.replace(endForwardSlashRegex, ''), file.replace(endForwardSlashRegex, ''), rootPath)
169
+ }
170
+ }
171
+ }
62
172
  }
63
173
 
174
+ const allowedPath = opts.allowedPath
175
+
64
176
  function pumpSendToReply (
65
177
  request,
66
178
  reply,
@@ -271,121 +383,12 @@ async function fastifyStatic (fastify, opts) {
271
383
  stream.pipe(wrap)
272
384
  }
273
385
 
274
- const errorHandler = (error, request, reply) => {
275
- if (error && error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
276
- reply.request.raw.destroy()
277
- return
278
- }
279
-
280
- fastify.errorHandler(error, request, reply)
281
- }
282
-
283
- // Set the schema hide property if defined in opts or true by default
284
- const routeOpts = {
285
- constraints: opts.constraints,
286
- schema: {
287
- hide: typeof opts.schemaHide !== 'undefined' ? opts.schemaHide : true
288
- },
289
- errorHandler
290
- }
291
-
292
- if (opts.decorateReply !== false) {
293
- fastify.decorateReply('sendFile', function (filePath, rootPath, options) {
294
- const opts = typeof rootPath === 'object' ? rootPath : options
295
- const root = typeof rootPath === 'string' ? rootPath : opts && opts.root
296
- pumpSendToReply(
297
- this.request,
298
- this,
299
- filePath,
300
- root || sendOptions.root,
301
- 0,
302
- opts
303
- )
304
- return this
305
- })
306
-
307
- fastify.decorateReply(
308
- 'download',
309
- function (filePath, fileName, options = {}) {
310
- const { root, ...opts } =
311
- typeof fileName === 'object' ? fileName : options
312
- fileName = typeof fileName === 'string' ? fileName : filePath
313
-
314
- // Set content disposition header
315
- this.header('content-disposition', contentDisposition(fileName))
316
-
317
- pumpSendToReply(this.request, this, filePath, root, 0, opts)
318
-
319
- return this
320
- }
321
- )
322
- }
323
-
324
- if (opts.serve !== false) {
325
- if (opts.wildcard && typeof opts.wildcard !== 'boolean') {
326
- throw new Error('"wildcard" option must be a boolean')
327
- }
328
- if (opts.wildcard === undefined || opts.wildcard === true) {
329
- fastify.head(prefix + '*', routeOpts, function (req, reply) {
330
- pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
331
- })
332
- fastify.get(prefix + '*', routeOpts, function (req, reply) {
333
- pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
334
- })
335
- if (opts.redirect === true && prefix !== opts.prefix) {
336
- fastify.get(opts.prefix, routeOpts, function (req, reply) {
337
- reply.redirect(301, getRedirectUrl(req.raw.url))
338
- })
339
- }
340
- } else {
341
- const globPattern = '**/**'
342
- const indexDirs = new Map()
343
- const routes = new Set()
344
-
345
- const winSeparatorRegex = new RegExp(`\\${path.win32.sep}`, 'g')
346
-
347
- for (const rootPath of Array.isArray(sendOptions.root) ? sendOptions.root : [sendOptions.root]) {
348
- const files = await globPromise(path.join(rootPath, globPattern).replace(winSeparatorRegex, path.posix.sep), { nodir: true, dot: opts.serveDotFiles })
349
- const indexes = typeof opts.index === 'undefined' ? ['index.html'] : [].concat(opts.index)
350
-
351
- for (let file of files) {
352
- file = file
353
- .replace(rootPath.replace(/\\/g, '/'), '')
354
- .replace(/^\//, '')
355
- const route = (prefix + file).replace(/\/\//g, '/')
356
- if (routes.has(route)) {
357
- continue
358
- }
359
- routes.add(route)
360
-
361
- setUpHeadAndGet(routeOpts, route, '/' + file, rootPath)
362
-
363
- const key = path.posix.basename(route)
364
- if (indexes.includes(key) && !indexDirs.has(key)) {
365
- indexDirs.set(path.posix.dirname(route), rootPath)
366
- }
367
- }
368
- }
369
-
370
- for (const [dirname, rootPath] of indexDirs.entries()) {
371
- const pathname = dirname + (dirname.endsWith('/') ? '' : '/')
372
- const file = '/' + pathname.replace(prefix, '')
373
- setUpHeadAndGet(routeOpts, pathname, file, rootPath)
374
-
375
- if (opts.redirect === true) {
376
- setUpHeadAndGet(routeOpts, pathname.replace(/\/$/, ''), file.replace(/\/$/, ''), rootPath)
377
- }
378
- }
379
- }
380
- }
381
-
382
386
  function setUpHeadAndGet (routeOpts, route, file, rootPath) {
383
- const toSetUp = {
384
- ...routeOpts,
387
+ const toSetUp = Object.assign({}, routeOpts, {
385
388
  method: ['HEAD', 'GET'],
386
389
  url: route,
387
390
  handler: serveFileHandler
388
- }
391
+ })
389
392
  toSetUp.config = toSetUp.config || {}
390
393
  toSetUp.config.file = file
391
394
  toSetUp.config.rootPath = rootPath
@@ -393,9 +396,10 @@ async function fastifyStatic (fastify, opts) {
393
396
  }
394
397
 
395
398
  function serveFileHandler (req, reply) {
396
- const file = req.routeConfig.file
397
- const rootPath = req.routeConfig.rootPath
398
- pumpSendToReply(req, reply, file, rootPath)
399
+ // TODO: remove the fallback branch when bump major
400
+ /* istanbul ignore next */
401
+ const routeConfig = req.routeOptions?.config || req.routeConfig
402
+ pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
399
403
  }
400
404
  }
401
405
 
@@ -404,13 +408,13 @@ function normalizeRoot (root) {
404
408
  return root
405
409
  }
406
410
  if (root instanceof URL && root.protocol === 'file:') {
407
- return url.fileURLToPath(root)
411
+ return fileURLToPath(root)
408
412
  }
409
413
  if (Array.isArray(root)) {
410
414
  const result = []
411
415
  for (let i = 0, il = root.length; i < il; ++i) {
412
416
  if (root[i] instanceof URL && root[i].protocol === 'file:') {
413
- result.push(url.fileURLToPath(root[i]))
417
+ result.push(fileURLToPath(root[i]))
414
418
  } else {
415
419
  result.push(root[i])
416
420
  }
@@ -503,13 +507,13 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
503
507
  return false
504
508
  }
505
509
 
506
- const supportedEncodings = ['br', 'gzip', 'deflate']
507
-
508
510
  // Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
509
511
  function getEncodingHeader (headers, checked) {
510
512
  if (!('accept-encoding' in headers)) return
511
513
 
512
- const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip')
514
+ // consider the no-preference token as gzip for downstream compat
515
+ const header = headers['accept-encoding'].toLowerCase().replace(asteriskRegex, 'gzip')
516
+
513
517
  return encodingNegotiator.negotiate(
514
518
  header,
515
519
  supportedEncodings.filter((enc) => !checked.has(enc))
@@ -529,14 +533,15 @@ function getEncodingExtension (encoding) {
529
533
  function getRedirectUrl (url) {
530
534
  let i = 0
531
535
  // we detect how many slash before a valid path
532
- for (i; i < url.length; i++) {
536
+ for (; i < url.length; ++i) {
533
537
  if (url[i] !== '/' && url[i] !== '\\') break
534
538
  }
535
539
  // turns all leading / or \ into a single /
536
540
  url = '/' + url.substr(i)
537
541
  try {
538
542
  const parsed = new URL(url, 'http://localhost.com/')
539
- return parsed.pathname + (parsed.pathname[parsed.pathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
543
+ const parsedPathname = parsed.pathname
544
+ return parsedPathname + (parsedPathname[parsedPathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
540
545
  } catch (error) {
541
546
  // the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
542
547
  /* istanbul ignore next */
package/lib/dirList.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const path = require('path')
4
- const fs = require('fs').promises
3
+ const path = require('node:path')
4
+ const fs = require('node:fs/promises')
5
5
  const pLimit = require('p-limit')
6
6
 
7
7
  const dirList = {
@@ -16,7 +16,7 @@ const dirList = {
16
16
  const entries = { dirs: [], files: [] }
17
17
  let files = await fs.readdir(dir)
18
18
  if (dotfiles === 'deny' || dotfiles === 'ignore') {
19
- files = files.filter(f => f.charAt(0) !== '.')
19
+ files = files.filter(file => file.charAt(0) !== '.')
20
20
  }
21
21
  if (files.length < 1) {
22
22
  return entries
@@ -92,6 +92,7 @@ const dirList = {
92
92
 
93
93
  entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
94
94
  entries.files.sort((a, b) => a.name.localeCompare(b.name))
95
+
95
96
  return entries
96
97
  },
97
98
 
@@ -141,7 +142,7 @@ const dirList = {
141
142
  * @return {ListFile}
142
143
  */
143
144
  htmlInfo: function (entry, route, prefix, options) {
144
- if (options.names && options.names.includes(path.basename(route))) {
145
+ if (options.names?.includes(path.basename(route))) {
145
146
  route = path.normalize(path.join(route, '..'))
146
147
  }
147
148
  return {
@@ -159,12 +160,9 @@ const dirList = {
159
160
  * @return {boolean}
160
161
  */
161
162
  handle: function (route, options) {
162
- if (!options.names) {
163
- return false
164
- }
165
- return options.names.includes(path.basename(route)) ||
163
+ return options.names?.includes(path.basename(route)) ||
166
164
  // match trailing slash
167
- (options.names.includes('/') && route[route.length - 1] === '/')
165
+ ((options.names?.includes('/') && route[route.length - 1] === '/') ?? false)
168
166
  },
169
167
 
170
168
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "6.11.0",
3
+ "version": "6.11.2",
4
4
  "description": "Plugin for serving static files as fast as possible.",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -35,8 +35,7 @@
35
35
  "content-disposition": "^0.5.3",
36
36
  "fastify-plugin": "^4.0.0",
37
37
  "glob": "^8.0.1",
38
- "p-limit": "^3.1.0",
39
- "readable-stream": "^4.0.0"
38
+ "p-limit": "^3.1.0"
40
39
  },
41
40
  "devDependencies": {
42
41
  "@fastify/compress": "^6.0.0",
@@ -55,7 +54,7 @@
55
54
  "snazzy": "^9.0.0",
56
55
  "standard": "^17.0.0",
57
56
  "tap": "^16.0.0",
58
- "tsd": "^0.28.0",
57
+ "tsd": "^0.29.0",
59
58
  "typescript": "^5.1.6"
60
59
  },
61
60
  "tsd": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  /* eslint n/no-deprecated-api: "off" */
4
4
 
5
- const path = require('path')
5
+ const path = require('node:path')
6
6
  const { test } = require('tap')
7
7
  const simple = require('simple-get')
8
8
  const Fastify = require('fastify')
@@ -2,8 +2,8 @@
2
2
 
3
3
  /* eslint n/no-deprecated-api: "off" */
4
4
 
5
- const fs = require('fs')
6
- const path = require('path')
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
7
  const t = require('tap')
8
8
  const simple = require('simple-get')
9
9
  const Fastify = require('fastify')
@@ -2,10 +2,10 @@
2
2
 
3
3
  /* eslint n/no-deprecated-api: "off" */
4
4
 
5
- const path = require('path')
6
- const fs = require('fs')
7
- const url = require('url')
8
- const http = require('http')
5
+ const path = require('node:path')
6
+ const fs = require('node:fs')
7
+ const url = require('node:url')
8
+ const http = require('node:http')
9
9
  const t = require('tap')
10
10
  const simple = require('simple-get')
11
11
  const Fastify = require('fastify')
@@ -2823,7 +2823,7 @@ t.test(
2823
2823
  'register with rootpath that causes statSync to fail with non-ENOENT code',
2824
2824
  (t) => {
2825
2825
  const fastifyStatic = proxyquire('../', {
2826
- fs: {
2826
+ 'node:fs': {
2827
2827
  statSync: function statSyncStub (path) {
2828
2828
  throw new Error({ code: 'MOCK' })
2829
2829
  }
Binary file