@fastify/static 6.12.0 → 7.0.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
@@ -124,7 +124,7 @@ A URL path prefix used to create a virtual mount path for the static directory.
124
124
  Default: `{}`
125
125
 
126
126
  Constraints that will be added to registered routes. See Fastify's documentation for
127
- [route constraints](https://www.fastify.io/docs/latest/Reference/Routes/#constraints).
127
+ [route constraints](https://fastify.dev/docs/latest/Reference/Routes/#constraints).
128
128
 
129
129
  #### `prefixAvoidTrailingSlash`
130
130
 
@@ -450,7 +450,7 @@ decorators.
450
450
 
451
451
  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
- handler with [`fastify.setNotFoundHandler()`](https://www.fastify.io/docs/latest/Reference/Server/#setnotfoundhandler).
453
+ handler with [`fastify.setNotFoundHandler()`](https://fastify.dev/docs/latest/Reference/Server/#setnotfoundhandler).
454
454
 
455
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
456
 
@@ -475,7 +475,7 @@ This code will send the `index.html` for the paths `docs`, `docs/`, and `docs/in
475
475
 
476
476
  If an error occurs while trying to send a file, the error will be passed
477
477
  to Fastify's error handler. You can set a custom error handler with
478
- [`fastify.setErrorHandler()`](https://www.fastify.io/docs/latest/Reference/Server/#seterrorhandler).
478
+ [`fastify.setErrorHandler()`](https://fastify.dev/docs/latest/Reference/Server/#seterrorhandler).
479
479
 
480
480
  ### Payload `stream.filename`
481
481
 
package/index.js CHANGED
@@ -4,9 +4,7 @@ const { PassThrough } = require('node:stream')
4
4
  const path = require('node:path')
5
5
  const { fileURLToPath } = require('node:url')
6
6
  const { statSync } = require('node:fs')
7
- const { promisify } = require('node:util')
8
- const glob = require('glob')
9
- const globPromise = promisify(glob)
7
+ const { glob } = require('glob')
10
8
  const fp = require('fastify-plugin')
11
9
  const send = require('@fastify/send')
12
10
  const encodingNegotiator = require('@fastify/accept-negotiator')
@@ -14,11 +12,7 @@ const contentDisposition = require('content-disposition')
14
12
 
15
13
  const dirList = require('./lib/dirList')
16
14
 
17
- const winSeparatorRegex = new RegExp(`\\${path.win32.sep}`, 'gu')
18
- const backslashRegex = /\\/gu
19
- const startForwardSlashRegex = /^\//u
20
15
  const endForwardSlashRegex = /\/$/u
21
- const doubleForwardSlashRegex = /\/\//gu
22
16
  const asteriskRegex = /\*/gu
23
17
 
24
18
  const supportedEncodings = ['br', 'gzip', 'deflate']
@@ -117,32 +111,30 @@ async function fastifyStatic (fastify, opts) {
117
111
  throw new Error('"wildcard" option must be a boolean')
118
112
  }
119
113
  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) {
114
+ fastify.get(prefix + '*', { ...routeOpts, exposeHeadRoute: true }, (req, reply) => {
124
115
  pumpSendToReply(req, reply, '/' + req.params['*'], sendOptions.root)
125
116
  })
126
117
  if (opts.redirect === true && prefix !== opts.prefix) {
127
- fastify.get(opts.prefix, routeOpts, function (req, reply) {
118
+ fastify.get(opts.prefix, routeOpts, (req, reply) => {
128
119
  reply.redirect(301, getRedirectUrl(req.raw.url))
129
120
  })
130
121
  }
131
122
  } else {
132
- const globPattern = '**/**'
123
+ const indexes = opts.index === undefined ? ['index.html'] : [].concat(opts.index)
133
124
  const indexDirs = new Map()
134
125
  const routes = new Set()
135
126
 
136
127
  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)
128
+ for (let rootPath of roots) {
129
+ rootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
130
+ !rootPath.endsWith('/') && (rootPath += '/')
131
+ const files = await glob('**/**', {
132
+ cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles
133
+ })
141
134
 
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, '/')
135
+ for (let file of files) {
136
+ file = file.split(path.win32.sep).join(path.posix.sep)
137
+ const route = prefix + file
146
138
 
147
139
  if (routes.has(route)) {
148
140
  continue
@@ -150,7 +142,7 @@ async function fastifyStatic (fastify, opts) {
150
142
 
151
143
  routes.add(route)
152
144
 
153
- setUpHeadAndGet(routeOpts, route, '/' + file, rootPath)
145
+ setUpHeadAndGet(routeOpts, route, `/${file}`, rootPath)
154
146
 
155
147
  const key = path.posix.basename(route)
156
148
  if (indexes.includes(key) && !indexDirs.has(key)) {
@@ -179,7 +171,7 @@ async function fastifyStatic (fastify, opts) {
179
171
  pathname,
180
172
  rootPath,
181
173
  rootPathOffset = 0,
182
- pumpOptions = {},
174
+ pumpOptions,
183
175
  checkedEncodings
184
176
  ) {
185
177
  const options = Object.assign({}, sendOptions, pumpOptions)
package/lib/dirList.js CHANGED
@@ -1,10 +1,60 @@
1
1
  'use strict'
2
2
 
3
+ const os = require('node:os')
3
4
  const path = require('node:path')
4
5
  const fs = require('node:fs/promises')
5
- const pLimit = require('p-limit')
6
+ const fastq = require('fastq')
7
+ const fastqConcurrency = Math.max(1, os.cpus().length - 1)
6
8
 
7
9
  const dirList = {
10
+ _getExtendedInfo: async function (dir, info) {
11
+ const depth = dir.split(path.sep).length
12
+ const files = await fs.readdir(dir)
13
+
14
+ const worker = async (filename) => {
15
+ const filePath = path.join(dir, filename)
16
+ let stats
17
+ try {
18
+ stats = await fs.stat(filePath)
19
+ } catch {
20
+ return
21
+ }
22
+
23
+ if (stats.isDirectory()) {
24
+ info.totalFolderCount++
25
+ filePath.split(path.sep).length === depth + 1 && info.folderCount++
26
+ await dirList._getExtendedInfo(filePath, info)
27
+ } else {
28
+ info.totalSize += stats.size
29
+ info.totalFileCount++
30
+ filePath.split(path.sep).length === depth + 1 && info.fileCount++
31
+ info.lastModified = Math.max(info.lastModified, stats.mtimeMs)
32
+ }
33
+ }
34
+ const queue = fastq.promise(worker, fastqConcurrency)
35
+ await Promise.all(files.map(filename => queue.push(filename)))
36
+ },
37
+
38
+ /**
39
+ * get extended info about a folder
40
+ * @param {string} folderPath full path fs dir
41
+ * @return {Promise<ExtendedInfo>}
42
+ */
43
+ getExtendedInfo: async function (folderPath) {
44
+ const info = {
45
+ totalSize: 0,
46
+ fileCount: 0,
47
+ totalFileCount: 0,
48
+ folderCount: 0,
49
+ totalFolderCount: 0,
50
+ lastModified: 0
51
+ }
52
+
53
+ await dirList._getExtendedInfo(folderPath, info)
54
+
55
+ return info
56
+ },
57
+
8
58
  /**
9
59
  * get files and dirs from dir, or error
10
60
  * @param {string} dir full path fs dir
@@ -22,8 +72,7 @@ const dirList = {
22
72
  return entries
23
73
  }
24
74
 
25
- const limit = pLimit(4)
26
- await Promise.all(files.map(filename => limit(async () => {
75
+ const worker = async (filename) => {
27
76
  let stats
28
77
  try {
29
78
  stats = await fs.stat(path.join(dir, filename))
@@ -33,62 +82,15 @@ const dirList = {
33
82
  const entry = { name: filename, stats }
34
83
  if (stats.isDirectory()) {
35
84
  if (options.extendedFolderInfo) {
36
- entry.extendedInfo = await getExtendedInfo(path.join(dir, filename))
85
+ entry.extendedInfo = await dirList.getExtendedInfo(path.join(dir, filename))
37
86
  }
38
87
  entries.dirs.push(entry)
39
88
  } else {
40
89
  entries.files.push(entry)
41
90
  }
42
- })))
43
-
44
- async function getExtendedInfo (folderPath) {
45
- const depth = folderPath.split(path.sep).length
46
- let totalSize = 0
47
- let fileCount = 0
48
- let totalFileCount = 0
49
- let folderCount = 0
50
- let totalFolderCount = 0
51
- let lastModified = 0
52
-
53
- async function walk (dir) {
54
- const files = await fs.readdir(dir)
55
- const limit = pLimit(4)
56
- await Promise.all(files.map(filename => limit(async () => {
57
- const filePath = path.join(dir, filename)
58
- let stats
59
- try {
60
- stats = await fs.stat(filePath)
61
- } catch {
62
- return
63
- }
64
-
65
- if (stats.isDirectory()) {
66
- totalFolderCount++
67
- if (filePath.split(path.sep).length === depth + 1) {
68
- folderCount++
69
- }
70
- await walk(filePath)
71
- } else {
72
- totalSize += stats.size
73
- totalFileCount++
74
- if (filePath.split(path.sep).length === depth + 1) {
75
- fileCount++
76
- }
77
- lastModified = Math.max(lastModified, stats.mtimeMs)
78
- }
79
- })))
80
- }
81
-
82
- await walk(folderPath)
83
- return {
84
- totalSize,
85
- fileCount,
86
- totalFileCount,
87
- folderCount,
88
- totalFolderCount,
89
- lastModified
90
- }
91
91
  }
92
+ const queue = fastq.promise(worker, fastqConcurrency)
93
+ await Promise.all(files.map(filename => queue.push(filename)))
92
94
 
93
95
  entries.dirs.sort((a, b) => a.name.localeCompare(b.name))
94
96
  entries.files.sort((a, b) => a.name.localeCompare(b.name))
@@ -115,6 +117,7 @@ const dirList = {
115
117
  } catch {
116
118
  return reply.callNotFound()
117
119
  }
120
+
118
121
  const format = reply.request.query.format || options.format
119
122
  if (format !== 'html') {
120
123
  if (options.jsonFormat !== 'extended') {
@@ -203,7 +206,6 @@ const dirList = {
203
206
  return new TypeError('The `list.render` option must be a function and is required with html format')
204
207
  }
205
208
  }
206
-
207
209
  }
208
210
 
209
211
  module.exports = dirList
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "6.12.0",
3
+ "version": "7.0.1",
4
4
  "description": "Plugin for serving static files as fast as possible.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/fastify/fastify-static.git"
21
+ "url": "git+https://github.com/fastify/fastify-static.git"
22
22
  },
23
23
  "keywords": [
24
24
  "fastify",
@@ -35,11 +35,11 @@
35
35
  "@fastify/send": "^2.0.0",
36
36
  "content-disposition": "^0.5.3",
37
37
  "fastify-plugin": "^4.0.0",
38
- "glob": "^8.0.1",
39
- "p-limit": "^3.1.0"
38
+ "fastq": "^1.17.0",
39
+ "glob": "^10.3.4"
40
40
  },
41
41
  "devDependencies": {
42
- "@fastify/compress": "^6.0.0",
42
+ "@fastify/compress": "^7.0.0",
43
43
  "@fastify/pre-commit": "^2.0.2",
44
44
  "@types/node": "^20.1.0",
45
45
  "@typescript-eslint/eslint-plugin": "^6.3.0",
@@ -55,7 +55,7 @@
55
55
  "snazzy": "^9.0.0",
56
56
  "standard": "^17.0.0",
57
57
  "tap": "^16.0.0",
58
- "tsd": "^0.29.0",
58
+ "tsd": "^0.30.0",
59
59
  "typescript": "^5.1.6"
60
60
  },
61
61
  "tsd": {
@@ -2012,6 +2012,96 @@ t.test('register with wildcard false', t => {
2012
2012
  })
2013
2013
  })
2014
2014
 
2015
+ t.test('register with wildcard false (trailing slash in the root)', t => {
2016
+ t.plan(6)
2017
+
2018
+ const pluginOptions = {
2019
+ root: path.join(__dirname, '/static/'),
2020
+ prefix: '/assets/',
2021
+ index: false,
2022
+ wildcard: false
2023
+ }
2024
+ const fastify = Fastify({
2025
+ ignoreTrailingSlash: true
2026
+ })
2027
+ fastify.register(fastifyStatic, pluginOptions)
2028
+
2029
+ fastify.get('/*', (request, reply) => {
2030
+ reply.send({ hello: 'world' })
2031
+ })
2032
+
2033
+ t.teardown(fastify.close.bind(fastify))
2034
+
2035
+ fastify.listen({ port: 0 }, (err) => {
2036
+ t.error(err)
2037
+
2038
+ fastify.server.unref()
2039
+
2040
+ t.test('/index.css', (t) => {
2041
+ t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT)
2042
+ simple.concat({
2043
+ method: 'GET',
2044
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/index.css'
2045
+ }, (err, response, body) => {
2046
+ t.error(err)
2047
+ t.equal(response.statusCode, 200)
2048
+ genericResponseChecks(t, response)
2049
+ })
2050
+ })
2051
+
2052
+ t.test('/not-defined', (t) => {
2053
+ t.plan(3)
2054
+ simple.concat({
2055
+ method: 'GET',
2056
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/not-defined'
2057
+ }, (err, response, body) => {
2058
+ t.error(err)
2059
+ t.equal(response.statusCode, 200)
2060
+ t.same(JSON.parse(body), { hello: 'world' })
2061
+ })
2062
+ })
2063
+
2064
+ t.test('/deep/path/for/test/purpose/foo.html', (t) => {
2065
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
2066
+ simple.concat({
2067
+ method: 'GET',
2068
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/deep/path/for/test/purpose/foo.html'
2069
+ }, (err, response, body) => {
2070
+ t.error(err)
2071
+ t.equal(response.statusCode, 200)
2072
+ t.equal(body.toString(), deepContent)
2073
+ genericResponseChecks(t, response)
2074
+ })
2075
+ })
2076
+
2077
+ t.test('/../index.js', (t) => {
2078
+ t.plan(3)
2079
+ simple.concat({
2080
+ method: 'GET',
2081
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/../index.js',
2082
+ followRedirect: false
2083
+ }, (err, response, body) => {
2084
+ t.error(err)
2085
+ t.equal(response.statusCode, 200)
2086
+ t.same(JSON.parse(body), { hello: 'world' })
2087
+ })
2088
+ })
2089
+
2090
+ t.test('/index.css', t => {
2091
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
2092
+ simple.concat({
2093
+ method: 'HEAD',
2094
+ url: 'http://localhost:' + fastify.server.address().port + '/assets/index.css'
2095
+ }, (err, response, body) => {
2096
+ t.error(err)
2097
+ t.equal(response.statusCode, 200)
2098
+ t.equal(body.toString(), '')
2099
+ genericResponseChecks(t, response)
2100
+ })
2101
+ })
2102
+ })
2103
+ })
2104
+
2015
2105
  t.test('register with wildcard string', (t) => {
2016
2106
  t.plan(1)
2017
2107
 
package/types/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Leo <https://github.com/leomelzer>
3
3
  /// <reference types="node" />
4
4
 
5
- import { FastifyPluginAsync, FastifyRequest, RouteOptions } from 'fastify'
5
+ import { FastifyPluginAsync, FastifyReply, FastifyRequest, RouteOptions } from 'fastify'
6
6
  import { Stats } from 'fs'
7
7
 
8
8
  declare module 'fastify' {
@@ -19,6 +19,13 @@ declare module 'fastify' {
19
19
  type FastifyStaticPlugin = FastifyPluginAsync<NonNullable<fastifyStatic.FastifyStaticOptions>>;
20
20
 
21
21
  declare namespace fastifyStatic {
22
+ export interface SetHeadersResponse {
23
+ getHeader: FastifyReply['getHeader'];
24
+ setHeader: FastifyReply['header'];
25
+ readonly filename: string;
26
+ statusCode: number;
27
+ }
28
+
22
29
  export interface ExtendedInformation {
23
30
  fileCount: number;
24
31
  totalFileCount: number;
@@ -83,7 +90,7 @@ declare namespace fastifyStatic {
83
90
  serve?: boolean;
84
91
  decorateReply?: boolean;
85
92
  schemaHide?: boolean;
86
- setHeaders?: (...args: any[]) => void;
93
+ setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
87
94
  redirect?: boolean;
88
95
  wildcard?: boolean;
89
96
  list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
@@ -1,5 +1,6 @@
1
- import fastify, { FastifyInstance, FastifyPluginAsync, FastifyRequest } from 'fastify'
1
+ import fastify, { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'
2
2
  import { Server } from 'http'
3
+ import { Stats } from 'fs'
3
4
  import { expectAssignable, expectError, expectType } from 'tsd'
4
5
  import * as fastifyStaticStar from '..'
5
6
  import fastifyStatic, {
@@ -49,8 +50,15 @@ const options: FastifyStaticOptions = {
49
50
  serve: true,
50
51
  wildcard: true,
51
52
  list: false,
52
- setHeaders: (res: any, pathName: any) => {
53
- res.setHeader('test', pathName)
53
+ setHeaders: (res, path, stat) => {
54
+ expectType<string>(res.filename)
55
+ expectType<number>(res.statusCode)
56
+ expectType<ReturnType<FastifyReply['getHeader']>>(res.getHeader('X-Test'))
57
+ res.setHeader('X-Test', 'string')
58
+
59
+ expectType<string>(path)
60
+
61
+ expectType<Stats>(stat)
54
62
  },
55
63
  preCompressed: false,
56
64
  allowedPath: (pathName: string, root: string, request: FastifyRequest) => {