@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 +2 -5
- package/.github/dependabot.yml +42 -2
- package/README.md +5 -1
- package/example/server-benchmark.js +39 -0
- package/index.js +105 -11
- package/package.json +8 -9
- package/test/static.test.js +190 -22
- package/types/index.d.ts +18 -13
- package/types/{index.test-d.ts → index.tst.ts} +59 -72
package/.gitattributes
CHANGED
package/.github/dependabot.yml
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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}
|
|
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,
|
|
673
|
+
function getPathnameForSend (url, matchRoutePrefix) {
|
|
577
674
|
const questionMark = url.indexOf('?')
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const routePrefix = route.endsWith('*')
|
|
581
|
-
? route.slice(0, -1)
|
|
582
|
-
: route
|
|
675
|
+
const pathnameEnd = questionMark === -1 ? url.length : questionMark
|
|
583
676
|
|
|
584
|
-
|
|
677
|
+
const prefixLength = matchRoutePrefix(url, pathnameEnd)
|
|
678
|
+
if (prefixLength === undefined) {
|
|
585
679
|
return
|
|
586
680
|
}
|
|
587
681
|
|
|
588
|
-
pathname =
|
|
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.
|
|
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": "
|
|
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": "^
|
|
70
|
-
"@types/node": "^
|
|
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
|
-
"
|
|
80
|
-
},
|
|
81
|
-
"tsd": {
|
|
82
|
-
"directory": "test/types"
|
|
81
|
+
"tstyche": "^7.0.0"
|
|
83
82
|
},
|
|
84
83
|
"publishConfig": {
|
|
85
84
|
"access": "public"
|
package/test/static.test.js
CHANGED
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
&
|
|
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?: (
|
|
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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
59
|
+
expect(path).type.toBe<string>()
|
|
62
60
|
|
|
63
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
82
|
+
expect<FastifyStaticOptions>()
|
|
83
|
+
.type.toBeAssignableFrom({
|
|
84
|
+
root: '',
|
|
85
|
+
list: {
|
|
86
|
+
format: 'json' as const,
|
|
87
|
+
render: () => ''
|
|
88
|
+
}
|
|
89
|
+
})
|
|
110
90
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
91
|
+
expect<FastifyStaticOptions>()
|
|
92
|
+
.type.toBeAssignableFrom({
|
|
93
|
+
root: '',
|
|
94
|
+
list: {
|
|
95
|
+
format: 'html' as const,
|
|
96
|
+
render: () => ''
|
|
97
|
+
}
|
|
98
|
+
})
|
|
114
99
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
expect<FastifyStaticOptions>()
|
|
101
|
+
.type.toBeAssignableFrom({
|
|
102
|
+
root: ['']
|
|
103
|
+
})
|
|
118
104
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
105
|
+
expect<FastifyStaticOptions>()
|
|
106
|
+
.type.toBeAssignableFrom({
|
|
107
|
+
root: new URL('file://')
|
|
108
|
+
})
|
|
122
109
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
})
|
|
110
|
+
expect<FastifyStaticOptions>()
|
|
111
|
+
.type.toBeAssignableFrom({ root: [new URL('file://')] })
|
|
126
112
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
113
|
+
expect<FastifyStaticOptions>()
|
|
114
|
+
.type.toBeAssignableFrom({
|
|
115
|
+
serve: true as const,
|
|
116
|
+
root: ''
|
|
117
|
+
})
|
|
131
118
|
|
|
132
|
-
|
|
133
|
-
serve:
|
|
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)
|