@fastify/static 6.10.2 → 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/.eslintrc.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "extends": [
3
+ "eslint:recommended",
4
+ "plugin:@typescript-eslint/eslint-recommended",
5
+ "plugin:@typescript-eslint/recommended",
6
+ "standard"
7
+ ],
8
+ "parser": "@typescript-eslint/parser",
9
+ "plugins": ["@typescript-eslint"],
10
+ "env": { "node": true },
11
+ "parserOptions": {
12
+ "ecmaVersion": 6,
13
+ "sourceType": "module",
14
+ "project": "./tsconfig.eslint.json",
15
+ "createDefaultProgram": true
16
+ },
17
+ "rules": {
18
+ "no-console": "off",
19
+ "@typescript-eslint/indent": ["error", 2],
20
+ "semi": ["error", "never"],
21
+ "import/export": "off" // this errors on multiple exports (overload interfaces)
22
+ },
23
+ "overrides": [
24
+ {
25
+ "files": ["*.d.ts","*.test-d.ts"],
26
+ "rules": {
27
+ "no-use-before-define": "off",
28
+ "no-redeclare": "off",
29
+ "@typescript-eslint/no-explicit-any": "off"
30
+ }
31
+ },
32
+ {
33
+ "files": ["*.test-d.ts"],
34
+ "rules": {
35
+ "@typescript-eslint/no-var-requires": "off",
36
+ "no-unused-vars": "off",
37
+ "n/handle-callback-err": "off",
38
+ "@typescript-eslint/no-empty-function": "off",
39
+ "@typescript-eslint/explicit-function-return-type": "off",
40
+ "@typescript-eslint/no-unused-vars": "off",
41
+ "@typescript-eslint/no-non-null-assertion": "off",
42
+ "@typescript-eslint/no-misused-promises": ["error", {
43
+ "checksVoidReturn": false
44
+ }]
45
+ },
46
+ "globals": {
47
+ "NodeJS": "readonly"
48
+ }
49
+ }
50
+ ]
51
+ }
package/.taprc ADDED
@@ -0,0 +1,2 @@
1
+ files:
2
+ - test/**/*.test.js
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'),
@@ -452,6 +452,25 @@ If a request matches the URL `prefix` but a file cannot be found for the
452
452
  request, Fastify's 404 handler will be called. You can set a custom 404
