@fastify/static 8.1.1 → 8.3.0

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.
@@ -14,8 +14,19 @@ on:
14
14
  - 'docs/**'
15
15
  - '*.md'
16
16
 
17
+ # This allows a subsequently queued workflow run to interrupt previous runs
18
+ concurrency:
19
+ group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
20
+ cancel-in-progress: true
21
+
22
+ permissions:
23
+ contents: read
24
+
17
25
  jobs:
18
26
  test:
27
+ permissions:
28
+ contents: write
29
+ pull-requests: write
19
30
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
20
31
  with:
21
32
  license-check: true
package/LICENSE CHANGED
@@ -1,6 +1,8 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2017-2023 Fastify
3
+ Copyright (c) 2017-present The Fastify team
4
+
5
+ The Fastify team members are listed at https://github.com/fastify/fastify#team.
4
6
 
5
7
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
8
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -15,10 +15,10 @@ npm i @fastify/static
15
15
 
16
16
  | Plugin version | Fastify version |
17
17
  | ---------------|-----------------|
18
- | `^8.x` | `^5.x` |
19
- | `^7.x` | `^4.x` |
20
- | `^5.x` | `^3.x` |
21
- | `^2.x` | `^2.x` |
18
+ | `>=8.x` | `^5.x` |
19
+ | `>=7.x <8.x` | `^4.x` |
20
+ | `>=5.x <7.x` | `^3.x` |
21
+ | `>=2.x <5.x` | `^2.x` |
22
22
  | `^1.x` | `^1.x` |
23
23
 
24
24
 
