From 9b875f47fb14aba70feda68455bedda774ce85a8 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 | 59 +++++++++++++++++++++++++++++- packages/adapter-node/package.json | 3 +- pnpm-lock.yaml | 19 +--------- 6 files changed, 75 insertions(+), 21 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..100f311647d79 --- /dev/null +++ b/.changeset/clever-eagles-live.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/adapter-node': patch +--- + +precompress assets (html,js,css,svg,xml) for sirv 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..4ba4adbecde0d 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 fg from 'fast-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('Precompressing 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,51 @@ export default function ({ out = 'build' } = {}) { await utils.prerender({ dest: `${out}/prerendered` }); + if (precompress) { + await compress(`${out}/prerendered`); + } } }; return adapter; } + +/** + * @param {string} directory + */ +async function compress(directory) { + const files = await fg('*.{html,js,json,css,svg,xml}', { + cwd: directory, + dot: true, + absolute: true, + baseNameMatch: 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..1bb0742ac973f 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", + "fast-glob": "^3.2.5" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 693d088d4c12d..f8e3c51ecaaba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,6 +96,7 @@ importers: c8: ^7.7.2 compression: ^1.7.4 esbuild: ^0.12.5 + fast-glob: ^3.2.5 node-fetch: ^3.0.0-beta.9 polka: ^1.0.0-next.15 rollup: ^2.47.0 @@ -104,6 +105,7 @@ importers: uvu: ^0.5.1 dependencies: esbuild: 0.12.5 + fast-glob: 3.2.5 devDependencies: '@rollup/plugin-json': 4.1.0_rollup@2.47.0 '@sveltejs/kit': link:../kit @@ -548,12 +550,10 @@ packages: dependencies: '@nodelib/fs.stat': 2.0.4 run-parallel: 1.2.0 - dev: true /@nodelib/fs.stat/2.0.4: resolution: {integrity: sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==} engines: {node: '>= 8'} - dev: true /@nodelib/fs.walk/1.2.6: resolution: {integrity: sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==} @@ -561,7 +561,6 @@ packages: dependencies: '@nodelib/fs.scandir': 2.1.4 fastq: 1.11.0 - dev: true /@polka/url/1.0.0-next.15: resolution: {integrity: sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA==} @@ -1086,7 +1085,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /breakword/1.0.5: resolution: {integrity: sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==} @@ -1775,7 +1773,6 @@ packages: merge2: 1.4.1 micromatch: 4.0.4 picomatch: 2.2.3 - dev: true /fast-json-stable-stringify/2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -1789,7 +1786,6 @@ packages: resolution: {integrity: sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==} dependencies: reusify: 1.0.4 - dev: true /fd-slicer/1.1.0: resolution: {integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=} @@ -1819,7 +1815,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /find-up/2.1.0: resolution: {integrity: sha1-RdG35QbHF93UgndaK3eSCjwMV6c=} @@ -1941,7 +1936,6 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.1 - dev: true /glob/7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} @@ -2140,7 +2134,6 @@ packages: /is-extglob/2.1.1: resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} engines: {node: '>=0.10.0'} - dev: true /is-fullwidth-code-point/2.0.0: resolution: {integrity: sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=} @@ -2157,7 +2150,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 - dev: true /is-module/1.0.0: resolution: {integrity: sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=} @@ -2176,7 +2168,6 @@ packages: /is-number/7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-plain-obj/1.1.0: resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=} @@ -2456,7 +2447,6 @@ packages: /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - dev: true /micromatch/4.0.4: resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} @@ -2464,7 +2454,6 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.2.3 - dev: true /mime-db/1.47.0: resolution: {integrity: sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==} @@ -2962,7 +2951,6 @@ packages: /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true /quick-lru/4.0.1: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} @@ -3086,7 +3074,6 @@ packages: /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -3106,7 +3093,6 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 - dev: true /sade/1.7.4: resolution: {integrity: sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==} @@ -3578,7 +3564,6 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /totalist/1.1.0: resolution: {integrity: sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==}