diff --git a/.changeset/clever-eagles-live.md b/.changeset/clever-eagles-live.md new file mode 100644 index 000000000000..100f311647d7 --- /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 05ec77ce77e7..b8e12614756a 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 fc87605e6137..2132e79d9a73 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 5643b87b3dce..da40d19444c6 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 + }); + + for await (const file of files) { + await 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 73a6c004a63c..1bb0742ac973 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 693d088d4c12..f8e3c51ecaab 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==}