@fastify/static 6.11.0 → 6.11.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/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
+ )
62
113
  }
63
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
+ }
172
+ }
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,8 @@ 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
+ const routeConfig = req.routeOptions.config
400
+ pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
399
401
  }
400
402
  }
401
403
 
@@ -404,13 +406,13 @@ function normalizeRoot (root) {
404
406
  return root
405
407
  }
406
408
  if (root instanceof URL && root.protocol === 'file:') {
407
- return url.fileURLToPath(root)
409
+ return fileURLToPath(root)
408
410
  }
409
411
  if (Array.isArray(root)) {
410
412
  const result = []
411
413
  for (let i = 0, il = root.length; i < il; ++i) {
412
414
  if (root[i] instanceof URL && root[i].protocol === 'file:') {
413
- result.push(url.fileURLToPath(root[i]))
415
+ result.push(fileURLToPath(root[i]))
414
416
  } else {
415
417
  result.push(root[i])
416
418
  }
@@ -503,13 +505,13 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
503
505
  return false
504
506
  }
505
507
 
506
- const supportedEncodings = ['br', 'gzip', 'deflate']
507
-
508
508
  // Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
509
509
  function getEncodingHeader (headers, checked) {
510
510
  if (!('accept-encoding' in headers)) return
511
511
 
512
- const header = headers['accept-encoding'].toLowerCase().replace(/\*/g, 'gzip')
512
+ // consider the no-preference token as gzip for downstream compat
513
+ const header = headers['accept-encoding'].toLowerCase().replace(asteriskRegex, 'gzip')
514
+
513
515
  return encodingNegotiator.negotiate(
514
516
  header,
515
517
  supportedEncodings.filter((enc) => !checked.has(enc))
@@ -529,14 +531,15 @@ function getEncodingExtension (encoding) {
529
531
  function getRedirectUrl (url) {
530
532
  let i = 0
531
533
  // we detect how many slash before a valid path
532
- for (i; i < url.length; i++) {
534
+ for (; i < url.length; ++i) {
533
535
  if (url[i] !== '/' && url[i] !== '\\') break
534
536
  }
535
537
  // turns all leading / or \ into a single /
536
538
  url = '/' + url.substr(i)
537
539
  try {
538
540
  const parsed = new URL(url, 'http://localhost.com/')
539
- return parsed.pathname + (parsed.pathname[parsed.pathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
541
+ const parsedPathname = parsed.pathname
542
+ return parsedPathname + (parsedPathname[parsedPathname.length - 1] !== '/' ? '/' : '') + (parsed.search || '')
540
543
  } catch (error) {
541
544
  // the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
542
545
  /* istanbul ignore next */
@@ -549,7 +552,7 @@ function getRedirectUrl (url) {
549
552
  }
550
553
 
551
554
  module.exports = fp(fastifyStatic, {
552
- fastify: '4.x',
555
+ fastify: '^4.23.0',
553
556
  name: '@fastify/static'
554
557
  })
555
558
  module.exports.default = fastifyStatic
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.1",
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