-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expose a way to inject a start script into adapter-node #927
Comments
Why don't not just run it before starting SvelteKit? Like |
I can't be the only one that needs a way to setup the environment for my application and would like a central place for that? E.g. things like this // I _think_ the aws-sdk can do this by itself using env variables, so it might be a bad example.
require('aws-sdk').config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: 'eu-west-1'
}); or if (process.env.NODE_ENV !== 'development') {
// This will make the fonts folder discoverable for Chrome.
// https://github.com/alixaxel/chrome-aws-lambda/blob/eddc7fbfb2d38f236ea20e2b5861736d3a22783a/source/index.js#L31
process.env.HOME = '/tmp';
fs.ensureDirSync('/tmp/.fonts');
fs.copySync(path.join(__dirname, 'fonts'), '/tmp/.fonts');
} I could put each of them in a module so importing would execute it once. But where would I import it if there is no central place? I guess I could abuse the I also ran into a case where a module had to be the very first thing that I required (lovell/sharp#860 (comment)) but SvelteKit does not give me that type of flexibility. Another ideaSo what if the node-adapter could have a flag that would make |
Uh, maybe I'll just lazily |
Another real-world example: fetching metadata once during startup before launching the server https://docs.digitalocean.com/products/droplets/how-to/provide-user-data/ I'll leave this open to get some more feedback |
You could always create your own adapter, using I wanted to remove the |
Does top-level code in |
Thanks for the hint, that sounds reasonable. I'll look into your Express adapter when I start working on this again.
I'd prefer not to introduce race-conditions and wait for the initialization to succeed before starting the server. I might even need some of the initialization logic to decide on certain parameters for the server. E.g. querying the |
I brought this up back in December, and I still think when we load the adapter in kit we should look for and await an init function, then the adapter can use that however it wants to (if at all). In the case of the node adapter, it could execute an async method passed to the adapter from svelte.config.js and we could bootstrap our pools and whatnot there without making things weird for other adapters. I think it would be cool if the init function could pass back an initial context state for each request. |
This comment was marked as duplicate.
This comment was marked as duplicate.
I use a hack-around for my SvelteKit TypeScript Node project: // Create a promise, therefore start execution
const setup = doSomeAsyncSetup().catch(err=>{
console.error(err)
// Exit the app if setup has failed
process.exit(-1)
})
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
// Ensure that the promise is resolved before the first request
// It'll stay resolved for the time being
await setup;
const response = await resolve(request);
return response
} Here's how it works:
Of course, having a |
Related to #1538 |
@ValeriaVG big thanks for your suggestion! |
@livehtml Sure const doSomeAsyncSetup = async ()=>{
await connectToDB()
await runMigrations()
await chantASpell()
await doWhateverElseYouNeedAsyncSetupFor()
}
// Create a promise, therefore start execution
const setup = doSomeAsyncSetup().catch(err=>{
console.error(err)
// Exit the app if setup has failed
process.exit(-1)
})
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ request, resolve }) {
// Ensure that the promise is resolved before the first request
// It'll stay resolved for the time being
await setup;
const response = await resolve(request);
return response
} |
Thanks ValeriaVG for this clever code which has been very useful. As pointed out in another thread, one issue with this solution is that the hook.js/ts file only gets loaded on first client request. And also server initialization shouldn't be in the handle hook. So relying on your proposal and mixing it with mankins proposal to create a specific adapter, we could do the following: Create an initialize.js/ts file that handles your initializations: import { browser} from '$app/env';
const initializeAppCode = async () => {
console.log('Client and server initialization');
// Some global initializations here
if (browser) {
console.log('Client initialization');
// Some client-side initializations here
} else {
console.log('Server initialization');
// Some server-side initializations here
}
};
// Start initializations as soon as this module is imported. However, vite only loads
// the hook.js/ts module when needed, i.e. on first client request. So import this module from our own server in production mode
const initializeApp = initializeAppCode().catch((err) => {
console.error(err);
// Exit the app if setup has failed
process.exit(-1);
});
export {initializeApp}; Then create a server.js/ts file in your src directory that calls your initializations: import { path, host, port } from './env.js';
import { assetsMiddleware, kitMiddleware, prerenderedMiddleware } from './middlewares.js';
import compression from 'compression';
import polka from 'polka';
import '$lib/local_libs/initialize'; // <---- IMPORTANT LINE
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 };
server.listen(listenOpts, () => {
console.log(`Listening on ${path ? path : host + ':' + port}`);
});
export { server }; Modify the svelte.config.js to tell node-adapter to use our server instead of the default one: const config = {
kit: {
adapter: node({entryPoint: 'src/server.ts' }), // You can pass other options if needed but entryPoint is the crucial part here
}
} Then modify the handle hook like you proposed: import type { Handle} from '@sveltejs/kit';
import { initializeApp } from '$lib/local_libs/initialize';
const handle: Handle = async function ({ request, resolve }) {
// This will wait until the promise is resolved. If not resolved yet, it will block the first call to handle
// but not subsequent calls.
await initializeApp;
const response = await resolve(request);
return response;
}; There are two reasons for adding the
Unfortunately, I could not come up with an elegant solution for client-side initialization. So this remains a workaround. A global init.js/ts file would still be appreciated. |
I'm still subscribed to this issue (d'oh, it's my own). Wasn't this recently solved? You can now use I haven't worked on the SveliteKit migration since opening this issue, but looking at my original example I should now be able to do literally the same thing I did in Sapper. I can't speak for other adapters, but for adapter-node I don't see an issue any longer (in before I actually try to migrate to |
Take some time to read carefully my answer. In my opinion, the Promise Spaghetti (as you call it) is still needed, even after defining a new entryPoint. And, so far, we have no clean solution in dev mode and no solution at all for client-side initialization. I'm afraid we're in for pasta for a bit longer... |
@martinjeromelouis gotcha, please add syntax highlighting to your code blocks in the future, it's hard to read 👍 . I don't think I fully understand the problems you're having with hooks. Aren't hooks entirely unrelated? What I want to do is run initialization before even calling This works as custom entry point: import { assetsMiddleware, prerenderedMiddleware, kitMiddleware } from '../build/middlewares.js';
import init from './init.js';
import polka from 'polka';
const app = polka();
app.use(assetsMiddleware, prerenderedMiddleware, kitMiddleware);
init().then(() => {
app.listen(3000);
}); The only thing missing is an option in Vite that allows injecting code before starting the server. But what keeps us from monkey patching This works during dev (where it's not critical to me that import adapter from '@sveltejs/adapter-node';
import init from './src/init.js';
const myPlugin = {
name: 'init-when-listen',
configureServer(server) {
const listen = server.listen;
server.listen = (port, isRestart) => {
init();
return listen(port, isRestart);
};
}
};
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte',
adapter: adapter({
entryPoint: './src/server.js'
}),
vite: {
plugins: [myPlugin]
}
}
};
export default config; |
@Prinzhorn Thanks for the listen monkey patching. Of course this is no persistent solution but I tried to implement it and ran into the following issue : as svelte.config.js is a js file, I cannot import my init.ts module. But still it can be of some help to people who write plain javascript. Regarding the import of the init module in your entryPoint (prod), your solution can be improved by ValeriaVG Promise proposal as I showed in the example code above. In your code, the call to the init function blocks the server until initialization is fully performed which can be annoying if the init function depends for instance on external apis that take some time to respond. With the Promise trick, you start the init asynchronously and just keep the first request pending. Of course, this only matters when you have a website with a lot of traffic. Finally, all this does not look like a definitive solution. A regular init.js/ts file that would automatically be called by Sveltekit both at client and server start would be a much cleaner solution. |
@martinjeromelouis I agree, it's a workaround and not a clean solution. I'm honestly overwhelmed by the complexity of SvelteKit, which is why I'm hesitant to adopt it at all. But that's a different story. |
@Prinzhorn I tried your solution too. It works while svelte kit is in dev mode, which is great. However it is ignored entirely when building for I should mention I am not criticizing your design, just pointing out again the need for an official solution here. |
perhaps the easiest solution would be to add an option to avoid lazy loading the |
@andykais I think you missed the first code block, which is the custom entry point for adapter-node that uses the same |
My build scripts need access to Svelte's context. For example:
I started with a set of external typescript files, which would get compiled, then ran, but keeping Typescript configs and such in sync was a bother. Slight environment differences would eat a lot of debugging time. So instead, building on the handle thing, I have something a bit different: // routes/init.ts
import type { RequestHandler } from '@sveltejs/kit'
let hasInitiated = false
export const get: RequestHandler = async () => {
const body = hasInitiated ? true : await initSequence
return { body }
}
const initSequence = (async (): Promise<boolean> => {
/**
* Do the init stuff here
*/
return new Promise((ok) => setTimeout(ok, 3000, true))
})()
initSequence.then(() => (hasInitiated = true)) On first run, I do It's rough, but simple and sufficient for my needs. Maybe it gives someone inspiration. |
I'll confess I'm not totally sure what the feature request is here — between creating a custom server and doing setup work in Would love to close this issue if possible! |
@Rich-Harris I think to put it concisely, we want the ability to run arbitrary code before the server starts. The general reason for this is:
For me personally, I am using sveltekit to build a cli web app. So you can imagine there are cases where a user passes some flags to the app which are invalid (heck, parsing args is a bit of a pain via hooks right now as well). So their workflow looks like: 1. run the app on the cli, 2. open their browser, 3. see an error, 4. go back to the terminal and cancel and restart the app with different params. I also have no ability to run something like a |
ah, great to know. I'll need this feature soon, so you most probably save me a lot of headache too, thanks you for trying it out! |
[edit] it appears that |
@andykais You mean it was just fixed within the last 2 days? Which version did you try out? |
@mellanslag-de looks like version 1.0.0 in |
Yeah, it works for me too (sveltejs/kit 1.0.5). But before closing the issue, it would be nice to have official handlers for both startup and shutdown. A shutdown handler would help with cleanup tasks. |
maybe we should start a separate issue for that? Personally I am interested in startup and shutdown triggers, I imagine you are talking about startup and shutdown hooks though. When does shutdown occur anyways? When the process is killed? You could probably detect |
Yes, please go ahead and create one. I have only just started experimenting with svelte, and I am not familiar with the distinction between trigger and hook.
Agreed, could be discussed. Some of us are hoping for an idiomatic and documented way to do these things, that will work consistently on all platforms where startup/shutdown can be supported. |
My import { handleSession } from 'svelte-kit-cookie-session';
import { PRIVATE_COOKIE_SECRET } from '$env/static/private'
console.log("Hello?")
export const first = handleSession({
secret: `${PRIVATE_COOKIE_SECRET}`
});
export const handle = first; The specific file in question: https://github.com/MiraiSubject/oth-verification/blob/2254f1693dd48cbed46ccf40d186754398798bc7/src/hooks.server.ts When booting up in production however by using |
I can confirm what @MiraiSubject has said, in the latest sveletkit ( |
https://kit.svelte.dev/docs/hooks
@Rich-Harris Does the Svelte team have a plan to resolve this issue since the documentation isn't clear as to what's happening? |
@PeytonHanel , hooks.server.js indeed runs when server starts up ...but only in production mode (i. e. As for correct approach... I don't think |
Coming from NextJS I would like to see that hot-reloads are handled by the request handler provided by // Current SvelteKit request handler does not support hot-reloads
import express from "express";
import { handler } from "../build/handler.js";
const app = express();
app.use(handler); // add hot-reload here!
app.listen(); Currently, this design requires a manual build and restart of the server in order to see component changes. The logic for hot-reloads can definitely be embedded into the request handler as seen in NextJS: // NextJS Request Handler that supports hot-reloads
import express from 'express';
import next from 'next';
const nextApp = next({dev}); // tell if dev or production
await nextApp.prepare(); // initial build
const nextHandler = nextApp.getRequestHandler(); // request handler with hot-reload integrated
const app = express();
app.use(nextHandler); // watches and hot-reloads components if needed (only in dev mode)
app.listen(); |
Sorry to ping but has there been any working solution to this? But still this also touches on another issue, that goes hand-in-hand with this one: How can we run the server over some Node-API? I want to be able to mock stuff in the server which needs to run obviously in the same process for that. Currently, I reside in simulating requests to the respective API handler functions.
That's also not working over npm run preview. How can this be so hard to have some code running on startup? |
I also hope this issue gets resolved. |
Just as @robpc I had the requirement to start a websocket handler (Socket.io in this case) together with the Polka server instantiated by node-adapter. I found the solution proposed by robpc unsatisfying, because it means running I realized that I also wanted to use the original startup code generated by adapter-node as much as possible, because it contains important setup. I don't want to replace with a custom handler, because then I would no longer benefit from updates etc. A wrapper around the generated code is ok, though. All in all my solution looks like this: For the production code generated to // start.js
global.httpServer = new Promise((resolve) => {
import('./index.js').then(index => resolve(index.server.server))
}); This script starts the server (by importing the generated In import {startSocketIOServer} from '$lib/server/socket/websocketServer'
...
startSocketIOServer(); where import {Server} from 'socket.io'
function startSocketIOServer() {
global.httpServer?.then(httpServer => {
const io = new Server(httpServer);
io.on('connection', (socket) => {
... // do whatever should happen on a new connection
})
console.log('SocketIO configured')
})
}
export {startSocketIOServer} This works for production, ie. // vite.config.ts
import {sveltekit} from "@sveltejs/kit/vite"
import {defineConfig} from "vite"
export default defineConfig({
plugins: [
sveltekit(),
{
name: 'sveltekit-inject-httpserver',
configureServer(server) {
global.httpServer = Promise.resolve(server.httpServer!)
},
configurePreviewServer: {
order: 'pre', // important so that the handler executes _before_ hooks.server.ts
handler: (server) => {
global.httpServer = Promise.resolve(server.httpServer)
},
},
},
],
... That's all! 😒 Almost. To give // src/globals.d.ts
import http from 'node:http'
declare global {
namespace globalThis {
var httpServer: Promise<http.Server>
}
} It took me a whole day to figure this all out. It should be easier to add initialization code in SvelteKit. IMHO Hope this helps someone. Initializing a websocket handler along with the SvelteKit server is not an uncommon requirement. Still waiting for #1491 though. |
@dmoebius Thanks for the share of this great knowledge. I was about to create a SvelteKit adapter on base of adapter-node to solve this issue and some behavior I dislike (like including package.json in the build, etc.). This literally saved me a lot of time building the new adapter. Ofc. I will also credit you :) |
@UnlimitedBytes Note that someone already went the route of creating a custom adapter, see https://github.com/carlosV2/adapter-node-ws A short addendum to my suggestion above: during HMR in function startSocketIOServer() {
global.httpServer?.then(httpServer => {
let io : Server;
if (global.currentSocketIOServer) {
io = global.currentSocketIOServer; // HMR: reuse previous instance
} else {
io = new Server(httpServer);
global.currentSocketIOServer = io;
}
io.on('connection', (socket) => {
... // do whatever should happen on a new connection
});
console.log('SocketIO configured');
})
} |
@dmoebius Thanks for the information I looked at the linked repo and actually could reuse some of the code for my adapter. I added both of you in the credits. If someone is interested I released a very early alpha build on https://github.com/UnlimitedBytes/sveltekit-adapter-custom. It's already capable of hijacking the http server in your Heres a quick example how to add a socket.io websocket server: // hooks.server.ts
import type { SetupHook } from 'sveltekit-adapter-custom';
export const setup: SetupHook = async (httpServer) => {
const io = new Server(httpServer);
io.on('connect', (socket) => {
console.log(`New user (${socket.client.conn.remoteAddress}) connected.`);
socket.on('message', (message) => {
io.emit('message', message);
});
socket.on('disconnect', () => {
console.log(`User (${socket.client.conn.remoteAddress}) disconnected.`);
});
});
}; |
I've been reading this entire thread for a while now and I do believe too that things should not be that complicated and that we need a simple solution. As a dirty yet easy workaround I added |
I had a similar issue and I used vite itself to make an initial request to the server as soon as the server has initialized. This only works in development which is what I wanted. Maybe it could help someone in the future. {
name: 'start-up-request',
configureServer(server) {
server.httpServer?.on('listening', async () => {
await fetch('http://localhost:5173').catch((e) => console.error(e));
});
}
} |
I also need this. My current solution is to just add a setup function to A custom server would work, but is otherwise unnecessary and means I would have to re-implement the existing functionality myself, when all I need is to be able to run a function at startup. |
Is your feature request related to a problem? Please describe.
I'm tying to migrate from Sapper to SvelteKit. I'm aware of the discussion in #334 and hooks seem to solve that.
I don't think there is currently a way to have code that runs on startup with the Node adapter. There are things that need to happen once and not for every request. In Sapper my
server.js
looked like this:The first line I've marked is something that would probably not hurt if done in every hook (setting up my Objection models with the knex instance).
The second line I've marked is running the database migration. In a serverfull environment this is a rather common occurrence. I want to run the migration right before starting the server so that I'm in a consistent state between database and application code. Even if this is scaled across multiple instances it works, since the second time the migrations run it's a NOOP. I honestly have no idea how people do that in a serverless environment with a guarantee that no code is run that expects an outdated schema resulting in possibly undefined behavior.
Describe the solution you'd like
A way to run setup code before the server starts. Maybe an
async function setup() {}
that is awaited before the server starts? But that can't work with every adapter.Describe alternatives you've considered
I guess some will argue I should have a second API server separated from SvelteKit and whatnot. But I want less complexity, not more.
How important is this feature to you?
I cannot migrate to SvelteKit, unless I'm missing something.
The text was updated successfully, but these errors were encountered: