@fastify/static 8.1.0 → 8.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.
@@ -4,7 +4,6 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
- - master
8
7
  - next
9
8
  - 'v*'
10
9
  paths-ignore:
@@ -15,8 +14,14 @@ on:
15
14
  - 'docs/**'
16
15
  - '*.md'
17
16
 
17
+ permissions:
18
+ contents: read
19
+
18
20
  jobs:
19
21
  test:
22
+ permissions:
23
+ contents: write
24
+ pull-requests: write
20
25
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
21
26
  with:
22
27
  license-check: true
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fastify/static
2
2
 
3
- [![CI](https://github.com/fastify/fastify-static/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/fastify-static/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/fastify/fastify-static/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-static/actions/workflows/ci.yml)
4
4
  [![NPM version](https://img.shields.io/npm/v/@fastify/static.svg?style=flat)](https://www.npmjs.com/package/@fastify/static)
5
5
  [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
6
6
 
@@ -15,10 +15,10 @@ npm i @fastify/static
15
15
 
16
16
  | Plugin version | Fastify version |
17
17
  | ---------------|-----------------|
18
- | `^8.x` | `^5.x` |
18
+ | `>=8.x` | `^5.x` |
19
19
  | `^7.x` | `^4.x` |
20
- | `^5.x` | `^3.x` |
21
- | `^2.x` | `^2.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,6 +111,31 @@ 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
141
  #### `root` (required)
@@ -208,6 +233,14 @@ are applied for getting the file list.
208
233
 
209
234
  This option cannot be `false` if `redirect` is `true` and `ignoreTrailingSlash` is `true`.
210
235
 
236
+ #### `globIgnore`
237
+
238
+ Default: `undefined`
239
+
240
+ This is passed to [`glob`](https://www.npmjs.com/package/glob)
241
+ as the `ignore` option. It can be used to ignore files or directories
242
+ when using the `wildcard: false` option.
243
+
211
244
  #### `allowedPath`
212
245
 
213
246
  Default: `(pathName, root, request) => true`
package/index.js CHANGED
@@ -135,7 +135,7 @@ async function fastifyStatic (fastify, opts) {
135
135
  rootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
136
136
  !rootPath.endsWith('/') && (rootPath += '/')
137
137
  const files = await glob('**/**', {
138
- cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles
138
+ cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles, ignore: opts.globIgnore
139
139
  })
140
140
 
141
141
  for (let file of files) {
@@ -284,23 +284,26 @@ async function fastifyStatic (fastify, opts) {
284
284
  prefix,
285
285
  dotfiles: opts.dotfiles
286
286
  }).catch((err) => reply.send(err))
287
+ return
287
288
  }
288
289
  }
289
290
 
290
291
  if (metadata.error.code === 'ENOENT') {
291
292
  // when preCompress is enabled and the path is a directory without a trailing slash
292
293
  if (opts.preCompressed && encoding) {
293
- const indexPathname = findIndexFile(pathname, options.root, options.index)
294
- if (indexPathname) {
295
- return pumpSendToReply(
296
- request,
297
- reply,
298
- pathname + '/',
299
- rootPath,
300
- undefined,
301
- undefined,
302
- checkedEncodings
303
- )
294
+ if (opts.redirect !== true) {
295
+ const indexPathname = findIndexFile(pathname, options.root, options.index)
296
+ if (indexPathname) {
297
+ return pumpSendToReply(
298
+ request,
299
+ reply,
300
+ pathname + '/',
301
+ rootPath,
302
+ undefined,
303
+ undefined,
304
+ checkedEncodings
305
+ )
306
+ }
304
307
  }
305
308
  }
306
309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "8.1.0",
3
+ "version": "8.2.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",
@@ -69,7 +69,7 @@
69
69
  "@fastify/compress": "^8.0.0",
70
70
  "@fastify/pre-commit": "^2.1.0",
71
71
  "@types/node": "^22.0.0",
72
- "borp": "^0.19.0",
72
+ "borp": "^0.20.0",
73
73
  "c8": "^10.1.3",
74
74
  "concat-stream": "^2.0.0",
75
75
  "eslint": "^9.17.0",
@@ -77,7 +77,7 @@
77
77
  "neostandard": "^0.12.0",
78
78
  "pino": "^9.1.0",
79
79
  "proxyquire": "^2.1.3",
80
- "tsd": "^0.31.0"
80
+ "tsd": "^0.32.0"
81
81
  },
82
82
  "tsd": {
83
83
  "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>
@@ -0,0 +1,3 @@
1
+ {
2
+ "baz": "baz"
3
+ }
@@ -2952,6 +2952,34 @@ test(
2952
2952
  }
2953
2953
  )
2954
2954
 
2955
+ test(
2956
+ 'will redirect to preCompressed index without trailing slash when redirect is true',
2957
+ async (t) => {
2958
+ const pluginOptions = {
2959
+ root: path.join(__dirname, '/static-pre-compressed'),
2960
+ prefix: '/static-pre-compressed/',
2961
+ preCompressed: true,
2962
+ redirect: true,
2963
+ }
2964
+
2965
+ const fastify = Fastify()
2966
+
2967
+ fastify.register(fastifyStatic, pluginOptions)
2968
+ t.after(() => fastify.close())
2969
+
2970
+ const response = await fastify.inject({
2971
+ method: 'GET',
2972
+ url: '/static-pre-compressed/dir',
2973
+ headers: {
2974
+ 'accept-encoding': 'gzip, deflate, br'
2975
+ }
2976
+ })
2977
+
2978
+ t.assert.deepStrictEqual(response.statusCode, 301)
2979
+ t.assert.deepStrictEqual(response.headers.location, '/static-pre-compressed/dir/')
2980
+ }
2981
+ )
2982
+
2955
2983
  test(
2956
2984
  'will serve precompressed gzip index in subdir',
2957
2985
  async (t) => {
@@ -3038,6 +3066,34 @@ test(
3038
3066
  }
3039
3067
  )
3040
3068
 
3069
+ test(
3070
+ 'will not redirect but serve a file if preCompressed but no compressed file exists and redirect is true',
3071
+ async (t) => {
3072
+ const pluginOptions = {
3073
+ root: path.join(__dirname, '/static-pre-compressed'),
3074
+ prefix: '/static-pre-compressed/',
3075
+ preCompressed: true,
3076
+ redirect: true
3077
+ }
3078
+
3079
+ const fastify = Fastify()
3080
+
3081
+ fastify.register(fastifyStatic, pluginOptions)
3082
+ t.after(() => fastify.close())
3083
+
3084
+ const response = await fastify.inject({
3085
+ method: 'GET',
3086
+ url: '/static-pre-compressed/baz.json',
3087
+ headers: {
3088
+ 'accept-encoding': '*'
3089
+ }
3090
+ })
3091
+
3092
+ t.assert.deepStrictEqual(response.statusCode, 200)
3093
+ t.assert.deepStrictEqual(response.headers['content-type'], 'application/json; charset=utf-8')
3094
+ }
3095
+ )
3096
+
3041
3097
  test(
3042
3098
  'nonexistent index with precompressed option',
3043
3099
  async (t) => {
@@ -3613,3 +3669,77 @@ test('register /static/ with custom log level', async t => {
3613
3669
  t.assert.deepStrictEqual(response2.status, 304)
3614
3670
  })
3615
3671
  })
3672
+
3673
+ test('register with wildcard false and globIgnore', async t => {
3674
+ t.plan(5)
3675
+
3676
+ const indexContent = fs
3677
+ .readFileSync('./test/static-filtered/index.html')
3678
+ .toString('utf8')
3679
+
3680
+ const deepContent = fs
3681
+ .readFileSync('./test/static-filtered/deep/path/to/baz.html')
3682
+ .toString('utf8')
3683
+
3684
+ const pluginOptions = {
3685
+ root: path.join(__dirname, '/static-filtered'),
3686
+ wildcard: false,
3687
+ globIgnore: ['**/*.private']
3688
+ }
3689
+ const fastify = Fastify()
3690
+ fastify.register(fastifyStatic, pluginOptions)
3691
+
3692
+ t.after(() => fastify.close())
3693
+
3694
+ await fastify.listen({ port: 0 })
3695
+
3696
+ fastify.server.unref()
3697
+
3698
+ await t.test('/index.html', async t => {
3699
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
3700
+
3701
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html')
3702
+ t.assert.ok(response.ok)
3703
+ t.assert.deepStrictEqual(response.status, 200)
3704
+ t.assert.deepStrictEqual(await response.text(), indexContent)
3705
+ genericResponseChecks(t, response)
3706
+ })
3707
+
3708
+ await t.test('/bar.private', async t => {
3709
+ t.plan(2)
3710
+
3711
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/bar.private')
3712
+ t.assert.ok(!response.ok)
3713
+ t.assert.deepStrictEqual(response.status, 404)
3714
+ await response.text()
3715
+ })
3716
+
3717
+ await t.test('/', async (t) => {
3718
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
3719
+
3720
+ const response = await fetch('http://localhost:' + fastify.server.address().port)
3721
+ t.assert.ok(response.ok)
3722
+ t.assert.deepStrictEqual(response.status, 200)
3723
+ t.assert.deepStrictEqual(await response.text(), indexContent)
3724
+ genericResponseChecks(t, response)
3725
+ })
3726
+
3727
+ await t.test('/deep/path/to/baz.html', async (t) => {
3728
+ t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
3729
+
3730
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/deep/path/to/baz.html')
3731
+ t.assert.ok(response.ok)
3732
+ t.assert.deepStrictEqual(response.status, 200)
3733
+ t.assert.deepStrictEqual(await response.text(), deepContent)
3734
+ genericResponseChecks(t, response)
3735
+ })
3736
+
3737
+ await t.test('/deep/path/to/baz.private', async (t) => {
3738
+ t.plan(2)
3739
+
3740
+ const response = await fetch('http://localhost:' + fastify.server.address().port + '/deep/path/to/baz.private')
3741
+ t.assert.ok(!response.ok)
3742
+ t.assert.deepStrictEqual(response.status, 404)
3743
+ await response.text()
3744
+ })
3745
+ })
package/types/index.d.ts CHANGED
@@ -94,6 +94,7 @@ declare namespace fastifyStatic {
94
94
  setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
95
95
  redirect?: boolean;
96
96
  wildcard?: boolean;
97
+ globIgnore?: string[];
97
98
  list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
98
99
  allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
99
100
  /**
@@ -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)