Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"language": "en,en-gb",
"words": [
"memfs",
"colorette",
"noextension",
"fullhash",
"execa",
Expand All @@ -24,7 +23,8 @@
"cachable",
"finalhandler",
"hono",
"rspack"
"rspack",
"malformed"
],
"ignorePaths": [
"CHANGELOG.md",
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ jobs:
name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18.x, 20.x, 22.x, 24.x]
node-version: [20.x, 22.x, 24.x]
webpack-version: [latest]

runs-on: ${{ matrix.os }}
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ See [below](#other-servers) for an example of use with fastify.
| **[`etag`](#tag)** | `boolean\| "weak"\| "strong"` | `undefined` | Enable or disable etag generation. |
| **[`lastModified`](#lastmodified)** | `boolean` | `undefined` | Enable or disable `Last-Modified` header. Uses the file system's last modified value. |
| **[`cacheControl`](#cachecontrol)** | `boolean\|number\|string\|Object` | `undefined` | Enable or disable setting `Cache-Control` response header. |
| **[`cacheImmutable`](#cacheimmutable)** | `boolean\` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. |
| **[`cacheImmutable`](#cacheimmutable)** | `boolean` | `undefined` | Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets. |
| **[`publicPath`](#publicpath)** | `string` | `undefined` | The public path that the middleware is bound to. |
| **[`stats`](#stats)** | `boolean\|string\|Object` | `stats` (from a configuration) | Stats options object or preset name. |
| **[`serverSideRender`](#serversiderender)** | `boolean` | `undefined` | Instructs the module to enable or disable the server-side rendering mode. |
Expand Down Expand Up @@ -459,7 +459,18 @@ const app = new express();
app.use(instance);

instance.waitUntilValid(() => {
const filename = instance.getFilenameFromUrl("/bundle.js");
let resolver;

try {
resolved = instance.getFilenameFromUrl("/bundle.js");
} catch (err) {
console.log(`Error: ${err}`);
}

if (!resolved) {
console.log("Not found");
return;
}

console.log(`Filename is ${filename}`);
});
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
"release": "standard-version"
},
"dependencies": {
"colorette": "^2.0.10",
"memfs": "^4.43.1",
"mime-types": "^3.0.1",
"on-finished": "^2.4.1",
Expand Down Expand Up @@ -95,14 +94,14 @@
"webpack": "^5.101.0"
},
"peerDependencies": {
"webpack": "^5.0.0"
"webpack": "^5.101.0"
},
"peerDependenciesMeta": {
"webpack": {
"optional": true
}
},
"engines": {
"node": ">= 18.12.0"
"node": ">= 20.9.0"
}
}
14 changes: 6 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,18 @@ const noop = () => {};
* @template {IncomingMessage} [RequestInternal=IncomingMessage]
* @template {ServerResponse} [ResponseInternal=ServerResponse]
* @callback Middleware
* @param {RequestInternal} req
* @param {ResponseInternal} res
* @param {NextFunction} next
* @param {RequestInternal} req request
* @param {ResponseInternal} res response
* @param {NextFunction} next next function
* @returns {Promise<void>}
*/

/** @typedef {import("./utils/getFilenameFromUrl").Extra} Extra */

/**
* @callback GetFilenameFromUrl
* @param {string} url
* @param {Extra=} extra
* @returns {string | undefined}
* @param {string} url request URL
* @returns {{ filename: string, extra: Extra } | undefined} a filename with additional information, or `undefined` if nothing is found
*/

/**
Expand Down Expand Up @@ -278,8 +277,7 @@ function wdm(compiler, options = {}) {
(middleware(filledContext));

// API
instance.getFilenameFromUrl = (url, extra) =>
getFilenameFromUrl(filledContext, url, extra);
instance.getFilenameFromUrl = (url) => getFilenameFromUrl(filledContext, url);

instance.waitUntilValid = (callback = noop) => {
ready(filledContext, callback);
Expand Down
104 changes: 54 additions & 50 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const ready = require("./utils/ready");
/** @typedef {import("./index.js").IncomingMessage} IncomingMessage */
/** @typedef {import("./index.js").ServerResponse} ServerResponse */
/** @typedef {import("./index.js").NormalizedHeaders} NormalizedHeaders */
/** @typedef {import("./utils/getFilenameFromUrl.js").FilenameError} FilenameError */
/** @typedef {import("./utils/getFilenameFromUrl.js").Extra} Extra */
/** @typedef {import("fs").ReadStream} ReadStream */

const BYTES_RANGE_REGEXP = /^ *bytes/i;
Expand Down Expand Up @@ -498,30 +500,37 @@ function wrapper(context) {
*/
async function processRequest() {
// Pipe and SendFile
/** @type {import("./utils/getFilenameFromUrl").Extra} */
const extra = {};
const filename = getFilenameFromUrl(
context,
/** @type {string} */ (getRequestURL(req)),
extra,
);

if (extra.errorCode) {
if (extra.errorCode === 403) {
context.logger.error(`Malicious path "${filename}".`);
/** @type {{ filename: string, extra: Extra } | undefined} */
let resolved;

const requestUrl = /** @type {string} */ (getRequestURL(req));

try {
resolved = getFilenameFromUrl(context, requestUrl);
} catch (err) {
// Fallback to 403 for unknown errors
const errorCode =
typeof err === "object" &&
err !== null &&
typeof (/** @type {FilenameError} */ (err).code) !== "undefined"
? /** @type {FilenameError} */ (err).code
: 403;

if (errorCode === 403) {
context.logger.error(`Malicious path "${requestUrl}".`);
}

await sendError(
extra.errorCode === 400 ? "Bad Request" : "Forbidden",
extra.errorCode,
errorCode === 400 ? "Bad Request" : "Forbidden",
errorCode,
{
modifyResponseData: context.options.modifyResponseData,
},
);
return;
}

if (!filename) {
if (!resolved) {
await goNext();
return;
}
Expand All @@ -531,7 +540,8 @@ function wrapper(context) {
return;
}

const { size } = /** @type {import("fs").Stats} */ (extra.stats);
const { extra, filename } = resolved;
const { size } = extra.stats;

let len = size;
let offset = 0;
Expand Down Expand Up @@ -571,40 +581,36 @@ function wrapper(context) {
}

if (!getResponseHeader(res, "Cache-Control")) {
// TODO enable the `cacheImmutable` by default for the next major release
const cacheControl =
context.options.cacheImmutable && extra.immutable
? { immutable: true }
: context.options.cacheControl;

if (cacheControl) {
let cacheControlValue;

if (typeof cacheControl === "boolean") {
cacheControlValue = "public, max-age=31536000";
} else if (typeof cacheControl === "number") {
const maxAge = Math.floor(
Math.min(Math.max(0, cacheControl), MAX_MAX_AGE) / 1000,
);
const { cacheControl, cacheImmutable } = context.options;

cacheControlValue = `public, max-age=${maxAge}`;
} else if (typeof cacheControl === "string") {
cacheControlValue = cacheControl;
} else {
const maxAge = cacheControl.maxAge
? Math.floor(
Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE) /
1000,
)
: MAX_MAX_AGE / 1000;

cacheControlValue = `public, max-age=${maxAge}`;

if (cacheControl.immutable) {
cacheControlValue += ", immutable";
}
let cacheControlValue;

if (
(cacheImmutable === undefined || cacheImmutable) &&
extra.immutable
) {
cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}, immutable`;
} else if (typeof cacheControl === "boolean") {
cacheControlValue = `public, max-age=${Math.floor(MAX_MAX_AGE / 1000)}`;
} else if (typeof cacheControl === "number") {
const maxAge = Math.min(Math.max(0, cacheControl), MAX_MAX_AGE);
cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;
} else if (typeof cacheControl === "string") {
cacheControlValue = cacheControl;
} else if (cacheControl) {
const maxAge =
cacheControl.maxAge !== undefined
? Math.min(Math.max(0, cacheControl.maxAge), MAX_MAX_AGE)
: MAX_MAX_AGE;

cacheControlValue = `public, max-age=${Math.floor(maxAge / 1000)}`;

if (cacheControl.immutable) {
cacheControlValue += ", immutable";
}
}

if (cacheControlValue) {
setResponseHeader(res, "Cache-Control", cacheControlValue);
}
}
Expand All @@ -613,9 +619,7 @@ function wrapper(context) {
context.options.lastModified &&
!getResponseHeader(res, "Last-Modified")
) {
const modified =
/** @type {import("fs").Stats} */
(extra.stats).mtime.toUTCString();
const modified = extra.stats.mtime.toUTCString();

setResponseHeader(res, "Last-Modified", modified);
}
Expand Down Expand Up @@ -671,7 +675,7 @@ function wrapper(context) {
const result = await getETag()(
isStrongETag
? /** @type {Buffer | ReadStream} */ (bufferOrStream)
: /** @type {import("fs").Stats} */ (extra.stats),
: extra.stats,
);

// Because we already read stream, we can cache buffer to avoid extra read from fs
Expand Down
Loading