@fastify/static 8.1.1 → 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.
@@ -14,8 +14,14 @@ on:
14
14
  - 'docs/**'
15
15
  - '*.md'
16
16
 
17
+ permissions:
18
+ contents: read
19
+
17
20
  jobs:
18
21
  test:
22
+ permissions:
23
+ contents: write
24
+ pull-requests: write
19
25
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
20
26
  with:
21
27
  license-check: true
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` |
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,6 +284,7 @@ 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/static",
3
- "version": "8.1.1",
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>
@@ -3669,3 +3669,77 @@ test('register /static/ with custom log level', async t => {
3669
3669
  t.assert.deepStrictEqual(response2.status, 304)
3670
3670
  })
3671
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)