From fb3ed4d400d569f8bd172e0322aeb88dfd8424ba Mon Sep 17 00:00:00 2001 From: Sascha Tandel Date: Tue, 15 Jun 2021 09:53:32 +0200 Subject: [PATCH] feat(adapter-node): precompress (gzip & brotli) assets sirv supports using precompressed assets but they are not generated during the build. This PR precompresses html, js, json, css, svg, and xml assets using gzip and brotli during the adapt phase which allows sirv to use these. --- .changeset/clever-eagles-live.md | 5 +++ packages/adapter-node/README.md | 5 +++ packages/adapter-node/index.d.ts | 5 ++- packages/adapter-node/index.js | 60 ++++++++++++++++++++++++++++- packages/adapter-node/package.json | 3 +- packages/adapter-node/src/server.js | 13 +++---- pnpm-lock.yaml | 11 +++++- 7 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 .changeset/clever-eagles-live.md diff --git a/.changeset/clever-eagles-live.md b/.changeset/clever-eagles-live.md new file mode 100644 index 0000000000000..63ba236f905a0 --- /dev/null +++ b/.changeset/clever-eagles-live.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +precompress assets and prerendered pages (html,js,json,css,svg,xml) diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index 05ec77ce77e79..b8e12614756ae 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -15,6 +15,7 @@ export default { adapter: adapter({ // default options are shown out: 'build' + precompress: false, }) } }; @@ -26,6 +27,10 @@ export default { The directory to build the server to. It defaults to `build` — i.e. `node build` would start the server locally after it has been created. +### precompress + +Enables precompressing using gzip and brotli for assets and prerendered pages. It defaults to `false`. + ## Environment variables By default, the server will accept connections on `0.0.0.0` using port 3000. These can be customised with the `PORT` and `HOST` environment variables: diff --git a/packages/adapter-node/index.d.ts b/packages/adapter-node/index.d.ts index fc87605e6137c..2132e79d9a736 100644 --- a/packages/adapter-node/index.d.ts +++ b/packages/adapter-node/index.d.ts @@ -1,3 +1,6 @@ -declare function plugin(options?: { out?: string }): import('@sveltejs/kit').Adapter; +declare function plugin(options?: { + out?: string; + precompress?: boolean; +}): import('@sveltejs/kit').Adapter; export = plugin; diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 5643b87b3dce9..5620620c7cf5a 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,14 +1,21 @@ -import { readFileSync } from 'fs'; +import { readFileSync, statSync, createReadStream, createWriteStream } from 'fs'; import { join } from 'path'; import { fileURLToPath } from 'url'; +import { pipeline } from 'stream'; +import { promisify } from 'util'; +import zlib from 'zlib'; import esbuild from 'esbuild'; +import glob from 'tiny-glob'; + +const pipe = promisify(pipeline); /** * @param {{ * out?: string; + * precompress?: boolean * }} options */ -export default function ({ out = 'build' } = {}) { +export default function ({ out = 'build', precompress } = {}) { /** @type {import('@sveltejs/kit').Adapter} */ const adapter = { name: '@sveltejs/adapter-node', @@ -19,6 +26,11 @@ export default function ({ out = 'build' } = {}) { utils.copy_client_files(static_directory); utils.copy_static_files(static_directory); + if (precompress) { + utils.log.minor('Compressing assets'); + await compress(static_directory); + } + utils.log.minor('Building server'); const files = fileURLToPath(new URL('./files', import.meta.url)); utils.copy(files, '.svelte-kit/node'); @@ -36,8 +48,52 @@ export default function ({ out = 'build' } = {}) { await utils.prerender({ dest: `${out}/prerendered` }); + if (precompress) { + utils.log.minor('Compressing prerendered pages'); + await compress(`${out}/prerendered`); + } } }; return adapter; } + +/** + * @param {string} directory + */ +async function compress(directory) { + const files = await glob('**/*.{html,js,json,css,svg,xml}', { + cwd: directory, + dot: true, + absolute: true, + filesOnly: true + }); + + await Promise.all( + files.map((file) => Promise.all([compress_file(file, 'gz'), compress_file(file, 'br')])) + ); +} + +/** + * @param {string} file + * @param {'gz' | 'br'} format + */ +async function compress_file(file, format = 'gz') { + const compress = + format == 'br' + ? zlib.createBrotliCompress({ + params: { + [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, + [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, + [zlib.constants.BROTLI_PARAM_SIZE_HINT]: statSync(file).size + } + }) + : zlib.createGzip({ + level: zlib.constants.Z_BEST_COMPRESSION + }); + + const source = createReadStream(file); + const destination = createWriteStream(`${file}.${format}`); + + await pipe(source, compress, destination); +} diff --git a/packages/adapter-node/package.json b/packages/adapter-node/package.json index 73a6c004a63cb..a0af401eb335b 100644 --- a/packages/adapter-node/package.json +++ b/packages/adapter-node/package.json @@ -21,7 +21,8 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "esbuild": "^0.12.5" + "esbuild": "^0.12.5", + "tiny-glob": "^0.2.9" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", diff --git a/packages/adapter-node/src/server.js b/packages/adapter-node/src/server.js index ce8cb5f20f4c5..fdcab0891d18e 100644 --- a/packages/adapter-node/src/server.js +++ b/packages/adapter-node/src/server.js @@ -17,14 +17,13 @@ const paths = { }; export function createServer({ render }) { - const mutable = (dir) => - sirv(dir, { - etag: true, - maxAge: 0 - }); - const prerendered_handler = fs.existsSync(paths.prerendered) - ? mutable(paths.prerendered) + ? sirv(paths.assets, { + etag: true, + maxAge: 0, + gzip: true, + brotli: true + }) : noop_handler; const assets_handler = fs.existsSync(paths.assets) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 693d088d4c12d..84e87598f1195 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,10 +100,12 @@ importers: polka: ^1.0.0-next.15 rollup: ^2.47.0 sirv: ^1.0.12 + tiny-glob: ^0.2.9 typescript: ^4.2.4 uvu: ^0.5.1 dependencies: esbuild: 0.12.5 + tiny-glob: 0.2.9 devDependencies: '@rollup/plugin-json': 4.1.0_rollup@2.47.0 '@sveltejs/kit': link:../kit @@ -1970,7 +1972,6 @@ packages: /globalyzer/0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - dev: true /globby/11.0.3: resolution: {integrity: sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==} @@ -1986,7 +1987,6 @@ packages: /globrex/0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - dev: true /graceful-fs/4.2.6: resolution: {integrity: sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==} @@ -3566,6 +3566,13 @@ packages: globrex: 0.1.2 dev: true + /tiny-glob/0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + dev: false + /tmp/0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'}