diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index a1e057d3..b1a040f8 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -9,7 +9,7 @@ import {posix} from 'path'; import {parse} from 'url'; import bind from 'bind-decorator'; -import gzipStatic, {RequestHandler} from 'connect-gzip-static'; +import expressStaticGzip, {RequestHandler} from 'express-static-gzip'; import finalhandler from 'finalhandler'; import stringify from 'json-stable-stringify-without-jsonify'; import WebSocket from 'ws'; @@ -73,13 +73,16 @@ export default class Frontend extends Extension { override async start(): Promise { /* istanbul ignore next */ const options = { - setHeaders: (res: ServerResponse, path: string): void => { - if (path.endsWith('index.html')) { - res.setHeader('Cache-Control', 'no-store'); - } + enableBrotli: true, + serveStatic: { + setHeaders: (res: ServerResponse, path: string): void => { + if (path.endsWith('index.html')) { + res.setHeader('Cache-Control', 'no-store'); + } + }, }, }; - this.fileServer = gzipStatic(frontend.getPath(), options); + this.fileServer = expressStaticGzip(frontend.getPath(), options); this.wss = new WebSocket.Server({noServer: true, path: posix.join(this.baseUrl, 'api')}); this.wss.on('connection', this.onWebSocketConnection); @@ -133,6 +136,7 @@ export default class Frontend extends Extension { // This is necessary for the browser to resolve relative assets paths correctly. request.originalUrl = request.url; request.url = '/' + newUrl; + request.path = request.url; this.fileServer(request, response, fin); } diff --git a/lib/types/zigbee2mqtt-frontend.d.ts b/lib/types/zigbee2mqtt-frontend.d.ts index fff7b51d..a38c2f12 100644 --- a/lib/types/zigbee2mqtt-frontend.d.ts +++ b/lib/types/zigbee2mqtt-frontend.d.ts @@ -5,11 +5,12 @@ declare module 'zigbee2mqtt-frontend' { declare module 'http' { interface IncomingMessage { originalUrl?: string; + path?: string; } } -declare module 'connect-gzip-static' { +declare module 'express-static-gzip' { import {IncomingMessage, ServerResponse} from 'http'; export type RequestHandler = (req: IncomingMessage, res: ServerResponse, finalhandler: (err: unknown) => void) => void; - export default function gzipStatic(root: string, options?: Record): RequestHandler; + export default function expressStaticGzip(root: string, options?: Record): RequestHandler; } diff --git a/package-lock.json b/package-lock.json index 1368cb45..bd0da0df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "ajv": "^8.17.1", "bind-decorator": "^1.0.11", - "connect-gzip-static": "3.0.1", "debounce": "^2.2.0", + "express-static-gzip": "^2.1.8", "fast-deep-equal": "^3.1.3", "finalhandler": "^1.3.1", "git-last-commit": "^1.0.1", @@ -57,6 +57,7 @@ "@types/object-assign-deep": "^0.4.3", "@types/readable-stream": "4.0.18", "@types/sd-notify": "^2.8.2", + "@types/serve-static": "^1.15.7", "@types/ws": "8.5.13", "babel-jest": "^29.7.0", "eslint": "^9.14.0", @@ -68,7 +69,7 @@ "typescript-eslint": "^8.12.2" }, "engines": { - "node": "^18 || ^20 || ^22" + "node": "^18 || ^20 || ^22 || ^23" }, "optionalDependencies": { "sd-notify": "^2.8.0" @@ -2149,20 +2150,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@folder/readdir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@folder/readdir/-/readdir-3.1.0.tgz", - "integrity": "sha512-mqkVdQ77BcCOWur/dxoPxjJS2eJg8Eqqx1Dgdc/qbTeGI5UMPDLmT0peGEjxLdlnD9SvhMnpJuiyaYl2btdT6A==", - "funding": [ - "https://github.com/sponsors/jonschlinkert", - "https://paypal.me/jonathanschlinkert", - "https://jonschlinkert.dev/sponsor" - ], - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3056,6 +3043,13 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/humanize-duration": { "version": "3.27.4", "resolved": "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.4.tgz", @@ -3115,6 +3109,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.8.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", @@ -3148,6 +3149,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -4182,19 +4206,6 @@ "node": ">= 6" } }, - "node_modules/connect-gzip-static": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/connect-gzip-static/-/connect-gzip-static-3.0.1.tgz", - "integrity": "sha512-VVqi1fJ4uZK8aIrb1BN9n7tsRAbc3BhkIo/mz1VuNhYC2KDuP1ZgCDapjYIfT3mcGgWzyUr04kv7nmnGOnCvWg==", - "license": "MIT", - "dependencies": { - "@folder/readdir": "^3.1.0", - "debug": "~2||~3||~4", - "parseurl": "~1", - "send": "~0", - "serve-static": "~1" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4813,6 +4824,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express-static-gzip": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz", + "integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==", + "license": "MIT", + "dependencies": { + "serve-static": "^1.16.2" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7563,45 +7583,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", - "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", diff --git a/package.json b/package.json index c0bf9e06..43a3847d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/Koenkk/zigbee2mqtt.git" }, "engines": { - "node": "^18 || ^20 || ^22" + "node": "^18 || ^20 || ^22 || ^23" }, "keywords": [ "xiaomi", @@ -39,8 +39,8 @@ "dependencies": { "ajv": "^8.17.1", "bind-decorator": "^1.0.11", - "connect-gzip-static": "3.0.1", "debounce": "^2.2.0", + "express-static-gzip": "^2.1.8", "fast-deep-equal": "^3.1.3", "finalhandler": "^1.3.1", "git-last-commit": "^1.0.1", @@ -82,6 +82,7 @@ "@types/object-assign-deep": "^0.4.3", "@types/readable-stream": "4.0.18", "@types/sd-notify": "^2.8.2", + "@types/serve-static": "^1.15.7", "@types/ws": "8.5.13", "babel-jest": "^29.7.0", "eslint": "^9.14.0", diff --git a/test/frontend.test.js b/test/frontend.test.js index 6a13c6de..e7321b3e 100644 --- a/test/frontend.test.js +++ b/test/frontend.test.js @@ -86,7 +86,7 @@ jest.mock('https', () => ({ Agent: jest.fn(), })); -jest.mock('connect-gzip-static', () => +jest.mock('express-static-gzip', () => jest.fn().mockImplementation((path) => { mockNodeStatic.variables.path = path; return mockNodeStatic.implementation; @@ -321,7 +321,11 @@ describe('Frontend', () => { mockHTTP.variables.onRequest({url: '/file.txt'}, 2); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + expect(mockNodeStatic.implementation).toHaveBeenCalledWith( + {originalUrl: '/file.txt', url: '/file.txt', path: '/file.txt'}, + 2, + expect.any(Function), + ); }); it('Static server', async () => { @@ -367,14 +371,18 @@ describe('Frontend', () => { mockHTTP.variables.onRequest({url: '/z2m'}, 2); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m', url: '/'}, 2, expect.any(Function)); + expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m', url: '/', path: '/'}, 2, expect.any(Function)); expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); mockNodeStatic.implementation.mockReset(); expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); mockHTTP.variables.onRequest({url: '/z2m/file.txt'}, 2); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m/file.txt', url: '/file.txt'}, 2, expect.any(Function)); + expect(mockNodeStatic.implementation).toHaveBeenCalledWith( + {originalUrl: '/z2m/file.txt', url: '/file.txt', path: '/file.txt'}, + 2, + expect.any(Function), + ); expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); mockNodeStatic.implementation.mockReset(); @@ -393,7 +401,11 @@ describe('Frontend', () => { mockHTTP.variables.onRequest({url: '/z2m-more++/c0mplex.url'}, 2); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith({originalUrl: '/z2m-more++/c0mplex.url', url: '/'}, 2, expect.any(Function)); + expect(mockNodeStatic.implementation).toHaveBeenCalledWith( + {originalUrl: '/z2m-more++/c0mplex.url', url: '/', path: '/'}, + 2, + expect.any(Function), + ); expect(mockFinalHandler.implementation).not.toHaveBeenCalledWith(); mockNodeStatic.implementation.mockReset(); @@ -401,7 +413,7 @@ describe('Frontend', () => { mockHTTP.variables.onRequest({url: '/z2m-more++/c0mplex.url/file.txt'}, 2); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); expect(mockNodeStatic.implementation).toHaveBeenCalledWith( - {originalUrl: '/z2m-more++/c0mplex.url/file.txt', url: '/file.txt'}, + {originalUrl: '/z2m-more++/c0mplex.url/file.txt', url: '/file.txt', path: '/file.txt'}, 2, expect.any(Function), );