453
453
  handler with [`fastify.setNotFoundHandler()`](https://www.fastify.io/docs/latest/Reference/Server/#setnotfoundhandler).
454
454
 
455
+ When registering `@fastify/static` within an encapsulated context, the `wildcard` option may need to be set to `false` in order to support index resolution and nested not-found-handler:
456
+
457
+ ```js
458
+ const app = require('fastify')();
459
+
460
+ app.register((childContext, _, done) => {
461
+ childContext.register(require('@fastify/static'), {
462
+ root: path.join(__dirname, 'docs'), // docs is a folder that contains `index.html` and `404.html`
463
+ wildcard: false
464
+ });
465
+ childContext.setNotFoundHandler((_, reply) => {
466
+ return reply.code(404).type('text/html').sendFile('404.html');
467
+ });
468
+ done();
469
+ }, { prefix: 'docs' });
470
+ ```
471
+
472
+ This code will send the `index.html` for the paths `docs`, `docs/`, and `docs/index.html`. For all other `docs/<undefined-routes>` it will reply with `404.html`.
473
+
455
474
  ### Handling Errors
456
475
 
457
476
  If an error occurs while trying to send a file, the error will be passed
@@ -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(fastify, 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(fastify, routeOpts, pathname, file, rootPath)
374
-
375
- if (opts.redirect === true) {
376
- setUpHeadAndGet(fastify, routeOpts, pathname.replace(/\/$/, ''), file.replace(/\/$/, ''), rootPath)
377
- }
378
- }
379
- }
380
- }
381
-
382
- function setUpHeadAndGet (fastify, routeOpts, route, file, rootPath) {
383
- const toSetUp = {
384
- ...routeOpts,
386
+ function setUpHeadAndGet (routeOpts, route, file, rootPath) {
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,23 +396,23 @@ 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
  }
403
+
401
404
  function normalizeRoot (root) {
402
405
  if (root === undefined) {
403
406
  return root
404
407
  }
405
408
  if (root instanceof URL && root.protocol === 'file:') {
406
- return url.fileURLToPath(root)
409
+ return fileURLToPath(root)
407
410
  }
408
411
  if (Array.isArray(root)) {
409
412
  const result = []
410
413
  for (let i = 0, il = root.length; i < il; ++i) {
411
414
  if (root[i] instanceof URL && root[i].protocol === 'file:') {
412
- result.push(url.fileURLToPath(root[i]))
415
+ result.push(fileURLToPath(root[i]))
413
416
  } else {
414
417
  result.push(root[i])
415
418
  }
@@ -475,8 +478,6 @@ function checkPath (fastify, rootPath) {
475
478
  }
476
479
  }
477
480
 
478
- const supportedEncodings = ['br', 'gzip', 'deflate']
479
-
480
481
  function getContentType (path) {
481
482
  const type = send.mime.getType(path) || send.mime.default_type
482
483
 
@@ -508,7 +509,9 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
508
509
  function getEncodingHeader (headers, checked) {
509
510
  if (!('accept-encoding' in headers)) return
510
511
 
511
- 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
+
512
515
  return encodingNegotiator.negotiate(
513
516
  header,
514
517
  supportedEncodings.filter((enc) => !checked.has(enc))
@@ -528,14 +531,15 @@ function getEncodingExtension (encoding) {
528
531
  function getRedirectUrl (url) {
529
532
  let i = 0
530
533
  // we detect how many slash before a valid path
531
- for (i; i < url.length; i++) {
534
+ for (; i < url.length; ++i) {
532
535
  if (url[i] !== '/' && url[i] !== '\\') break
533
536
  }
534
537
  // turns all leading / or \ into a single /
535
538
  url = '/' + url.substr(i)
536
539
  try {
537
540
  const parsed = new URL(url, 'http://localhost.com/')
538
- 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 || '')
539
543
  } catch (error) {
540
544
  // the try-catch here is actually unreachable, but we keep it for safety and prevent DoS attack
541
545
  /* istanbul ignore next */
@@ -548,7 +552,7 @@ function getRedirectUrl (url) {
548
552
  }
549
553
 
550
554
  module.exports = fp(fastifyStatic, {
551
- fastify: '4.x',
555
+ fastify: '^4.23.0',
552
556
  name: '@fastify/static'
553
557
  })
554
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,18 +1,19 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "6.10.2",
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",
7
7
  "scripts": {
8
- "lint": "standard | snazzy",
9
- "lint:fix": "standard --fix",
10
- "unit": "tap test/*.test.js",
11
- "typescript": "tsd",
12
- "test": "npm run lint && npm run unit && npm run typescript",
13
- "example": "node example/server.js",
14
- "coverage": "tap --cov --coverage-report=html test",
15
- "coveralls": "tap test/*test.js test/*/*.test.js --cov"
8
+ "coverage": "npm run test:unit -- --coverage-report=html",
9
+ "lint": "npm run lint:javascript && npm run lint:typescript",
10
+ "lint:javascript": "standard | snazzy",
11
+ "lint:fix": "standard --fix && npm run lint:typescript -- --fix",
12
+ "lint:typescript": "eslint -c .eslintrc.json types/**/*.d.ts types/**/*.test-d.ts",
13
+ "test": "npm run test:unit && npm run test:typescript",
14
+ "test:typescript": "tsd",
15
+ "test:unit": "tap",
16
+ "example": "node example/server.js"
16
17
  },
17
18
  "repository": {
18
19
  "type": "git",
@@ -29,20 +30,19 @@
29
30
  },
30
31
  "homepage": "https://github.com/fastify/fastify-static",
31
32
  "dependencies": {
32
- "content-disposition": "^0.5.3",
33
33
  "@fastify/accept-negotiator": "^1.0.0",
34
+ "@fastify/send": "^2.0.0",
35
+ "content-disposition": "^0.5.3",
34
36
  "fastify-plugin": "^4.0.0",
35
37
  "glob": "^8.0.1",
36
- "p-limit": "^3.1.0",
37
- "readable-stream": "^4.0.0",
38
- "@fastify/send": "^2.0.0"
38
+ "p-limit": "^3.1.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@fastify/compress": "^6.0.0",
42
42
  "@fastify/pre-commit": "^2.0.2",
43
43
  "@types/node": "^20.1.0",
44
- "@typescript-eslint/eslint-plugin": "^2.29.0",
45
- "@typescript-eslint/parser": "^2.29.0",
44
+ "@typescript-eslint/eslint-plugin": "^6.3.0",
45
+ "@typescript-eslint/parser": "^6.3.0",
46
46
  "concat-stream": "^2.0.0",
47
47
  "coveralls": "^3.0.4",
48
48
  "eslint-plugin-typescript": "^0.14.0",
@@ -54,7 +54,8 @@
54
54
  "snazzy": "^9.0.0",
55
55
  "standard": "^17.0.0",
56
56
  "tap": "^16.0.0",
57
- "tsd": "^0.28.0"
57
+ "tsd": "^0.29.0",
58
+ "typescript": "^5.1.6"
58
59
  },
59
60
  "tsd": {
60
61
  "directory": "test/types"
@@ -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')
@@ -0,0 +1 @@
1
+ example
@@ -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
  }
@@ -3756,6 +3756,7 @@ t.test(
3756
3756
  t.end()
3757
3757
  }
3758
3758
  )
3759
+
3759
3760
  t.test(
3760
3761
  'converts URL to path',
3761
3762
  async (t) => {
@@ -3803,3 +3804,39 @@ t.test(
3803
3804
  t.same(response.body, foobarContent)
3804
3805
  }
3805
3806
  )
3807
+
3808
+ t.test(
3809
+ 'serves files with paths that have characters modified by encodeUri when wildcard is false',
3810
+ async (t) => {
3811
+ const aContent = fs.readFileSync(path.join(__dirname, 'static-encode/[...]', 'a .md'), 'utf-8')
3812
+
3813
+ t.plan(4)
3814
+ const pluginOptions = {
3815
+ root: url.pathToFileURL(path.join(__dirname, '/static-encode')),
3816
+ wildcard: false
3817
+ }
3818
+
3819
+ const fastify = Fastify()
3820
+
3821
+ fastify.register(fastifyStatic, pluginOptions)
3822
+ const response = await fastify.inject({
3823
+ method: 'GET',
3824
+ url: '[...]/a .md',
3825
+ headers: {
3826
+ 'accept-encoding': '*, *'
3827
+ }
3828
+ })
3829
+ t.equal(response.statusCode, 200)
3830
+ t.same(response.body, aContent)
3831
+
3832
+ const response2 = await fastify.inject({
3833
+ method: 'GET',
3834
+ url: '%5B...%5D/a%20.md',
3835
+ headers: {
3836
+ 'accept-encoding': '*, *'
3837
+ }
3838
+ })
3839
+ t.equal(response2.statusCode, 200)
3840
+ t.same(response2.body, aContent)
3841
+ }
3842
+ )
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "lib": [ "es2015" ],
5
+ "module": "commonjs",
6
+ "noEmit": true,
7
+ "strict": true
8
+ },
9
+ "include": [
10
+ "types/*.test-d.ts",
11
+ "types/*.d.ts"
12
+ ]
13
+ }
package/types/index.d.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  // Leo <https://github.com/leomelzer>
3
3
  /// <reference types="node" />
4
4
 
5
- import {FastifyPluginAsync, FastifyRequest, RouteOptions} from 'fastify';
6
- import { Stats } from 'fs';
5
+ import { FastifyPluginAsync, FastifyRequest, RouteOptions } from 'fastify'
6
+ import { Stats } from 'fs'
7
7
 
8
- declare module "fastify" {
8
+ declare module 'fastify' {
9
9
  interface FastifyReply {
10
10
  sendFile(filename: string, rootPath?: string): FastifyReply;
11
11
  sendFile(filename: string, options?: fastifyStatic.SendOptions): FastifyReply;
@@ -77,7 +77,7 @@ declare namespace fastifyStatic {
77
77
  }
78
78
 
79
79
  export interface FastifyStaticOptions extends SendOptions {
80
- root: string | string[];
80
+ root: string | string[] | URL | URL[];
81
81
  prefix?: string;
82
82
  prefixAvoidTrailingSlash?: boolean;
83
83
  serve?: boolean;
@@ -107,9 +107,9 @@ declare namespace fastifyStatic {
107
107
  constraints?: RouteOptions['constraints'];
108
108
  }
109
109
 
110
- export const fastifyStatic: FastifyStaticPlugin;
110
+ export const fastifyStatic: FastifyStaticPlugin
111
111
 
112
- export { fastifyStatic as default };
112
+ export { fastifyStatic as default }
113
113
  }
114
114
 
115
115
  declare function fastifyStatic(...params: Parameters<FastifyStaticPlugin>): ReturnType<FastifyStaticPlugin>;
@@ -1,34 +1,34 @@
1
1
  import fastify, { FastifyInstance, FastifyPluginAsync, FastifyRequest } from 'fastify'
2
- import { Server } from 'http';
2
+ import { Server } from 'http'
3
3
  import { expectAssignable, expectError, expectType } from 'tsd'
4
- import * as fastifyStaticStar from '..';
4
+ import * as fastifyStaticStar from '..'
5
5
  import fastifyStatic, {
6
6
  FastifyStaticOptions,
7
- fastifyStatic as fastifyStaticNamed,
7
+ fastifyStatic as fastifyStaticNamed
8
8
  } from '..'
9
9
 
10
10
  import fastifyStaticCjsImport = require('..');
11
- const fastifyStaticCjs = require('..');
12
-
13
- const app: FastifyInstance = fastify();
14
-
15
- app.register(fastifyStatic);
16
- app.register(fastifyStaticNamed);
17
- app.register(fastifyStaticCjs);
18
- app.register(fastifyStaticCjsImport.default);
19
- app.register(fastifyStaticCjsImport.fastifyStatic);
20
- app.register(fastifyStaticStar.default);
21
- app.register(fastifyStaticStar.fastifyStatic);
22
-
23
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStatic);
24
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticNamed);
25
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.default);
26
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.fastifyStatic);
27
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticStar.default);
11
+ const fastifyStaticCjs = require('..')
12
+
13
+ const app: FastifyInstance = fastify()
14
+
15
+ app.register(fastifyStatic)
16
+ app.register(fastifyStaticNamed)
17
+ app.register(fastifyStaticCjs)
18
+ app.register(fastifyStaticCjsImport.default)
19
+ app.register(fastifyStaticCjsImport.fastifyStatic)
20
+ app.register(fastifyStaticStar.default)
21
+ app.register(fastifyStaticStar.fastifyStatic)
22
+
23
+ expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStatic)
24
+ expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticNamed)
25
+ expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.default)
26
+ expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.fastifyStatic)
27
+ expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticStar.default)
28
28
  expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(
29
- fastifyStaticStar.fastifyStatic
30
- );
31
- expectType<any>(fastifyStaticCjs);
29
+ fastifyStaticStar.fastifyStatic
30
+ )
31
+ expectType<any>(fastifyStaticCjs)
32
32
 
