@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.
- package/.github/workflows/ci.yml +11 -0
- package/LICENSE +3 -1
- package/README.md +45 -7
- package/index.js +69 -35
- package/package.json +5 -6
- 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.test.js +130 -8
- package/types/index.d.ts +44 -31
- package/types/index.test-d.ts +15 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
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
|
-
#### `
|
|
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
|
-
>
|
|
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.
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 +
|
|
232
|
+
pathnameForSend = pathnameForSend + pathname + encodingExtensionMap[encoding]
|
|
219
233
|
} else {
|
|
220
|
-
pathnameForSend = pathname +
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
-
"@
|
|
71
|
-
"
|
|
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.
|
|
79
|
+
"tsd": "^0.33.0"
|
|
81
80
|
},
|
|
82
81
|
"tsd": {
|
|
83
82
|
"directory": "test/types"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bar
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
baz
|
package/test/static.test.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 }
|
package/types/index.test-d.ts
CHANGED
|
@@ -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:
|
|
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(() => {
|