Skip to content
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

SvelteKit Process Hooks #693

Closed
ConProgramming opened this issue Mar 25, 2021 · 19 comments
Closed

SvelteKit Process Hooks #693

ConProgramming opened this issue Mar 25, 2021 · 19 comments
Labels
documentation Improvements or additions to documentation

Comments

@ConProgramming
Copy link
Contributor

Is your feature request related to a problem? Please describe.
As of right now, there's no real way to hook into the SvelteKit process to run code before build, dev, or start.

The SvelteKit hooks.js (not sure if this idea below works better for the former setup.js file), has hooks for when a route is called serverside. However, it'd be nice to have hooks that would essentially correspond with svelte-kit build, svelte-kit dev, and svelte-kit start. Elder.js has "Build Hooks" that handle hooking into the build process. JungleJS also has ways of configuring async code in between build/start process steps.

Describe the solution you'd like
My current idea is code like the following, probably in hooks.js.

export async function devProcess({ dev, port, config }) {
        //Fancy async code here

	return await dev({ port, config }); 
}

export async function buildProcess({ build, adapt, verbose, config }) {
        //Fancy async code here

        await build(config);

        return await adapt(config, { verbose });
}

export async function startProcess({ start, port, config }) {
        //Fancy async code here

        return await start(config);
}

And then the cli.js file would be like it is now, but if one of these above (ofc optional) options is specified it runs it instead of the default code. For example, for the dev command

//Somehow import the devProcess command from above as customDevProcess

.action(async ({ port, open }) => {
	process.env.NODE_ENV = 'development';
	const config = await get_config();

	const { dev } = await import('./core/dev/index.js');

	const devPromise = customDevProcess ? customDevProcess({ dev, port, config }) : dev({ port, config });

	try {
		const watcher = await devPromise;

		watcher.on('stdout', (data) => {
			process.stdout.write(data);
		});

		watcher.on('stderr', (data) => {
			process.stderr.write(data);
		});

		console.log(colors.bold().cyan(`> Listening on http://localhost:${watcher.port}`));
		if (open) launch(watcher.port);
	} catch (error) {
		handle_error(error);
	}
});

Describe alternatives you've considered
My original idea were options for preDev, preBuild, and preStart just in the svelte.config.cjs, for simplicity sake. This is of course kinda messy, and probably not the most dynamic way to do it. But also, these options for "process hooks" might not make sense in the current hooks.js file.

How important is this feature to you?
In my efforts to recreate the ✨magic✨ of JungleJS within SvelteKit (take a look at progress here), I hit a road block where this is kinda critical. As of rn I have a literal temp-cli.js file that I'm using to pull in dataSources in a separate command. I could make a legit CLI for JungleJS, and will probably have to do so if this isn't pushed forward, but I feel this would be better as hooking into the whole SvelteKit process.

Additional context
I decided to go ahead and make this an issue after seeing others ask for stuff that could be solved with this, one example here of where some sort of preStart hook would be useful.

@Rich-Harris
Copy link
Member

What's wrong with the separate command? You can define npm run build so that it looks like this...

"scripts": {
  "build": "node scripts/prebuild.js && svelte-kit build"
}

...which would be a perfectly normal thing to do.

@shahidcodes
Copy link
Contributor

shahidcodes commented Mar 26, 2021

@Rich-Harris another use case of these hooks could be to connect to a database when the server starts, to keep the connection alive. I was migrating a sapper project to svelte-kit and I realized there is no way to achieve the above. In sapper, I used server.js. Right now I can use the handle hook but I will have to create a database connection for each request which not optimal. I could cache the connection but I am not sure how will that work in HMR or production build.
I thought to use a modified node-adapter but that will only take effect after the build.

@cainux
Copy link

cainux commented Mar 26, 2021

For startProcess, it looks like a modified node-adapter is the way to go? I can't see it fitting with SSG or serverless approaches.

I have a scenario where I need to connect to Kafka at application startup, so this'll be ideal for me :)

@Rich-Harris
Copy link
Member

You can create db connections etc in src/hooks.js. Whether those connections are short-lived or long-lived depends solely on whether you deploy the app as a Node server or to a serverless platform

https://kit.svelte.dev/docs#hooks

@JBusillo
Copy link
Contributor

@cainux @shahidcodes I've been experimenting with this issue, specifically running initialization code.

In a dev build, hooks.js seems to be dynamically imported when the first page is accessed. This may be due to the dynamic nature of dev environment (vite, HMR, etc.)

In a prod build, hook.js is 'resolved' and available upon startup. I placed my initialization code outside of the exported functions (as a side-effect), and that code is being run upon startup.

My hooks.js is at https://github.com/JBusillo/cuencador-kit/blob/master/src/hooks.js Feel free to peruse the project.

I'm not sure if this behavior is intentional and will continue to work this way after SvelteKit goes beta or to production. But this approach may help you.

@GCastilho
Copy link

If I can give my opinion, the documentation for the hooks module, https://kit.svelte.dev/docs#hooks, does not let it clear that it will keep a side-effect running in a node environment, like a database connection

