@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.
- package/.github/workflows/ci.yml +6 -1
- package/README.md +37 -4
- package/index.js +15 -12
- package/package.json +4 -4
- package/test/static-filtered/bar.private +1 -0
- package/test/static-filtered/deep/path/to/baz.html +3 -0
- package/test/static-filtered/deep/path/to/baz.private +1 -0
- package/test/static-filtered/index.html +3 -0
- package/test/static-pre-compressed/baz.json +3 -0
- package/test/static.test.js +130 -0
- package/types/index.d.ts +1 -0
- package/types/index.test-d.ts +1 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -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
|
-
[](https://github.com/fastify/fastify-static/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@fastify/static)
|
|
5
5
|
[](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
|
-
|
|
|
18
|
+
| `>=8.x` | `^5.x` |
|
|
19
19
|
| `^7.x` | `^4.x` |
|
|
20
|
-
|
|
|
21
|
-
|
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
80
|
+
"tsd": "^0.32.0"
|
|
81
81
|
},
|
|
82
82
|
"tsd": {
|
|
83
83
|
"directory": "test/types"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bar
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
baz
|
package/test/static.test.js
CHANGED
|
@@ -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
|
/**
|