33
33
  const appWithImplicitHttp = fastify()
34
34
  const options: FastifyStaticOptions = {
@@ -54,7 +54,7 @@ const options: FastifyStaticOptions = {
54
54
  },
55
55
  preCompressed: false,
56
56
  allowedPath: (pathName: string, root: string, request: FastifyRequest) => {
57
- return true;
57
+ return true
58
58
  },
59
59
  constraints: {
60
60
  host: /.*\.example\.com/,
@@ -70,7 +70,7 @@ expectError<FastifyStaticOptions>({
70
70
  expectAssignable<FastifyStaticOptions>({
71
71
  root: '',
72
72
  list: {
73
- format: 'json',
73
+ format: 'json'
74
74
  }
75
75
  })
76
76
 
@@ -93,10 +93,22 @@ expectAssignable<FastifyStaticOptions>({
93
93
  expectError<FastifyStaticOptions>({
94
94
  root: '',
95
95
  list: {
96
- format: 'html',
96
+ format: 'html'
97
97
  }
98
98
  })
99
99
 
100
+ expectAssignable<FastifyStaticOptions>({
101
+ root: ['']
102
+ })
103
+
104
+ expectAssignable<FastifyStaticOptions>({
105
+ root: new URL('')
106
+ })
107
+
108
+ expectAssignable<FastifyStaticOptions>({
109
+ root: [new URL('')]
110
+ })
111
+
100
112
  appWithImplicitHttp
101
113
  .register(fastifyStatic, options)
102
114
  .after(() => {
@@ -123,7 +135,7 @@ appWithHttp2
123
135
  })
124
136
 
125
137
  appWithHttp2.get('/download/2', (request, reply) => {
126
- reply.download('some-file-name', 'some-filename' ,{ cacheControl: false, acceptRanges: true })
138
+ reply.download('some-file-name', 'some-filename', { cacheControl: false, acceptRanges: true })
127
139
  })
128
140
  })
129
141
 
@@ -168,7 +180,17 @@ noIndexApp
168
180
  noIndexApp.get('/', (request, reply) => {
169
181
  reply.send('<h1>fastify-static</h1>')
170
182
  })
171
- })
183
+ })
184
+
185
+ options.root = new URL('')
186
+
187
+ const URLRootApp = fastify()
188
+ URLRootApp.register(fastifyStatic, options)
189
+ .after(() => {
190
+ URLRootApp.get('/', (request, reply) => {
191
+ reply.send('<h1>fastify-static</h1>')
192
+ })
193
+ })
172
194
 
173
195
  const defaultIndexApp = fastify()
174
196
  options.index = 'index.html'
Binary file