I think that's something that should be addressed, if that's the case

@JBusillo
Copy link
Contributor

JBusillo commented Mar 28, 2021

Another observation: Any side effects of hooks.js are invoked during "build".

This might prevent the build process from being truly "portable". By "portable", I mean being able to build a project on any machine without having to configure an application run environment (e.g., config files stored on the file system, database servers, etc.). I don't think that side effects in hooks.js need to be executed for the build process.

My approach is to set an environment variable for the build script in package.json:

"build": "hooks_side_effects=no svelte-kit build"

and conditionally execute side effects in hooks.js:

if (process.env['hooks_side_effects'] !== 'no')
    initializeModules()

async function initializeModules() {
    ... your initialization code here ...
}

...and all of this depends on concerns expressed by @GCastilho and me -- being able to rely on side affect behavior and persistence within a node environment.

[Note: There's also an environment variable: npm_lifecycle_event: 'build' which one might be able to use, instead of a custom environment variable]

@Rich-Harris
Copy link
Member

That's because of prerendering (SvelteKit can't know that you don't have any prerenderable pages without trying to prerender them). You can disable it in your config:

module.exports = {
  kit: {
    prerender: {
      enabled: false
    }
  }
};

It's possible that that should be the default.

@dummdidumm
Copy link
Member

dummdidumm commented Mar 29, 2021

I was thinking that maybe it would be good to instead return one function from hooks.js, like export function setup() which in turn returns a object with getContext / getSession / handle on it or a promise of those. That way the setup-logic is better encapsulated (feels less hacky) and it has the possibility to pass in other things into setup once if we later see a need/use case for that. Not sure how that fits into Kit's setup flow though.

export async function setup() {
   const db = await initMyDb();
   return {
     handle: (..) => ...
   }
}

@Rich-Harris
Copy link
Member

I'm not sure I understand why we'd do that. Whenever I've written Express servers and the like, I've always just had a db.js or similar that sets up a connection immediately and makes the client accessible throughout the app as a singleton. Do most people not do that?

@JBusillo
Copy link
Contributor

I understand what you're saying, Rich -- assuming that an Express server is handling the database calls. For small projects, it might be desirable/easier to use the server (Polka) in adapter-node to handle those calls -- i.e., only using the SvelteKit endpoints.

Aside from the database use case, I have initialization code that pulls in json configuration files that contain app secrets, which creates a JS object that is used in my endpoints. This might not be the best approach -- I'm new to all of this.

Personally, I'm satisfied with the 'workaround'. Maybe a more formal solution could be an enhancement in a future release. I know you're all busy getting the kinks out for SvelteKit 1.0 -- I appreciate your hard work and dedication.

@Rich-Harris
Copy link
Member

Aside from the database use case, I have initialization code that pulls in json configuration files that contain app secrets, which creates a JS object that is used in my endpoints

This is potentially a good use case for request.context (note that the name might change, per #647 (comment)):

let secrets;

export async function getContext() {
  return {
    secrets: secrets || (secrets = await load_secrets_from_json())
  };
}

It might be more idiomatic to use environment variables though

@abendigo
Copy link

abendigo commented Apr 1, 2021

I'm not sure I understand why we'd do that. Whenever I've written Express servers and the like, I've always just had a db.js or similar that sets up a connection immediately and makes the client accessible throughout the app as a singleton. Do most people not do that?

I generally dislike having code run as a side effect of importing a file. I personally like cainux's suggestion, of making a change to the node adapter, or creating a modified version, which calls a startup hook from server.js

@Rich-Harris
Copy link
Member

What about this?

// src/hooks.js
import db from '$lib/db';

export async function handle(request, render) {
  if (!db.ready) await db.init();
  return render(request);
}

@JBusillo
Copy link
Contributor

JBusillo commented Apr 1, 2021

That should work for me. I'll use the technique that you suggested. Thank you.

@abendigo
Copy link

abendigo commented Apr 1, 2021

What about this?

// src/hooks.js
import db from '$lib/db';

export async function handle(request, render) {
  if (!db.ready) await db.init();
  return render(request);
}

So basically, the first time a page was rendered, the connection would be made. That works.

I'm trying to think of situations where it doesn't, (ie: a microservice that responds to rest endpoints, or redis/kafka events, and has an html status page), but those are probably better suited to a different tool.

@benmccann benmccann mentioned this issue Apr 2, 2021
Closed
@Conduitry
Copy link
Member

Is there anything else we want to document here before closing this issue?

@Conduitry Conduitry added the documentation Improvements or additions to documentation label May 10, 2021
@ignatiusmb
Copy link
Member

The "How do I setup a database?" section under https://kit.svelte.dev/faq#integrations should be enough, we wouldn't want to write more stuff that is out of our scope

@ErraticFox
Copy link

I don't understand why people are suggesting to put it in the handle hook while others have concerns of it running multiple instances. When in this case, from some simple testing I just did, everything placed outside the hook functions runs once on server start up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests