From 6b12d345a75cb5f1a673f9e6b402cb25160851a6 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Sat, 11 Mar 2023 15:21:57 +0800 Subject: [PATCH] refactor(cloud): send compressed spa file when possible --- packages/cloud/package.json | 4 +- packages/cloud/src/middleware/with-spa.ts | 46 ++++++++++++++++++++--- packages/core/package.json | 2 +- packages/integration-tests/package.json | 2 +- packages/schemas/package.json | 2 +- pnpm-lock.yaml | 34 +++++++++-------- 6 files changed, 64 insertions(+), 26 deletions(-) diff --git a/packages/cloud/package.json b/packages/cloud/package.json index b0a547921..487844e19 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -29,7 +29,8 @@ "@logto/shared": "workspace:*", "@silverhand/essentials": "2.4.0", "@withtyped/postgres": "^0.8.1", - "@withtyped/server": "^0.8.0", + "@withtyped/server": "^0.8.1", + "accepts": "^1.3.8", "chalk": "^5.0.0", "decamelize": "^6.0.0", "dotenv": "^16.0.0", @@ -44,6 +45,7 @@ "@silverhand/eslint-config": "2.0.1", "@silverhand/jest-config": "^2.0.1", "@silverhand/ts-config": "2.0.3", + "@types/accepts": "^1.3.5", "@types/http-proxy": "^1.17.9", "@types/jest": "^29.4.0", "@types/mime-types": "^2.1.1", diff --git a/packages/cloud/src/middleware/with-spa.ts b/packages/cloud/src/middleware/with-spa.ts index 69d6c6231..267bed713 100644 --- a/packages/cloud/src/middleware/with-spa.ts +++ b/packages/cloud/src/middleware/with-spa.ts @@ -1,9 +1,11 @@ import { createReadStream } from 'node:fs'; import fs from 'node:fs/promises'; +import type { IncomingMessage } from 'node:http'; import path from 'node:path'; -import { assert } from '@silverhand/essentials'; -import type { NextFunction, RequestContext } from '@withtyped/server'; +import { assert, conditional } from '@silverhand/essentials'; +import type { HttpContext, NextFunction, RequestContext } from '@withtyped/server'; +import accepts from 'accepts'; import mime from 'mime-types'; import { matchPathname } from '#src/utils/url.js'; @@ -39,7 +41,11 @@ export default function withSpa({ }: WithSpaConfig) { assert(root, new Error('Root directory is required to serve files.')); - return async (context: InputContext, next: NextFunction) => { + return async ( + context: InputContext, + next: NextFunction, + { request }: HttpContext + ) => { const { headers, request: { url }, @@ -63,14 +69,15 @@ export default function withSpa({ return next({ ...context, status: 404 }); } - const [pathLike, stat] = result; + const [pathLike, stat, compression] = (await tryCompressedFile(request, result[0])) ?? result; return next({ ...context, headers: { ...headers, - 'Content-Length': stat.size, - 'Content-Type': mime.lookup(pathLike), + ...(compression && { 'Content-Encoding': compression }), + ...(!compression && { 'Content-Length': stat.size }), + 'Content-Type': mime.lookup(result[0]), // Use the original path to lookup 'Last-Modified': stat.mtime.toUTCString(), 'Cache-Control': `max-age=${maxAge}`, ETag: `"${stat.size.toString(16)}-${stat.mtimeMs.toString(16)}"`, @@ -81,6 +88,33 @@ export default function withSpa({ }; } +type CompressionEncoding = keyof typeof compressionExtensions; + +const compressionExtensions = { + br: 'br', + gzip: 'gz', +} as const; + +const compressionEncodings = Object.freeze(Object.keys(compressionExtensions)); + +const isValidEncoding = (value?: string): value is CompressionEncoding => + Boolean(value && compressionEncodings.includes(value)); + +const tryCompressedFile = async (request: IncomingMessage, pathLike: string) => { + // Honor the compression preference + const compression = conditional(accepts(request).encodings([...compressionEncodings])); + + if (!isValidEncoding(compression)) { + return; + } + + const result = await tryStat(pathLike + '.' + compressionExtensions[compression]); + + if (result) { + return [...result, compression] as const; + } +}; + const tryStat = async (pathLike: string) => { try { const stat = await fs.stat(pathLike); diff --git a/packages/core/package.json b/packages/core/package.json index 43f18cd3a..ec672455f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,7 @@ "@logto/shared": "workspace:*", "@silverhand/essentials": "2.4.0", "@withtyped/postgres": "^0.8.1", - "@withtyped/server": "^0.8.0", + "@withtyped/server": "^0.8.1", "aws-sdk": "^2.1329.0", "chalk": "^5.0.0", "clean-deep": "^3.4.0", diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 8760dfeda..bfc1d9249 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -55,6 +55,6 @@ }, "prettier": "@silverhand/eslint-config/.prettierrc", "dependencies": { - "@withtyped/server": "^0.8.0" + "@withtyped/server": "^0.8.1" } } diff --git a/packages/schemas/package.json b/packages/schemas/package.json index e62ad1bf6..c9803afb2 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -85,7 +85,7 @@ "@logto/language-kit": "workspace:*", "@logto/phrases": "workspace:*", "@logto/phrases-ui": "workspace:*", - "@withtyped/server": "^0.8.0", + "@withtyped/server": "^0.8.1", "zod": "^3.20.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26524dc16..2a832a863 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,12 +116,14 @@ importers: '@silverhand/essentials': 2.4.0 '@silverhand/jest-config': ^2.0.1 '@silverhand/ts-config': 2.0.3 + '@types/accepts': ^1.3.5 '@types/http-proxy': ^1.17.9 '@types/jest': ^29.4.0 '@types/mime-types': ^2.1.1 '@types/node': ^18.11.18 '@withtyped/postgres': ^0.8.1 - '@withtyped/server': ^0.8.0 + '@withtyped/server': ^0.8.1 + accepts: ^1.3.8 chalk: ^5.0.0 decamelize: ^6.0.0 dotenv: ^16.0.0 @@ -143,8 +145,9 @@ importers: '@logto/schemas': link:../schemas '@logto/shared': link:../shared '@silverhand/essentials': 2.4.0 - '@withtyped/postgres': 0.8.1_@withtyped+server@0.8.0 - '@withtyped/server': 0.8.0 + '@withtyped/postgres': 0.8.1_@withtyped+server@0.8.1 + '@withtyped/server': 0.8.1 + accepts: 1.3.8 chalk: 5.1.2 decamelize: 6.0.0 dotenv: 16.0.0 @@ -158,6 +161,7 @@ importers: '@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy '@silverhand/jest-config': 2.0.1_jest@29.5.0 '@silverhand/ts-config': 2.0.3_typescript@4.9.4 + '@types/accepts': 1.3.5 '@types/http-proxy': 1.17.9 '@types/jest': 29.4.0 '@types/mime-types': 2.1.1 @@ -358,7 +362,7 @@ importers: '@types/sinon': ^10.0.13 '@types/supertest': ^2.0.11 '@withtyped/postgres': ^0.8.1 - '@withtyped/server': ^0.8.0 + '@withtyped/server': ^0.8.1 aws-sdk: ^2.1329.0 chalk: ^5.0.0 clean-deep: ^3.4.0 @@ -421,8 +425,8 @@ importers: '@logto/schemas': link:../schemas '@logto/shared': link:../shared '@silverhand/essentials': 2.4.0 - '@withtyped/postgres': 0.8.1_@withtyped+server@0.8.0 - '@withtyped/server': 0.8.0 + '@withtyped/postgres': 0.8.1_@withtyped+server@0.8.1 + '@withtyped/server': 0.8.1 aws-sdk: 2.1329.0 chalk: 5.1.2 clean-deep: 3.4.0 @@ -575,7 +579,7 @@ importers: '@types/jest': ^29.4.0 '@types/jest-environment-puppeteer': ^5.0.3 '@types/node': ^18.11.18 - '@withtyped/server': ^0.8.0 + '@withtyped/server': ^0.8.1 dotenv: ^16.0.0 eslint: ^8.34.0 got: ^12.5.3 @@ -589,7 +593,7 @@ importers: text-encoder: ^0.0.4 typescript: ^4.9.4 dependencies: - '@withtyped/server': 0.8.0 + '@withtyped/server': 0.8.1 devDependencies: '@jest/types': 29.1.2 '@logto/connector-kit': link:../toolkit/connector-kit @@ -683,7 +687,7 @@ importers: '@types/jest': ^29.4.0 '@types/node': ^18.11.18 '@types/pluralize': ^0.0.29 - '@withtyped/server': ^0.8.0 + '@withtyped/server': ^0.8.1 camelcase: ^7.0.0 chalk: ^5.0.0 eslint: ^8.34.0 @@ -702,7 +706,7 @@ importers: '@logto/language-kit': link:../toolkit/language-kit '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui - '@withtyped/server': 0.8.0 + '@withtyped/server': 0.8.1 zod: 3.20.2 devDependencies: '@silverhand/eslint-config': 2.0.1_kjzxg5porcw5dx54sezsklj5cy @@ -4632,21 +4636,21 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@withtyped/postgres/0.8.1_@withtyped+server@0.8.0: + /@withtyped/postgres/0.8.1_@withtyped+server@0.8.1: resolution: {integrity: sha512-BkX1SPDV8bZFn1LEI6jzTes+y/4BGuPEBOi8p6jH9ZBySTuaW0/JqgEb1aKk5GtJ0aFGXhgq4Fk+JkLOc6zehA==} peerDependencies: '@withtyped/server': ^0.8.0 dependencies: '@types/pg': 8.6.6 - '@withtyped/server': 0.8.0 + '@withtyped/server': 0.8.1 '@withtyped/shared': 0.2.0 pg: 8.8.0 transitivePeerDependencies: - pg-native dev: false - /@withtyped/server/0.8.0: - resolution: {integrity: sha512-p9gRdEvUNBJ0X15jB4xZutMkzjF19EoBrGjvmaokbuO4+Ub5CSpfV/SOl+7vob8cAgIdWVriZfDGD73ZH0YWJQ==} + /@withtyped/server/0.8.1: + resolution: {integrity: sha512-gNJ0lmwYiAScb7oQWu7BHyy+PT2/j34kGWIjElyfrYZP7JWpZSrEyxaVwDFRo/oA9vzru3AOnPFTMcjmPq4AKg==} dependencies: '@withtyped/shared': 0.2.0 dev: false @@ -4689,7 +4693,6 @@ packages: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 - dev: true /acorn-globals/7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -10774,7 +10777,6 @@ packages: /negotiator/0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - dev: true /nise/5.1.3: resolution: {integrity: sha512-U597iWTTBBYIV72986jyU382/MMZ70ApWcRmkoF1AZ75bpqOtI3Gugv/6+0jLgoDOabmcSwYBkSSAWIp1eA5cg==}