@fastify/static 9.1.2 → 9.2.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.
package/.gitattributes CHANGED
@@ -1,5 +1,2 @@
1
- # Set the default behavior, in case people don't have core.autocrlf set
2
- * text=auto
3
-
4
- # Require Unix line endings
5
- * text eol=lf
1
+ # Set default behavior to automatically convert line endings
2
+ * text=auto eol=lf
@@ -2,12 +2,52 @@ version: 2
2
2
  updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
+ commit-message:
6
+ # Prefix all commit messages with "chore: "
7
+ prefix: "chore"
5
8
  schedule:
6
- interval: "monthly"
9
+ interval: "weekly"
10
+ cooldown:
11
+ default-days: 7
12
+ allow:
13
+ - dependency-name: "*"
14
+ update-types:
15
+ - "version-update:semver-major"
7
16
  open-pull-requests-limit: 10
8
17
 
9
18
  - package-ecosystem: "npm"
10
19
  directory: "/"
20
+ commit-message:
21
+ # Prefix all commit messages with "chore: "
22
+ prefix: "chore"
11
23
  schedule:
12
- interval: "monthly"
24
+ interval: "weekly"
25
+ cooldown:
26
+ default-days: 7
27
+ versioning-strategy: "increase-if-necessary"
28
+ allow:
29
+ - dependency-name: "*"
30
+ update-types:
31
+ - "version-update:semver-major"
32
+ ignore:
33
+ # TODO: remove ignore until neostandard support ESLint 10
34
+ - dependency-name: "eslint"
35
+ - dependency-name: "neostandard"
36
+ - dependency-name: "@stylistic/*"
13
37
  open-pull-requests-limit: 10
