diff --git a/documentation/faq/80-integrations.md b/documentation/faq/80-integrations.md index 4db8cd6667f8..65c787c5e180 100644 --- a/documentation/faq/80-integrations.md +++ b/documentation/faq/80-integrations.md @@ -67,7 +67,7 @@ Put the code to query your database in [endpoints](/docs#routing-endpoints) - do ### How do I use middleware? -In dev, you can add middleware to Vite by using a Vite plugin. For example: +`adapter-node` builds a middleware that use can use with your own server for production mode. In dev, you can add middleware to Vite by using a Vite plugin. For example: ```js const myPlugin = { diff --git a/packages/adapter-node/README.md b/packages/adapter-node/README.md index 1b90a76740f0..76ade0a730e3 100644 --- a/packages/adapter-node/README.md +++ b/packages/adapter-node/README.md @@ -45,6 +45,34 @@ HOST=127.0.0.1 PORT=4000 node build You can specify different environment variables if necessary using the `env` option. +## Middleware + +The adapter exports a middleware `(req, res, next) => {}` that's compatible with [Express](https://github.com/expressjs/expressjs.com) / [Connect](https://github.com/senchalabs/connect) / [Polka](https://github.com/lukeed/polka). Additionally, it also exports a reference server implementation using this middleware with a plain Node HTTP server. + +But you can use your favorite server framework to combine it with other middleware and server logic. You can import `kitMiddleware`, your ready-to-use SvelteKit bundle as middleware, from `./build/middlewares.js`. + +``` +import { assetsMiddleware, prerenderedMiddleware, kitMiddleware } from './build/middlewares.js'; +import polka from 'polka'; + +const app = polka(); + +const myMiddleware = function(req, res, next) { + console.log('Hello world!'); + next(); +}; + +app.use(myMiddleware); + +app.get('/no-svelte', (req, res) => { + res.end('This is not Svelte!') +}); + +app.use(assetsMiddleware, prerenderedMiddleware, kitMiddleware); + +app.listen(3000) +``` + ## Advanced Configuration ### esbuild diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index ae7cad98ab59..02e268491bff 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -41,7 +41,7 @@ export default function ({ await compress(static_directory); } - utils.log.minor('Building server'); + utils.log.minor('Building SvelteKit middleware'); const files = fileURLToPath(new URL('./files', import.meta.url)); utils.copy(files, '.svelte-kit/node'); writeFileSync( @@ -54,10 +54,11 @@ export default function ({ port_env )}] || (!path && 3000);` ); + /** @type {BuildOptions} */ const defaultOptions = { - entryPoints: ['.svelte-kit/node/index.js'], - outfile: join(out, 'index.js'), + entryPoints: ['.svelte-kit/node/middlewares.js'], + outfile: join(out, 'middlewares.js'), bundle: true, external: Object.keys(JSON.parse(readFileSync('package.json', 'utf8')).dependencies || {}), format: 'esm', @@ -71,6 +72,32 @@ export default function ({ const buildOptions = esbuildConfig ? await esbuildConfig(defaultOptions) : defaultOptions; await esbuild.build(buildOptions); + utils.log.minor('Building SvelteKit reference server'); + /** @type {BuildOptions} */ + const defaultOptionsRefServer = { + entryPoints: ['.svelte-kit/node/index.js'], + outfile: join(out, 'index.js'), + bundle: true, + external: ['./middlewares.js'], // does not work, eslint does not exclude middlewares from target + format: 'esm', + platform: 'node', + target: 'node12', + // external exclude workaround, see https://github.com/evanw/esbuild/issues/514 + plugins: [ + { + name: 'fix-middlewares-exclude', + setup(build) { + // Match an import called "./middlewares.js" and mark it as external + build.onResolve({ filter: /^\.\/middlewares\.js$/ }, () => ({ external: true })); + } + } + ] + }; + const buildOptionsRefServer = esbuildConfig + ? await esbuildConfig(defaultOptionsRefServer) + : defaultOptionsRefServer; + await esbuild.build(buildOptionsRefServer); + utils.log.minor('Prerendering static pages'); await utils.prerender({ dest: `${out}/prerendered` diff --git a/packages/adapter-node/rollup.config.js b/packages/adapter-node/rollup.config.js index 51c98aa0f083..619756ee6c1e 100644 --- a/packages/adapter-node/rollup.config.js +++ b/packages/adapter-node/rollup.config.js @@ -3,6 +3,16 @@ import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; export default [ + { + input: 'src/middlewares.js', + output: { + file: 'files/middlewares.js', + format: 'esm', + sourcemap: true + }, + plugins: [nodeResolve(), commonjs(), json()], + external: ['../output/server/app.js', ...require('module').builtinModules] + }, { input: 'src/index.js', output: { @@ -11,7 +21,7 @@ export default [ sourcemap: true }, plugins: [nodeResolve(), commonjs(), json()], - external: ['../output/server/app.js', './env.js', ...require('module').builtinModules] + external: ['./middlewares.js', './env.js', ...require('module').builtinModules] }, { input: 'src/shims.js', diff --git a/packages/adapter-node/src/index.js b/packages/adapter-node/src/index.js index 4db8b8be6723..b8e807ea8450 100644 --- a/packages/adapter-node/src/index.js +++ b/packages/adapter-node/src/index.js @@ -1,17 +1,22 @@ -// TODO hardcoding the relative location makes this brittle -// @ts-ignore -import { init, render } from '../output/server/app.js'; // @ts-ignore import { path, host, port } from './env.js'; -import { createServer } from './server'; - -init(); +import { assetsMiddleware, kitMiddleware, prerenderedMiddleware } from './middlewares.js'; +import compression from 'compression'; +import polka from 'polka'; -const instance = createServer({ render }); +const server = polka().use( + // https://github.com/lukeed/polka/issues/173 + // @ts-ignore - nothing we can do about so just ignore it + compression({ threshold: 0 }), + assetsMiddleware, + kitMiddleware, + prerenderedMiddleware +); const listenOpts = { path, host, port }; -instance.listen(listenOpts, () => { + +server.listen(listenOpts, () => { console.log(`Listening on ${path ? path : host + ':' + port}`); }); -export { instance }; +export { server }; diff --git a/packages/adapter-node/src/kit-middleware.js b/packages/adapter-node/src/kit-middleware.js new file mode 100644 index 000000000000..5807b3d587fb --- /dev/null +++ b/packages/adapter-node/src/kit-middleware.js @@ -0,0 +1,40 @@ +import { getRawBody } from '@sveltejs/kit/node'; + +/** + * @return {import('polka').Middleware} + */ +// TODO: type render function from @sveltejs/kit/adapter +// @ts-ignore +export function create_kit_middleware({ render }) { + return async (req, res) => { + const parsed = new URL(req.url || '', 'http://localhost'); + + let body; + + try { + body = await getRawBody(req); + } catch (err) { + res.statusCode = err.status || 400; + return res.end(err.reason || 'Invalid request body'); + } + + const rendered = await render({ + method: req.method, + headers: req.headers, // TODO: what about repeated headers, i.e. string[] + path: parsed.pathname, + query: parsed.searchParams, + rawBody: body + }); + + if (rendered) { + res.writeHead(rendered.status, rendered.headers); + if (rendered.body) { + res.write(rendered.body); + } + res.end(); + } else { + res.statusCode = 404; + res.end('Not found'); + } + }; +} diff --git a/packages/adapter-node/src/middlewares.js b/packages/adapter-node/src/middlewares.js new file mode 100644 index 000000000000..ba4ee030244d --- /dev/null +++ b/packages/adapter-node/src/middlewares.js @@ -0,0 +1,48 @@ +// TODO hardcoding the relative location makes this brittle +// Also, we need most of the logic in another file for testing because +// ../output/server/app.js doesn't exist when we run the tests +// @ts-ignore +import { init, render } from '../output/server/app.js'; +import { create_kit_middleware } from './kit-middleware.js'; + +import fs from 'fs'; +import { dirname, join } from 'path'; +import sirv from 'sirv'; +import { fileURLToPath } from 'url'; + +// App is a dynamic file built from the application layer. + +const __dirname = dirname(fileURLToPath(import.meta.url)); +/** @type {import('polka').Middleware} */ +const noop_handler = (_req, _res, next) => next(); +const paths = { + assets: join(__dirname, '/assets'), + prerendered: join(__dirname, '/prerendered') +}; + +export const prerenderedMiddleware = fs.existsSync(paths.prerendered) + ? sirv(paths.prerendered, { + etag: true, + maxAge: 0, + gzip: true, + brotli: true + }) + : noop_handler; + +export const assetsMiddleware = fs.existsSync(paths.assets) + ? sirv(paths.assets, { + setHeaders: (res, pathname) => { + // @ts-expect-error - dynamically replaced with define + if (pathname.startsWith(/* eslint-disable-line no-undef */ APP_DIR)) { + res.setHeader('cache-control', 'public, max-age=31536000, immutable'); + } + }, + gzip: true, + brotli: true + }) + : noop_handler; + +export const kitMiddleware = (function () { + init(); + return create_kit_middleware({ render }); +})(); diff --git a/packages/adapter-node/src/server.js b/packages/adapter-node/src/server.js deleted file mode 100644 index 9fb12ceb0f04..000000000000 --- a/packages/adapter-node/src/server.js +++ /dev/null @@ -1,82 +0,0 @@ -import { getRawBody } from '@sveltejs/kit/node'; -import compression from 'compression'; -import fs from 'fs'; -import { dirname, join } from 'path'; -import polka from 'polka'; -import sirv from 'sirv'; -import { fileURLToPath } from 'url'; - -// App is a dynamic file built from the application layer. - -const __dirname = dirname(fileURLToPath(import.meta.url)); -/** @type {import('polka').Middleware} */ -const noop_handler = (_req, _res, next) => next(); -const paths = { - assets: join(__dirname, '/assets'), - prerendered: join(__dirname, '/prerendered') -}; - -// TODO: type render function from @sveltejs/kit/adapter -// @ts-ignore -export function createServer({ render }) { - const prerendered_handler = fs.existsSync(paths.prerendered) - ? sirv(paths.prerendered, { - etag: true, - maxAge: 0, - gzip: true, - brotli: true - }) - : noop_handler; - - const assets_handler = fs.existsSync(paths.assets) - ? sirv(paths.assets, { - setHeaders: (res, pathname) => { - // @ts-expect-error - dynamically replaced with define - if (pathname.startsWith(/* eslint-disable-line no-undef */ APP_DIR)) { - res.setHeader('cache-control', 'public, max-age=31536000, immutable'); - } - }, - gzip: true, - brotli: true - }) - : noop_handler; - - const server = polka().use( - // https://github.com/lukeed/polka/issues/173 - // @ts-ignore - nothing we can do about so just ignore it - compression({ threshold: 0 }), - assets_handler, - prerendered_handler, - async (req, res) => { - const parsed = new URL(req.url || '', 'http://localhost'); - - let body; - - try { - body = await getRawBody(req); - } catch (err) { - res.statusCode = err.status || 400; - return res.end(err.reason || 'Invalid request body'); - } - - const rendered = await render({ - method: req.method, - headers: req.headers, // TODO: what about repeated headers, i.e. string[] - path: parsed.pathname, - query: parsed.searchParams, - rawBody: body - }); - - if (rendered) { - res.writeHead(rendered.status, rendered.headers); - if (rendered.body) res.write(rendered.body); - res.end(); - } else { - res.statusCode = 404; - res.end('Not found'); - } - } - ); - - return server; -} diff --git a/packages/adapter-node/tests/smoke.js b/packages/adapter-node/tests/smoke.js index 6a631ccdc9e8..f7292aea6142 100644 --- a/packages/adapter-node/tests/smoke.js +++ b/packages/adapter-node/tests/smoke.js @@ -1,14 +1,15 @@ import { test } from 'uvu'; -import { createServer } from '../src/server.js'; +import { create_kit_middleware } from '../src/kit-middleware.js'; import * as assert from 'uvu/assert'; import fetch from 'node-fetch'; +import polka from 'polka'; const { PORT = 3000 } = process.env; const DEFAULT_SERVER_OPTS = { render: () => {} }; -function startServer(opts = DEFAULT_SERVER_OPTS) { - const server = createServer(opts); +async function startServer(opts = DEFAULT_SERVER_OPTS) { return new Promise((fulfil, reject) => { + const server = polka().use(create_kit_middleware(opts)); server.listen(PORT, (err) => { if (err) { reject(err);