diff --git a/.changeset/clever-eagles-live.md b/.changeset/clever-eagles-live.md new file mode 100644 index 000000000000..63ba236f905a --- /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 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 3a0a36309205..095109c10572 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -1,14 +1,21 @@ -import esbuild from 'esbuild'; -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'); @@ -39,8 +51,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 73a6c004a63c..a0af401eb335 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 26c2d099d7ed..ff35aa835a5f 100644 --- a/packages/adapter-node/src/server.js +++ b/packages/adapter-node/src/server.js @@ -17,12 +17,6 @@ const paths = { }; export function createServer({ render }) { - const mutable = (dir) => - sirv(dir, { - etag: true, - maxAge: 0 - }); - const immutable_path = (pathname) => { // eslint-disable-next-line no-undef let app_dir = esbuild_app_dir; @@ -43,7 +37,12 @@ export function createServer({ render }) { }; const prerendered_handler = fs.existsSync(paths.prerendered) - ? mutable(paths.prerendered) + ? sirv(paths.prerendered, { + 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 693d088d4c12..84e87598f119 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'}