@@ -111,9 +111,39 @@ fastify.get('/path/without/cache/control', function (req, reply) {
111
111
 
112
112
  ```
113
113
 
114
+ ### Managing cache-control headers
115
+
116
+ Production sites should use a reverse-proxy to manage caching headers.
117
+ However, here is an example of using fastify-static to host a Single Page Application (for example a [vite.js](https://vite.dev/) build) with sane caching.
118
+
119
+ ```js
120
+ fastify.register(require('@fastify/static'), {
121
+ root: path.join(import.meta.dirname, 'dist'), // import.meta.dirname node.js >= v20.11.0
122
+ // By default all assets are immutable and can be cached for a long period due to cache bursting techniques
123
+ maxAge: '30d',
124
+ immutable: true,
125
+ })
126
+
127
+ // Explicitly reduce caching of assets that don't use cache bursting techniques
128
+ fastify.get('/', function (req, reply) {
129
+ // index.html should never be cached
130
+ reply.sendFile('index.html', {maxAge: 0, immutable: false})
131
+ })
132
+
133
+ fastify.get('/favicon.ico', function (req, reply) {
134
+ // favicon can be cached for a short period
135
+ reply.sendFile('favicon.ico', {maxAge: '1d', immutable: false})
136
+ })
137
+ ```
138
+
114
139
  ### Options
115
140
 
116
- #### `root` (required)
141
+ #### `serve`
142
+ Default: `true`
143
+
144
+ If set to `false`, the plugin will not serve files from the `root` directory.
145
+
146
+ #### `root` (required if `serve` is not false)
117
147
 
118
148
  The absolute path of the directory containing the files to serve.
119
149
  The file to serve is determined by combining `req.url` with the
@@ -208,6 +238,14 @@ are applied for getting the file list.
208
238
 
209
239
  This option cannot be `false` if `redirect` is `true` and `ignoreTrailingSlash` is `true`.
210
240
 
241
+ #### `globIgnore`
242
+
243
+ Default: `undefined`
244
+
245
+ This is passed to [`glob`](https://www.npmjs.com/package/glob)
246
+ as the `ignore` option. It can be used to ignore files or directories
247
+ when using the `wildcard: false` option.
248
+
211
249
  #### `allowedPath`
212
250
 
213
251
  Default: `(pathName, root, request) => true`
@@ -245,7 +283,7 @@ Example:
245
283
  fastify.register(require('@fastify/static'), {
246
284
  root: path.join(__dirname, 'public'),
247
285
  prefix: '/public/',
248
- index: false
286
+ index: false,
249
287
  list: true
250
288
  })
251
289
  ```
@@ -331,7 +369,7 @@ Default: `['']`
331
369
 
332
370
  Directory list can respond to different routes declared in `list.names`.
333
371
 
334
- > 🛈 Note: If a file with the same name exists, the actual file is sent.
372
+ > â„šī¸ Note: If a file with the same name exists, the actual file is sent.
335
373
 
336
374
  Example:
337
375
 
package/index.js CHANGED
@@ -16,10 +16,17 @@ const asteriskRegex = /\*/gu
16
16
 
17
17
  const supportedEncodings = ['br', 'gzip', 'deflate']
18
18
  send.mime.default_type = 'application/octet-stream'
19
+ const encodingExtensionMap = {
20
+ br: '.br',
21
+ gzip: '.gz'
22
+ }
19
23
 
24
+ /** @type {import("fastify").FastifyPluginAsync<import("./types").FastifyStaticOptions>} */
20
25
  async function fastifyStatic (fastify, opts) {
21
- opts.root = normalizeRoot(opts.root)
22
- checkRootPathForErrors(fastify, opts.root)
26
+ if (opts.serve !== false || opts.root !== undefined) {
27
+ opts.root = normalizeRoot(opts.root)
28
+ checkRootPathForErrors(fastify, opts.root)
29
+ }
23
30
 
24
31
  const setHeaders = opts.setHeaders
25
32
  if (setHeaders !== undefined && typeof setHeaders !== 'function') {
@@ -31,9 +38,7 @@ async function fastifyStatic (fastify, opts) {
31
38
  throw invalidDirListOpts
32
39
  }
33
40
 
34
- if (opts.dotfiles === undefined) {
35
- opts.dotfiles = 'allow'
36
- }
41
+ opts.dotfiles ??= 'allow'
37
42
 
38
43
  const sendOptions = {
39
44
  root: opts.root,
@@ -49,7 +54,7 @@ async function fastifyStatic (fastify, opts) {
49
54
  maxAge: opts.maxAge
50
55
  }
51
56
 
52
- let prefix = opts.prefix ?? (opts.prefix = '/')
57
+ let prefix = opts.prefix ??= '/'
53
58
 
54
59
  if (!opts.prefixAvoidTrailingSlash) {
55
60
  prefix =
@@ -62,7 +67,7 @@ async function fastifyStatic (fastify, opts) {
62
67
  const routeOpts = {
63
68
  constraints: opts.constraints,
64
69
  schema: {
65
- hide: opts.schemaHide !== undefined ? opts.schemaHide : true
70
+ hide: opts.schemaHide ?? true
66
71
  },
67
72
  logLevel: opts.logLevel,
68
73
  errorHandler (error, request, reply) {
@@ -126,7 +131,7 @@ async function fastifyStatic (fastify, opts) {
126
131
  })
127
132
  }
128
133
  } else {
129
- const indexes = opts.index === undefined ? ['index.html'] : [].concat(opts.index)
134
+ const indexes = new Set(opts.index === undefined ? ['index.html'] : [].concat(opts.index))
130
135
  const indexDirs = new Map()
131
136
  const routes = new Set()
132
137
 
@@ -135,7 +140,7 @@ async function fastifyStatic (fastify, opts) {
135
140
  rootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
136
141
  !rootPath.endsWith('/') && (rootPath += '/')
137
142
  const files = await glob('**/**', {
138
- cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles
143
+ cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles, ignore: opts.globIgnore
139
144
  })
140
145
 
141
146
  for (let file of files) {
@@ -151,7 +156,7 @@ async function fastifyStatic (fastify, opts) {
151
156
  setUpHeadAndGet(routeOpts, route, `/${file}`, rootPath)
152
157
 
153
158
  const key = path.posix.basename(route)
154
- if (indexes.includes(key) && !indexDirs.has(key)) {
159
+ if (indexes.has(key) && !indexDirs.has(key)) {
155
160
  indexDirs.set(path.posix.dirname(route), rootPath)
156
161
  }
157
162
  }
@@ -171,6 +176,15 @@ async function fastifyStatic (fastify, opts) {
171
176
 
172
177
  const allowedPath = opts.allowedPath
173
178
 
179
+ /**
180
+ * @param {import("fastify").FastifyRequest} request
181
+ * @param {import("fastify").FastifyReply} reply
182
+ * @param {string} pathname
183
+ * @param {import("./types").FastifyStaticOptions['root']} rootPath
184
+ * @param {number} [rootPathOffset]
185
+ * @param {import("@fastify/send").SendOptions} [pumpOptions]
186
+ * @param {Set<string>} [checkedEncodings]
187
+ */
174
188
  async function pumpSendToReply (
175
189
  request,
176
190
  reply,
@@ -189,6 +203,8 @@ async function fastifyStatic (fastify, opts) {
189
203
  } else {
190
204
  options.root = rootPath
191
205
  }
206
+ } else if (path.isAbsolute(pathname) === false) {
207
+ return reply.callNotFound()
192
208
  }
193
209
 
194
210
  if (allowedPath && !allowedPath(pathname, options.root, request)) {
@@ -203,9 +219,7 @@ async function fastifyStatic (fastify, opts) {
203
219
  * We conditionally create this structure to track our attempts
204
220
  * at sending pre-compressed assets
205
221
  */
206
- if (!checkedEncodings) {
207
- checkedEncodings = new Set()
208
- }
222
+ checkedEncodings ??= new Set()
209
223
 
210
224
  encoding = getEncodingHeader(request.headers, checkedEncodings)
211
225
 
@@ -215,9 +229,9 @@ async function fastifyStatic (fastify, opts) {
215
229
  if (!pathname) {
216
230
  return reply.callNotFound()
217
231
  }
218
- pathnameForSend = pathnameForSend + pathname + '.' + getEncodingExtension(encoding)
232
+ pathnameForSend = pathnameForSend + pathname + encodingExtensionMap[encoding]
219
233
  } else {
220
- pathnameForSend = pathname + '.' + getEncodingExtension(encoding)
234
+ pathnameForSend = pathname + encodingExtensionMap[encoding]
221
235
  }
222
236
  }
223
237
  }
@@ -284,6 +298,7 @@ async function fastifyStatic (fastify, opts) {
284
298
  prefix,
285
299
  dotfiles: opts.dotfiles
286
300
  }).catch((err) => reply.send(err))
301
+ return
287
302
  }
288
303
  }
289
304
 
@@ -359,9 +374,7 @@ async function fastifyStatic (fastify, opts) {
359
374
  // otherwise use send provided status code
360
375
  const newStatusCode = reply.statusCode !== 200 ? reply.statusCode : statusCode
361
376
  reply.code(newStatusCode)
362
- if (setHeaders !== undefined) {
363
- setHeaders(reply.raw, metadata.path, metadata.stat)
364
- }
377
+ setHeaders?.(reply.raw, metadata.path, metadata.stat)
365
378
  reply.headers(headers)
366
379
  if (encoding) {
367
380
  reply.header('content-type', getContentType(pathname))
@@ -379,20 +392,23 @@ async function fastifyStatic (fastify, opts) {
379
392
  url: route,
380
393
  handler: serveFileHandler
381
394
  })
382
- toSetUp.config = toSetUp.config || {}
395
+ toSetUp.config ??= {}
383
396
  toSetUp.config.file = file
384
397
  toSetUp.config.rootPath = rootPath
385
398
  fastify.route(toSetUp)
386
399
  }
387
400
 
401
+ /** @type {import("fastify").RouteHandlerMethod} */
388
402
  async function serveFileHandler (req, reply) {
389
- // TODO: remove the fallback branch when bump major
390
- /* c8 ignore next */
391
- const routeConfig = req.routeOptions?.config || req.routeConfig
403
+ const routeConfig = req.routeOptions?.config
392
404
  return pumpSendToReply(req, reply, routeConfig.file, routeConfig.rootPath)
393
405
  }
394
406
  }
395
407
 
408
+ /**
409
+ * @param {import("./types").FastifyStaticOptions['root']} root
410
+ * @returns {import("./types").FastifyStaticOptions['root']}
411
+ */
396
412
  function normalizeRoot (root) {
397
413
  if (root === undefined) {
398
414
  return root
@@ -416,6 +432,11 @@ function normalizeRoot (root) {
416
432
  return root
417
433
  }
418
434
 
435
+ /**
436
+ * @param {import("fastify").FastifyInstance} fastify
437
+ * @param {import("./types").FastifyStaticOptions['root']} rootPath
438
+ * @returns {void}
439
+ */
419
440
  function checkRootPathForErrors (fastify, rootPath) {
420
441
  if (rootPath === undefined) {
421
442
  throw new Error('"root" option is required')
@@ -444,6 +465,11 @@ function checkRootPathForErrors (fastify, rootPath) {
444
465
  throw new Error('"root" option must be a string or array of strings')
445
466
  }
446
467
 
468
+ /**
469
+ * @param {import("fastify").FastifyInstance} fastify
470
+ * @param {import("./types").FastifyStaticOptions['root']} rootPath
471
+ * @returns {void}
472
+ */
447
473
  function checkPath (fastify, rootPath) {
448
474
  if (typeof rootPath !== 'string') {
449
475
  throw new TypeError('"root" option must be a string')
@@ -470,6 +496,10 @@ function checkPath (fastify, rootPath) {
470
496
  }
471
497
  }
472
498
 
499
+ /**
500
+ * @param {string} path
501
+ * @return {string}
502
+ */
473
503
  function getContentType (path) {
474
504
  const type = send.mime.getType(path) || send.mime.default_type
475
505
 
@@ -479,6 +509,12 @@ function getContentType (path) {
479
509
  return `${type}; charset=utf-8`
480
510
  }
481
511
 
512
+ /**
513
+ * @param {string} pathname
514
+ * @param {*} root
515
+ * @param {import("./types").FastifyStaticOptions['index']} [indexFiles]
516
+ * @return {string|boolean}
517
+ */
482
518
  function findIndexFile (pathname, root, indexFiles = ['index.html']) {
483
519
  if (Array.isArray(indexFiles)) {
484
520
  return indexFiles.find(filename => {
@@ -495,7 +531,11 @@ function findIndexFile (pathname, root, indexFiles = ['index.html']) {
495
531
  return false
496
532
  }
497
533
 
498
- // Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
534
+ /**
535
+ * Adapted from https://github.com/fastify/fastify-compress/blob/665e132fa63d3bf05ad37df3c20346660b71a857/index.js#L451
536
+ * @param {import('fastify').FastifyRequest['headers']} headers
537
+ * @param {Set<string>} checked
538
+ */
499
539
  function getEncodingHeader (headers, checked) {
500
540
  if (!('accept-encoding' in headers)) return
501
541
 
@@ -508,24 +548,18 @@ function getEncodingHeader (headers, checked) {
508
548
  )
509
549
  }
510
550
 
511
- function getEncodingExtension (encoding) {
512
- switch (encoding) {
513
- case 'br':
514
- return 'br'
515
-
516
- case 'gzip':
517
- return 'gz'
518
- }
519
- }
520
-
551
+ /**
552
+ * @param {string} url
553
+ * @return {string}
554
+ */
521
555
  function getRedirectUrl (url) {
522
556
  let i = 0
523
557
  // we detect how many slash before a valid path
524
- for (; i < url.length; ++i) {
558
+ for (const ul = url.length; i < ul; ++i) {
525
559
  if (url[i] !== '/' && url[i] !== '\\') break
526
560
  }
527
561
  // turns all leading / or \ into a single /
528
- url = '/' + url.substr(i)
562
+ url = '/' + url.slice(i)
529
563
  try {
530
564
  const parsed = new URL(url, 'http://localhost.com/')
531
565
  const parsedPathname = parsed.pathname
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "8.1.1",
3
+ "version": "8.3.0",
4
4
  "description": "Plugin for serving static files as fast as possible.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "dependencies": {
61
61
  "@fastify/accept-negotiator": "^2.0.0",
62
- "@fastify/send": "^3.2.0",
62
+ "@fastify/send": "^4.0.0",
63
63
  "content-disposition": "^0.5.4",
64
64
  "fastify-plugin": "^5.0.0",
65
65
  "fastq": "^1.17.1",
@@ -67,9 +67,8 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@fastify/compress": "^8.0.0",
70
- "@fastify/pre-commit": "^2.1.0",
71
- "@types/node": "^22.0.0",
72
- "borp": "^0.19.0",
70
+ "@types/node": "^24.0.13",
71
+ "borp": "^0.20.0",
73
72
  "c8": "^10.1.3",
74
73
  "concat-stream": "^2.0.0",
75
74
  "eslint": "^9.17.0",
@@ -77,7 +76,7 @@
77
76
  "neostandard": "^0.12.0",
78
77
  "pino": "^9.1.0",
79
78
  "proxyquire": "^2.1.3",
80
- "tsd": "^0.31.0"
79
+ "tsd": "^0.33.0"
81
80
  },
82
81
  "tsd": {
83
82
  "directory": "test/types"
@@ -0,0 +1 @@
1
+ bar
@@ -0,0 +1,3 @@
1
+ <html>
2
+ <body>baz</body>
3
+ </html>
@@ -0,0 +1,3 @@
1
+ <html>
2
+ <body>index2</body>
3
+ </html>
@@ -753,6 +753,51 @@ test('serving disabled', async (t) => {
753
753
  })
754
754
  })
755
755
 
756
+ test('serving disabled without root', async (t) => {
757
+ t.plan(2)
758
+
759
+ const pluginOptions = {
760
+ prefix: '/static/',
761
+ serve: false
762
+ }
763
+ const fastify = Fastify()
764
+ fastify.register(fastifyStatic, pluginOptions)
765
+
766
+ t.after(() => fastify.close())
767
+
768
+ fastify.get('/foo/bar/r', (_request, reply) => {
769
+ reply.sendFile('index.html')
770
+ })
771
+
772
+ fastify.get('/foo/bar/a', (_request, reply) => {
773
+ reply.sendFile(path.join(__dirname, pluginOptions.prefix, 'index.html'))
774
+ })
775
+
776
+ t.after(() => fastify.close())
777
+
778
+ await fastify.listen({ port: 0 })
779
+
780
+ fastify.server.unref()
781
+
782
+ await t.test('/static/index.html via sendFile not found', async (t) => {
783
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
784
+
785
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/a')
786
+ t.assert.ok(response.ok)
787
+ t.assert.deepStrictEqual(response.status, 200)
788
+ t.assert.deepStrictEqual(await response.text(), indexContent)
789
+ genericResponseChecks(t, response)
790
+ })
791
+
792
+ await t.test('/static/index.html via sendFile with relative path not found', async (t) => {
793
+ t.plan(2)
794
+
795
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/r')
796
+ t.assert.ok(!response.ok)
797
+ t.assert.deepStrictEqual(response.status, 404)
798
+ })
799
+ })
800
+
756
801
  test('sendFile', async (t) => {
757
802
  t.plan(4)
758
803
 
@@ -1215,7 +1260,7 @@ test('maxAge option', async (t) => {
1215
1260
  })
1216
1261
 
1217
1262
  test('errors', async (t) => {
1218
- t.plan(11)
1263
+ t.plan(12)
1219
1264
 
1220
1265
  await t.test('no root', async (t) => {
1221
1266
  t.plan(1)
@@ -1280,6 +1325,16 @@ test('errors', async (t) => {
1280
1325
  await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions))
1281
1326
  })
1282
1327
 
1328
+ await t.test('no root and serve: false', async (t) => {
1329
+ t.plan(1)
1330
+ const pluginOptions = {
1331
+ serve: false,
1332
+ root: []
1333
+ }
1334
+ const fastify = Fastify({ logger: false })
1335
+ await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions))
1336
+ })
1337
+
1283
1338
  await t.test('duplicate root paths are not allowed', async (t) => {
1284
1339
  t.plan(1)
1285
1340
  const pluginOptions = {
@@ -1936,8 +1991,6 @@ test('register /static with wildcard false and alternative index', async t => {
1936
1991
 
1937
1992
  const { promise, resolve } = Promise.withResolvers()
1938
1993
 
1939
- // simple-get doesn't tell us about redirects so use http.request directly
1940
- // to verify we do not get a redirect when not requested
1941
1994
  const testurl = 'http://localhost:' + fastify.server.address().port + '/static'
1942
1995
  const req = http.request(url.parse(testurl), res => {
1943
1996
  t.assert.deepStrictEqual(res.statusCode, 200)
@@ -2042,7 +2095,6 @@ test('register /static with redirect true', async t => {
2042
2095
 
2043
2096
  const { promise, resolve } = Promise.withResolvers()
2044
2097
 
2045
- // simple-get doesn't tell us about redirects so use http.request directly
2046
2098
  const testurl = 'http://localhost:' + fastify.server.address().port + '/static?a=b'
2047
2099
  const req = http.request(url.parse(testurl), res => {
2048
2100
  t.assert.deepStrictEqual(res.statusCode, 301)
@@ -2066,7 +2118,6 @@ test('register /static with redirect true', async t => {
2066
2118
 
2067
2119
  const { promise, resolve } = Promise.withResolvers()
2068
2120
 
2069
- // simple-get doesn't tell us about redirects so use http.request directly
2070
2121
  const testurl = 'http://localhost:' + fastify.server.address().port + '/static'
2071
2122
  const req = http.request(url.parse(testurl), res => {
2072
2123
  t.assert.deepStrictEqual(res.statusCode, 301)
@@ -2104,7 +2155,6 @@ test('register /static with redirect true', async t => {
2104
2155
 
2105
2156
  const { promise, resolve } = Promise.withResolvers()
2106
2157
 
2107
- // simple-get doesn't tell us about redirects so use http.request directly
2108
2158
  const testurl = 'http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test?a=b'
2109
2159
  const req = http.request(url.parse(testurl), res => {
2110
2160
  t.assert.deepStrictEqual(res.statusCode, 301)
@@ -2161,7 +2211,6 @@ test('register /static with redirect true and wildcard false', async t => {
2161
2211
 
2162
2212
  const { promise, resolve } = Promise.withResolvers()
2163
2213
 
2164
- // simple-get doesn't tell us about redirects so use http.request directly
2165
2214
  const testurl = 'http://localhost:' + fastify.server.address().port + '/static?a=b'
2166
2215
  const req = http.request(url.parse(testurl), res => {
2167
2216
  t.assert.deepStrictEqual(res.statusCode, 301)
@@ -2213,7 +2262,6 @@ test('register /static with redirect true and wildcard false', async t => {
2213
2262
 
2214
2263
  const { promise, resolve } = Promise.withResolvers()
2215
2264
 
2216
- // simple-get doesn't tell us about redirects so use http.request directly
2217
2265
  const testurl = 'http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test?a=b'
2218
2266
  const req = http.request(url.parse(testurl), res => {
2219
2267
  t.assert.deepStrictEqual(res.statusCode, 301)
@@ -3669,3 +3717,77 @@ test('register /static/ with custom log level', async t => {
3669
3717
  t.assert.deepStrictEqual(response2.status, 304)
3670
3718
  })
3671
3719
  })
3720
+
3721
+ test('register with wildcard false and globIgnore', async t => {
3722
+ t.plan(5)
3723
+
3724
+ const indexContent = fs
3725
+ .readFileSync('./test/static-filtered/index.html')
3726
+ .toString('utf8')
3727
+
3728
+ const deepContent = fs
3729
+ .readFileSync('./test/static-filtered/deep/path/to/baz.html')
3730
+ .toString('utf8')
3731
+
3732
+ const pluginOptions = {
3733
+ root: path.join(__dirname, '/static-filtered'),
3734
+ wildcard: false,
3735
+ globIgnore: ['**/*.private']
3736
+ }
3737
+ const fastify = Fastify()
3738
+ fastify.register(fastifyStatic, pluginOptions)
3739
+
3740
+ t.after(() => fastify.close())
3741
+
3742
+ await fastify.listen({ port: 0 })
3743
+
3744
+ fastify.server.unref()
3745
+
3746
+ await t.test('/index.html', async t => {
3747
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
3748
+
3749
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html')
3750
+ t.assert.ok(response.ok)
3751
+ t.assert.deepStrictEqual(response.status, 200)
3752
+ t.assert.deepStrictEqual(await response.text(), indexContent)
3753
+ genericResponseChecks(t, response)
3754
+ })
3755
+
3756
+ await t.test('/bar.private', async t => {
3757
+ t.plan(2)
3758
+
3759
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/bar.private')
3760
+ t.assert.ok(!response.ok)
3761
+ t.assert.deepStrictEqual(response.status, 404)
3762
+ await response.text()
3763
+ })
3764
+
3765
+ await t.test('/', async (t) => {
3766
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
3767
+
3768
+ const response = await fetch('http://localhost:' + fastify.server.address().port)
3769
+ t.assert.ok(response.ok)
3770
+ t.assert.deepStrictEqual(response.status, 200)
3771
+ t.assert.deepStrictEqual(await response.text(), indexContent)
3772
+ genericResponseChecks(t, response)
3773
+ })
3774
+
3775
+ await t.test('/deep/path/to/baz.html', async (t) => {
3776
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
3777
+
3778
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/deep/path/to/baz.html')
3779
+ t.assert.ok(response.ok)
3780
+ t.assert.deepStrictEqual(response.status, 200)
3781
+ t.assert.deepStrictEqual(await response.text(), deepContent)
3782
+ genericResponseChecks(t, response)
3783
+ })
3784
+
3785
+ await t.test('/deep/path/to/baz.private', async (t) => {
3786
+ t.plan(2)
3787
+
3788
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/deep/path/to/baz.private')
3789
+ t.assert.ok(!response.ok)
3790
+ t.assert.deepStrictEqual(response.status, 404)
3791
+ await response.text()
3792
+ })
3793
+ })
package/types/index.d.ts CHANGED
@@ -84,39 +84,52 @@ declare namespace fastifyStatic {
84
84
  serveDotFiles?: boolean;
85
85
  }
86
86
 
87
- export interface FastifyStaticOptions extends SendOptions {
88
- root: string | string[] | URL | URL[];
89
- prefix?: string;
90
- prefixAvoidTrailingSlash?: boolean;
91
- serve?: boolean;
92
- decorateReply?: boolean;
93
- schemaHide?: boolean;
94
- setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
95
- redirect?: boolean;
96
- wildcard?: boolean;
97
- list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
98
- allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
99
- /**
100
- * @description
101
- * Opt-in to looking for pre-compressed files
102
- */
103
- preCompressed?: boolean;
104
-
105
- // Passed on to `send`
106
- acceptRanges?: boolean;
107
- contentType?: boolean;
108
- cacheControl?: boolean;
109
- dotfiles?: 'allow' | 'deny' | 'ignore';
110
- etag?: boolean;
111
- extensions?: string[];
112
- immutable?: boolean;
113
- index?: string[] | string | false;
114
- lastModified?: boolean;
115
- maxAge?: string | number;
116
- constraints?: RouteOptions['constraints'];
117
- logLevel?: RouteOptions['logLevel'];
87
+ type Root = string | string[] | URL | URL[]
88
+
89
+ type RootOptions = {
90
+ serve: true;
91
+ root: Root;
92
+ } | {
93
+ serve?: false;
94
+ root?: Root;
118
95
  }
119
96
 
97
+ export type FastifyStaticOptions =
98
+ SendOptions
99
+ & RootOptions
100
+ & {
101
+ // Added by this plugin
102
+ prefix?: string;
103
+ prefixAvoidTrailingSlash?: boolean;
104
+ decorateReply?: boolean;
105
+ schemaHide?: boolean;
106
+ setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
107
+ redirect?: boolean;
108
+ wildcard?: boolean;
109
+ globIgnore?: string[];
110
+ list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
111
+ allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
112
+ /**
113
+ * @description
114
+ * Opt-in to looking for pre-compressed files
115
+ */
116
+ preCompressed?: boolean;
117
+
118
+ // Passed on to `send`
119
+ acceptRanges?: boolean;
120
+ contentType?: boolean;
121
+ cacheControl?: boolean;
122
+ dotfiles?: 'allow' | 'deny' | 'ignore';
123
+ etag?: boolean;
124
+ extensions?: string[];
125
+ immutable?: boolean;
126
+ index?: string[] | string | false;
127
+ lastModified?: boolean;
128
+ maxAge?: string | number;
129
+ constraints?: RouteOptions['constraints'];
130
+ logLevel?: RouteOptions['logLevel'];
131
+ }
132
+
120
133
  export const fastifyStatic: FastifyStaticPlugin
121
134
 
122
135
  export { fastifyStatic as default }
@@ -50,6 +50,7 @@ const options: FastifyStaticOptions = {
50
50
  schemaHide: true,
51
51
  serve: true,
52
52
  wildcard: true,
53
+ globIgnore: ['**/*.private'],
53
54
  list: false,
54
55
  setHeaders: (res, path, stat) => {
55
56
  expectType<string>(res.filename)
@@ -66,7 +67,7 @@ const options: FastifyStaticOptions = {
66
67
  return true
67
68
  },
68
69
  constraints: {
69
- host: /.*\.example\.com/,
70
+ host: /^.*\.example\.com$/,
70
71
  version: '1.0.2'
71
72
  },
72
73
  logLevel: 'warn'
@@ -119,6 +120,19 @@ expectAssignable<FastifyStaticOptions>({
119
120
  root: [new URL('')]
120
121
  })
121
122
 
123
+ expectError<FastifyStaticOptions>({
124
+ serve: true
125
+ })
126
+
127
+ expectAssignable<FastifyStaticOptions>({
128
+ serve: true,
129
+ root: ''
130
+ })
131
+
132
+ expectAssignable<FastifyStaticOptions>({
133
+ serve: false
134
+ })
135
+
122
136
  appWithImplicitHttp
123
137
  .register(fastifyStatic, options)
124
138
  .after(() => {