38
+ groups:
39
+ # Production dependencies with breaking changes
40
+ dependencies:
41
+ dependency-type: "production"
42
+ # ESLint related dependencies
43
+ dev-dependencies-eslint:
44
+ patterns:
45
+ - "eslint"
46
+ - "neostandard"
47
+ - "@stylistic/*"
48
+ # TypeScript related dependencies
49
+ dev-dependencies-typescript:
50
+ patterns:
51
+ - "@types/*"
52
+ - "tstyche"
53
+ - "typescript"
package/README.md CHANGED
@@ -114,7 +114,7 @@ fastify.get('/path/without/cache/control', function (req, reply) {
114
114
  ### Managing cache-control headers
115
115
 
116
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.
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
118
 
119
119
  ```js
120
120
  fastify.register(require('@fastify/static'), {
@@ -253,6 +253,8 @@ Default: `(pathName, root, request) => true`
253
253
  This function filters served files. Using the request object, complex path authentication is possible.
254
254
  Returning `true` serves the file; returning `false` calls Fastify's 404 handler.
255
255
 
256
+ When using `preCompressed: true`, `allowedPath` receives the requested path before `.br` or `.gz` variants are selected; see the `preCompressed` note below before using extension-based access rules.
257
+
256
258
  #### `index`
257
259
 
258
260
  Default: `undefined`
@@ -470,6 +472,8 @@ Default: `false`
470
472
 
471
473
  First, try to send the brotli encoded asset (if supported by `Accept-Encoding` headers), then gzip, and finally the original `pathname`. Skip compression for smaller files that do not benefit from it.
472
474
 
475
+ > ⚠ Warning: `allowedPath` is evaluated against the requested path before pre-compressed variants are selected. A request for `/file.txt` can serve `/file.txt.gz` or `/file.txt.br` when the client accepts that encoding, even if direct requests to `.gz` or `.br` files are denied by `allowedPath`. Treat pre-compressed files as public alternate encodings of the same asset: keep restricted compressed files outside the served root, disable `preCompressed`, or deny the base path too.
476
+
473
477
  Assume this structure with the compressed asset as a sibling of the uncompressed counterpart:
474
478
 
475
479
  ```
@@ -0,0 +1,39 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+ const fastify = require('fastify')({ logger: false })
5
+ const fastifyStatic = require(process.env.PLUGIN_PATH || '../')
6
+
7
+ const root = path.join(__dirname, '/public')
8
+ const port = Number(process.env.PORT || 3000)
9
+
10
+ fastify.register(fastifyStatic, {
11
+ root,
12
+ prefix: '/static',
13
+ decorateReply: false
14
+ })
15
+
16
+ fastify.register(fastifyStatic, {
17
+ root,
18
+ prefix: '/app/:version',
19
+ decorateReply: false
20
+ })
21
+
22
+ fastify.register(async function (child) {
23
+ child.register(fastifyStatic, {
24
+ root,
25
+ prefix: '/public',
26
+ decorateReply: false
27
+ })
28
+ }, { prefix: '/nested' })
29
+
30
+ fastify.listen({ port }, err => {
31
+ if (err) throw err
32
+
33
+ console.log(`benchmark server listening on http://127.0.0.1:${port}`)
34
+ console.log('')
35
+ console.log('Try:')
36
+ console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/static/index.css`)
37
+ console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/app/1.2.3/index.css`)
38
+ console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/nested/public/index.css`)
39
+ })
package/index.js CHANGED
@@ -76,7 +76,7 @@ async function fastifyStatic (fastify, opts) {
76
76
  return
77
77
  }
78
78
 
79
- fastify.errorHandler(error, request, reply)
79
+ return fastify.errorHandler(error, request, reply)
80
80
  }
81
81
  }
82
82
 
@@ -117,12 +117,16 @@ async function fastifyStatic (fastify, opts) {
117
117
  throw new TypeError('"wildcard" option must be a boolean')
118
118
  }
119
119
  if (opts.wildcard === undefined || opts.wildcard === true) {
120
+ let matchRoutePrefix
121
+
120
122
  fastify.route({
121
123
  ...routeOpts,
122
124
  method: ['HEAD', 'GET'],
123
125
  path: prefix + '*',
124
126
  handler (req, reply) {
125
- const pathname = getPathnameForSend(req.raw.url, req.routeOptions.url)
127
+ matchRoutePrefix ??= createRoutePrefixMatcher(req.routeOptions.url)
128
+
129
+ const pathname = getPathnameForSend(req.raw.url, matchRoutePrefix)
126
130
  if (!pathname) {
127
131
  return reply.callNotFound()
128
132
  }
@@ -569,23 +573,113 @@ function getEncodingHeader (headers, checked) {
569
573
  }
570
574
 
571
575
  /**
572
- * @param {string} url
576
+ * @param {string} routePrefix
577
+ * @returns {Array<string|undefined>}
578
+ */
579
+ function createRoutePrefixTokens (routePrefix) {
580
+ const tokens = []
581
+ let routeIndex = 0
582
+ let segmentStart = 0
583
+
584
+ while (routeIndex < routePrefix.length) {
585
+ if (routePrefix[routeIndex] !== ':') {
586
+ routeIndex++
587
+ continue
588
+ }
589
+
590
+ if (segmentStart !== routeIndex) {
591
+ tokens.push(routePrefix.slice(segmentStart, routeIndex))
592
+ }
593
+
594
+ routeIndex++
595
+ while (routeIndex < routePrefix.length && routePrefix[routeIndex] !== '/') {
596
+ routeIndex++
597
+ }
598
+
599
+ tokens.push(undefined)
600
+ segmentStart = routeIndex
601
+ }
602
+
603
+ if (segmentStart !== routePrefix.length) {
604
+ tokens.push(routePrefix.slice(segmentStart))
605
+ }
606
+
607
+ return tokens
608
+ }
609
+
610
+ /**
611
+ * @param {string} pathname
612
+ * @param {number} pathnameEnd
613
+ * @param {Array<string|undefined>} tokens
614
+ * @returns {number|undefined}
615
+ */
616
+ function getRoutePrefixMatchLength (pathname, pathnameEnd, tokens) {
617
+ let pathnameIndex = 0
618
+
619
+ for (const token of tokens) {
620
+ if (token === undefined) {
621
+ const segmentStart = pathnameIndex
622
+ const slashIndex = pathname.indexOf('/', pathnameIndex)
623
+
624
+ pathnameIndex = slashIndex === -1 || slashIndex > pathnameEnd
625
+ ? pathnameEnd
626
+ : slashIndex
627
+
628
+ if (pathnameIndex === segmentStart) {
629
+ return
630
+ }
631
+
632
+ continue
633
+ }
634
+
635
+ const tokenEnd = pathnameIndex + token.length
636
+ if (tokenEnd > pathnameEnd || !pathname.startsWith(token, pathnameIndex)) {
637
+ return
638
+ }
639
+
640
+ pathnameIndex = tokenEnd
641
+ }
642
+
643
+ return pathnameIndex
644
+ }
645
+
646
+ /**
573
647
  * @param {string} route
648
+ * @returns {(pathname: string, pathnameEnd: number) => number|undefined}
649
+ */
650
+ function createRoutePrefixMatcher (route) {
651
+ const routePrefix = route.replace(/\*$/u, '')
652
+ const routePrefixLength = routePrefix.length
653
+
654
+ if (routePrefix === '/') {
655
+ return () => 0
656
+ }
657
+
658
+ if (routePrefix.includes(':') === false) {
659
+ return (pathname, pathnameEnd) => routePrefixLength <= pathnameEnd && pathname.startsWith(routePrefix)
660
+ ? routePrefixLength
661
+ : undefined
662
+ }
663
+
664
+ const tokens = createRoutePrefixTokens(routePrefix)
665
+ return (pathname, pathnameEnd) => getRoutePrefixMatchLength(pathname, pathnameEnd, tokens)
666
+ }
667
+
668
+ /**
669
+ * @param {string} url
670
+ * @param {(pathname: string, pathnameEnd: number) => number|undefined} matchRoutePrefix
574
671
  * @returns {string|undefined}
575
672
  */
576
- function getPathnameForSend (url, route) {
673
+ function getPathnameForSend (url, matchRoutePrefix) {
577
674
  const questionMark = url.indexOf('?')
578
- let pathname = questionMark === -1 ? url : url.slice(0, questionMark)
579
-
580
- const routePrefix = route.endsWith('*')
581
- ? route.slice(0, -1)
582
- : route
675
+ const pathnameEnd = questionMark === -1 ? url.length : questionMark
583
676
 
584
- if (routePrefix !== '/' && !pathname.startsWith(routePrefix)) {
677
+ const prefixLength = matchRoutePrefix(url, pathnameEnd)
678
+ if (prefixLength === undefined) {
585
679
  return
586
680
  }
587
681
 
588
- pathname = pathname.slice(routePrefix.length)
682
+ let pathname = url.slice(prefixLength, pathnameEnd)
589
683
 
590
684
  if (pathname === '') {
591
685
  pathname = '/'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "9.1.2",
3
+ "version": "9.2.0",
4
4
  "description": "Plugin for serving static files as fast as possible.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -10,9 +10,10 @@
10
10
  "lint": "eslint",
11
11
  "lint:fix": "eslint --fix",
12
12
  "test": "npm run test:unit && npm run test:typescript",
13
- "test:typescript": "tsd",
13
+ "test:typescript": "tstyche",
14
14
  "test:unit": "borp -C --check-coverage --lines 100",
15
- "example": "node example/server.js"
15
+ "example": "node example/server.js",
16
+ "example:benchmark": "node example/server-benchmark.js"
16
17
  },
17
18
  "repository": {
18
19
  "type": "git",
@@ -66,8 +67,9 @@
66
67
  "glob": "^13.0.0"
67
68
  },
68
69
  "devDependencies": {
69
- "@fastify/compress": "^8.0.0",
70
- "@types/node": "^25.0.3",
70
+ "@fastify/compress": "^9.0.0",
71
+ "@types/node": "^26.0.0",
72
+ "autocannon": "^8.0.0",
71
73
  "borp": "^1.0.0",
72
74
  "c8": "^11.0.0",
73
75
  "concat-stream": "^2.0.0",
@@ -76,10 +78,7 @@
76
78
  "neostandard": "^0.12.0",
77
79
  "pino": "^10.0.0",
78
80
  "proxyquire": "^2.1.3",
79
- "tsd": "^0.33.0"
80
- },
81
- "tsd": {
82
- "directory": "test/types"
81
+ "tstyche": "^7.0.0"
83
82
  },
84
83
  "publishConfig": {
85
84
  "access": "public"
@@ -6,7 +6,6 @@ const path = require('node:path')
6
6
  const fs = require('node:fs')
7
7
  const url = require('node:url')
8
8
  const http = require('node:http')
9
- const Module = require('node:module')
10
9
  const { test } = require('node:test')
11
10
  const Fastify = require('fastify')
12
11
  const compress = require('@fastify/compress')
@@ -82,16 +81,6 @@ if (typeof Promise.withResolvers === 'undefined') {
82
81
  }
83
82
  }
84
83
 
85
- function loadInternalPathnameForSend () {
86
- const modulePath = require.resolve('../index.js')
87
- const source = fs.readFileSync(modulePath, 'utf8') + '\nmodule.exports.__getPathnameForSend = getPathnameForSend\n'
88
- const testModule = new Module(modulePath)
89
- testModule.filename = modulePath
90
- testModule.paths = module.paths
91
- testModule._compile(source, modulePath)
92
- return testModule.exports.__getPathnameForSend
93
- }
94
-
95
84
  test('register /static prefixAvoidTrailingSlash', async t => {
96
85
  t.plan(11)
97
86
 
@@ -2648,6 +2637,43 @@ test('routes should fallback to default errorHandler', async t => {
2648
2637
  })
2649
2638
  })
2650
2639
 
2640
+ test('routes should propagate return value from async custom errorHandler', async t => {
2641
+ t.plan(3)
2642
+
2643
+ const pluginOptions = {
2644
+ root: path.join(__dirname, '/static'),
2645
+ prefix: '/static/'
2646
+ }
2647
+
2648
+ const fastify = Fastify()
2649
+
2650
+ // An async error handler that returns its payload (without an explicit
2651
+ // reply.send) relies on Fastify awaiting the returned promise. If
2652
+ // @fastify/static does not propagate the return value of
2653
+ // fastify.errorHandler(), the promise is dropped and the reply never
2654
+ // resolves with the customized body.
2655
+ fastify.setErrorHandler(async function (_error, _request, reply) {
2656
+ reply.code(418).type('text/plain')
2657
+ return 'handled by return value'
2658
+ })
2659
+
2660
+ fastify.addHook('onRoute', function (routeOptions) {
2661
+ routeOptions.preHandler = (_request, _reply, done) => {
2662
+ const fakeError = new Error()
2663
+ fakeError.code = 'SOMETHING_ELSE'
2664
+ done(fakeError)
2665
+ }
2666
+ })
2667
+
2668
+ fastify.register(fastifyStatic, pluginOptions)
2669
+ t.after(() => fastify.close())
2670
+
2671
+ const response = await fastify.inject({ method: 'GET', url: '/static/index.html' })
2672
+ t.assert.deepStrictEqual(response.statusCode, 418)
2673
+ t.assert.ok(response.headers['content-type'].startsWith('text/plain'))
2674
+ t.assert.deepStrictEqual(response.body, 'handled by return value')
2675
+ })
2676
+
2651
2677
  test('percent encoded URLs in glob mode', async (t) => {
2652
2678
  t.plan(3)
2653
2679
 
@@ -3601,16 +3627,6 @@ test(
3601
3627
  }
3602
3628
  )
3603
3629
 
3604
- test('getPathnameForSend returns undefined for mismatched and malformed routes', (t) => {
3605
- t.plan(3)
3606
-
3607
- const getPathnameForSend = loadInternalPathnameForSend()
3608
-
3609
- t.assert.deepStrictEqual(getPathnameForSend('/nested/public/index.css', '/public/*'), undefined)
3610
- t.assert.deepStrictEqual(getPathnameForSend('/static/%E0%A4%A', '/static/*'), undefined)
3611
- t.assert.deepStrictEqual(getPathnameForSend('/static/index.css', '/static'), '/index.css')
3612
- })
3613
-
3614
3630
  test('wildcard handler falls back to not found when the raw url does not match the route prefix', async (t) => {
3615
3631
  t.plan(2)
3616
3632
 
@@ -3634,7 +3650,7 @@ test('wildcard handler falls back to not found when the raw url does not match t
3634
3650
  t.assert.ok(wildcardHandler)
3635
3651
 
3636
3652
  let notFoundCalled = false
3637
- wildcardHandler({
3653
+ await wildcardHandler({
3638
3654
  raw: { url: '/nested/static/index.css' },
3639
3655
  routeOptions: { url: '/static/*' }
3640
3656
  }, {
@@ -3646,6 +3662,111 @@ test('wildcard handler falls back to not found when the raw url does not match t
3646
3662
  t.assert.deepStrictEqual(notFoundCalled, true)
3647
3663
  })
3648
3664
 
3665
+ test('wildcard handler falls back to not found when a param segment is empty', async (t) => {
3666
+ t.plan(2)
3667
+
3668
+ const fastify = Fastify()
3669
+ let wildcardHandler
3670
+
3671
+ fastify.addHook('onRoute', (route) => {
3672
+ if (route.url === '/app/:version/*') {
3673
+ wildcardHandler = route.handler
3674
+ }
3675
+ })
3676
+
3677
+ fastify.register(fastifyStatic, {
3678
+ root: path.join(__dirname, '/static'),
3679
+ prefix: '/app/:version'
3680
+ })
3681
+
3682
+ t.after(() => fastify.close())
3683
+
3684
+ await fastify.ready()
3685
+ t.assert.ok(wildcardHandler)
3686
+
3687
+ let notFoundCalled = false
3688
+ await wildcardHandler({
3689
+ raw: { url: '/app//index.css' },
3690
+ routeOptions: { url: '/app/:version/*' }
3691
+ }, {
3692
+ callNotFound () {
3693
+ notFoundCalled = true
3694
+ }
3695
+ })
3696
+
3697
+ t.assert.deepStrictEqual(notFoundCalled, true)
3698
+ })
3699
+
3700
+ test('wildcard handler falls back to not found when the wildcard remainder is malformed', async (t) => {
3701
+ t.plan(2)
3702
+
3703
+ const fastify = Fastify()
3704
+ let wildcardHandler
3705
+
3706
+ fastify.addHook('onRoute', (route) => {
3707
+ if (route.url === '/app/:version/*') {
3708
+ wildcardHandler = route.handler
3709
+ }
3710
+ })
3711
+
3712
+ fastify.register(fastifyStatic, {
3713
+ root: path.join(__dirname, '/static'),
3714
+ prefix: '/app/:version'
3715
+ })
3716
+
3717
+ t.after(() => fastify.close())
3718
+
3719
+ await fastify.ready()
3720
+ t.assert.ok(wildcardHandler)
3721
+
3722
+ let notFoundCalled = false
3723
+ await wildcardHandler({
3724
+ raw: { url: '/app/1.2.3/%E0%A4%A' },
3725
+ routeOptions: { url: '/app/:version/*' }
3726
+ }, {
3727
+ callNotFound () {
3728
+ notFoundCalled = true
3729
+ }
3730
+ })
3731
+
3732
+ t.assert.deepStrictEqual(notFoundCalled, true)
3733
+ })
3734
+
3735
+ test('wildcard handler falls back to not found when a literal segment after a param does not match', async (t) => {
3736
+ t.plan(2)
3737
+
3738
+ const fastify = Fastify()
3739
+ let wildcardHandler
3740
+
3741
+ fastify.addHook('onRoute', (route) => {
3742
+ if (route.url === '/app/:version/public/*') {
3743
+ wildcardHandler = route.handler
3744
+ }
3745
+ })
3746
+
3747
+ fastify.register(fastifyStatic, {
3748
+ root: path.join(__dirname, '/static'),
3749
+ prefix: '/app/:version/public'
3750
+ })
3751
+
3752
+ t.after(() => fastify.close())
3753
+
3754
+ await fastify.ready()
3755
+ t.assert.ok(wildcardHandler)
3756
+
3757
+ let notFoundCalled = false
3758
+ await wildcardHandler({
3759
+ raw: { url: '/app/1.2.3/private/index.css' },
3760
+ routeOptions: { url: '/app/:version/public/*' }
3761
+ }, {
3762
+ callNotFound () {
3763
+ notFoundCalled = true
3764
+ }
3765
+ })
3766
+
3767
+ t.assert.deepStrictEqual(notFoundCalled, true)
3768
+ })
3769
+
3649
3770
  test('does not serve static files with encoded path separators', async (t) => {
3650
3771
  t.plan(4)
3651
3772
 
@@ -3701,6 +3822,53 @@ test('serves wildcard files when registered in an encapsulated context', async (
3701
3822
  t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.css'), 'utf8'))
3702
3823
  })
3703
3824
 
3825
+ test('serves wildcard files when prefix contains a route param', async (t) => {
3826
+ t.plan(3)
3827
+
3828
+ const fastify = Fastify()
3829
+
3830
+ t.after(() => fastify.close())
3831
+
3832
+ fastify.register(fastifyStatic, {
3833
+ root: path.join(__dirname, '/static'),
3834
+ prefix: '/app/:version',
3835
+ decorateReply: false
3836
+ })
3837
+
3838
+ const response = await fastify.inject({
3839
+ method: 'GET',
3840
+ url: '/app/1.2.3/index.css'
3841
+ })
3842
+
3843
+ t.assert.deepStrictEqual(response.statusCode, 200)
3844
+ t.assert.deepStrictEqual(response.headers['content-type'], 'text/css; charset=utf-8')
3845
+ t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.css'), 'utf8'))
3846
+ })
3847
+
3848
+ test('serves wildcard index files when a param prefix uses prefixAvoidTrailingSlash', async (t) => {
3849
+ t.plan(3)
3850
+
3851
+ const fastify = Fastify()
3852
+
3853
+ t.after(() => fastify.close())
3854
+
3855
+ fastify.register(fastifyStatic, {
3856
+ root: path.join(__dirname, '/static'),
3857
+ prefix: '/app/:version',
3858
+ prefixAvoidTrailingSlash: true,
3859
+ decorateReply: false
3860
+ })
3861
+
3862
+ const response = await fastify.inject({
3863
+ method: 'GET',
3864
+ url: '/app/1.2.3'
3865
+ })
3866
+
3867
+ t.assert.deepStrictEqual(response.statusCode, 200)
3868
+ t.assert.deepStrictEqual(response.headers['content-type'], 'text/html; charset=utf-8')
3869
+ t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.html'), 'utf8'))
3870
+ })
3871
+
3704
3872
  test('content-length in head route should not return zero when using wildcard', async t => {
3705
3873
  t.plan(5)
3706
3874
 
package/types/index.d.ts CHANGED
@@ -82,18 +82,19 @@ declare namespace fastifyStatic {
82
82
 
83
83
  type Root = string | string[] | URL | URL[]
84
84
 
85
- type RootOptions = {
86
- serve: true;
87
- root: Root;
88
- } | {
89
- serve?: false;
90
- root?: Root;
91
- }
85
+ type RootOptions =
86
+ | {
87
+ serve: true;
88
+ root: Root;
89
+ }
90
+ | {
91
+ serve?: false;
92
+ root?: Root;
93
+ }
92
94
 
93
- export type FastifyStaticOptions =
94
- SendOptions
95
- & RootOptions
96
- & {
95
+ export type FastifyStaticOptions = SendOptions &
96
+ RootOptions &
97
+ Omit<import('fastify').RegisterOptions, 'logLevel'> & {
97
98
  // Added by this plugin
98
99
  prefix?: string;
99
100
  prefixAvoidTrailingSlash?: boolean;
@@ -104,7 +105,11 @@ declare namespace fastifyStatic {
104
105
  wildcard?: boolean;
105
106
  globIgnore?: string[];
106
107
  list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
107
- allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
108
+ allowedPath?: (
109
+ pathName: string,
110
+ root: string,
111
+ request: FastifyRequest
112
+ ) => boolean;
108
113
  /**
109
114
  * @description
110
115
  * Opt-in to looking for pre-compressed files
@@ -123,7 +128,7 @@ declare namespace fastifyStatic {
123
128
  lastModified?: boolean;
124
129
  maxAge?: string | number;
125
130
  constraints?: RouteOptions['constraints'];
126
- logLevel?: RouteOptions['logLevel'];
131
+ logLevel?: NonNullable<RouteOptions['logLevel']>;
127
132
  }
128
133
 
129
134
  export const fastifyStatic: FastifyStaticPlugin
@@ -1,10 +1,10 @@
1
- import fastify, { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify'
2
- import { Server } from 'node:http'
3
- import { Stats } from 'node:fs'
4
- import { expectAssignable, expectError, expectType } from 'tsd'
1
+ import fastify, { type FastifyInstance, type FastifyPluginAsync, type FastifyRequest, type FastifyReply } from 'fastify'
2
+ import { type Server } from 'node:http'
3
+ import { type Stats } from 'node:fs'
4
+ import { expect } from 'tstyche'
5
5
  import * as fastifyStaticStar from '..'
6
6
  import fastifyStatic, {
7
- FastifyStaticOptions,
7
+ type FastifyStaticOptions,
8
8
  fastifyStatic as fastifyStaticNamed
9
9
  } from '..'
10
10
 
@@ -21,15 +21,13 @@ app.register(fastifyStaticCjsImport.fastifyStatic, { root: __dirname })
21
21
  app.register(fastifyStaticStar.default, { root: __dirname })
22
22
  app.register(fastifyStaticStar.fastifyStatic, { root: __dirname })
23
23
 
24
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStatic)
25
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticNamed)
26
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.default)
27
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticCjsImport.fastifyStatic)
28
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(fastifyStaticStar.default)
29
- expectType<FastifyPluginAsync<FastifyStaticOptions, Server>>(
30
- fastifyStaticStar.fastifyStatic
31
- )
32
- expectType<any>(fastifyStaticCjs)
24
+ expect(fastifyStatic).type.toBe<FastifyPluginAsync<FastifyStaticOptions, Server>>()
25
+ expect(fastifyStaticNamed).type.toBe<FastifyPluginAsync<FastifyStaticOptions, Server>>()
26
+ expect(fastifyStaticCjsImport.default).type.toBe<FastifyPluginAsync<FastifyStaticOptions, Server>>()
27
+ expect(fastifyStaticCjsImport.fastifyStatic).type.toBe<FastifyPluginAsync<FastifyStaticOptions, Server>>()
28
+ expect(fastifyStaticStar.default).type.toBe<FastifyPluginAsync<FastifyStaticOptions, Server>>()
29
+ expect(fastifyStaticStar.fastifyStatic).type.toBe<FastifyPluginAsync<FastifyStaticOptions, Server>>()
30
+ expect(fastifyStaticCjs).type.toBe<any>()
33
31
 
34
32
  const appWithImplicitHttp = fastify()
35
33
  const options: FastifyStaticOptions = {
@@ -53,14 +51,14 @@ const options: FastifyStaticOptions = {
53
51
  globIgnore: ['**/*.private'],
54
52
  list: false,
55
53
  setHeaders: (res, path, stat) => {
56
- expectType<string>(res.filename)
57
- expectType<number>(res.statusCode)
58
- expectType<ReturnType<FastifyReply['getHeader']>>(res.getHeader('X-Test'))
54
+ expect(res.filename).type.toBe<string>()
55
+ expect(res.statusCode).type.toBe<number>()
56
+ expect(res.getHeader('X-Test')).type.toBe<ReturnType<FastifyReply['getHeader']>>()
59
57
  res.setHeader('X-Test', 'string')
60
58
 
61
- expectType<string>(path)
59
+ expect(path).type.toBe<string>()
62
60
 
63
- expectType<Stats>(stat)
61
+ expect(stat).type.toBe<Stats>()
64
62
  },
65
63
  preCompressed: false,
66
64
  allowedPath: (_pathName: string, _root: string, _request: FastifyRequest) => {
@@ -73,64 +71,53 @@ const options: FastifyStaticOptions = {
73
71
  logLevel: 'warn'
74
72
  }
75
73
 
76
- expectError<FastifyStaticOptions>({
77
- root: '',
78
- wildcard: '**/**'
79
- })
80
-
81
- expectAssignable<FastifyStaticOptions>({
82
- root: '',
83
- list: {
84
- format: 'json'
85
- }
86
- })
87
-
88
- expectAssignable<FastifyStaticOptions>({
89
- root: '',
90
- list: {
91
- format: 'json',
92
- render: () => ''
93
- }
94
- })
95
-
96
- expectAssignable<FastifyStaticOptions>({
97
- root: '',
98
- list: {
99
- format: 'html',
100
- render: () => ''
101
- }
102
- })
74
+ expect<FastifyStaticOptions>()
75
+ .type.toBeAssignableFrom({
76
+ root: '',
77
+ list: {
78
+ format: 'json' as const
79
+ }
80
+ })
103
81
 
104
- expectError<FastifyStaticOptions>({
105
- root: '',
106
- list: {
107
- format: 'html'
108
- }
109
- })
82
+ expect<FastifyStaticOptions>()
83
+ .type.toBeAssignableFrom({
84
+ root: '',
85
+ list: {
86
+ format: 'json' as const,
87
+ render: () => ''
88
+ }
89
+ })
110
90
 
111
- expectAssignable<FastifyStaticOptions>({
112
- root: ['']
113
- })
91
+ expect<FastifyStaticOptions>()
92
+ .type.toBeAssignableFrom({
93
+ root: '',
94
+ list: {
95
+ format: 'html' as const,
96
+ render: () => ''
97
+ }
98
+ })
114
99
 
115
- expectAssignable<FastifyStaticOptions>({
116
- root: new URL('')
117
- })
100
+ expect<FastifyStaticOptions>()
101
+ .type.toBeAssignableFrom({
102
+ root: ['']
103
+ })
118
104
 
119
- expectAssignable<FastifyStaticOptions>({
120
- root: [new URL('')]
121
- })
105
+ expect<FastifyStaticOptions>()
106
+ .type.toBeAssignableFrom({
107
+ root: new URL('file://')
108
+ })
122
109
 
123
- expectError<FastifyStaticOptions>({
124
- serve: true
125
- })
110
+ expect<FastifyStaticOptions>()
111
+ .type.toBeAssignableFrom({ root: [new URL('file://')] })
126
112
 
127
- expectAssignable<FastifyStaticOptions>({
128
- serve: true,
129
- root: ''
130
- })
113
+ expect<FastifyStaticOptions>()
114
+ .type.toBeAssignableFrom({
115
+ serve: true as const,
116
+ root: ''
117
+ })
131
118
 
132
- expectAssignable<FastifyStaticOptions>({
133
- serve: false
119
+ expect<FastifyStaticOptions>().type.not.toBeAssignableFrom({
120
+ serve: true as const,
134
121
  })
135
122
 
136
123
  appWithImplicitHttp
@@ -218,7 +205,7 @@ noIndexApp
218
205
  })
219
206
  })
220
207
 
221
- options.root = new URL('')
208
+ options.root = new URL('file://')
222
209
 
223
210
  const URLRootApp = fastify()
224
211
  URLRootApp.register(fastifyStatic, options)
@@ -229,7 +216,7 @@ URLRootApp.register(fastifyStatic, options)
229
216
  })
230
217
 
231
218
  const defaultIndexApp = fastify()
232
- options.index = 'index.html'
219
+ options.index = 'index.html' as const
233
220
 
234
221
  defaultIndexApp
235
222
  .register(fastifyStatic, options)