vikejs / vike

🔨 Flexible, lean, community-driven, dependable, fast Vite-based frontend framework.
https://vike.dev
MIT License
4.08k stars 343 forks source link

vite-plugin-vercel #222

Closed brillout closed 2 years ago

brillout commented 2 years ago

vite-plugin-ssr already supports Vercel https://vite-plugin-ssr.com/vercel but it would be lovely to have a vite-plugin-vercel so that the entire Vite ecosystem can benefit.

Discussoin about this: https://discord.com/channels/815937377888632913/815937377888632916/915547793987350548

I've secured the npm package and I'm happy to give it away.

patryk-smc commented 2 years ago

Got is started if someone wants to pick it up:

Requires Node 16

import fs from "fs";
import path from "path";

const VERCEL_OUTPUT_DIR = "../.output";
const VITE_DIST_DIR = "../dist";

// Step 1: Cleanup
fs.rmSync(path.resolve(__dirname, VERCEL_OUTPUT_DIR), { recursive: true });

// Step 2: Create output folder
fs.mkdirSync(path.resolve(__dirname, VERCEL_OUTPUT_DIR));

// Step 3: Copy static assets
fs.cpSync(
  path.resolve(__dirname, VITE_DIST_DIR + "/client"),
  path.resolve(__dirname, VERCEL_OUTPUT_DIR + "/static"),
  { recursive: true }
);

// Step 4: Transpile & bundle render function, straight to the pages folder
// TODO: Transpile & bundle instead copying the file
// Q: Which bundler to use? NCC? Rollup? ESBuild? None?
fs.cpSync(
  path.resolve(__dirname, "../vercel/render.ts"),
  path.resolve(__dirname, VERCEL_OUTPUT_DIR + "/server/pages/index.js")
);

// Step 5: Make render function run on every request (catch all)
const routesManifest = {
  version: 3,
  basePath: "/",
  pages404: false,
  dynamicRoutes: [
    {
      page: "/",
      regex: "/((?!assets/).*)",
    },
  ],
};

fs.writeFileSync(
  path.resolve(__dirname, VERCEL_OUTPUT_DIR + "/routes-manifest.json"),
  JSON.stringify(routesManifest, null, 2)
);

// Step 6: (Optional) Function configuration
const functionsManifest = {
  version: 1,
  pages: {
    "index.js": {
      memory: 1792,
      maxDuration: 10,
    },
  },
};

fs.writeFileSync(
  path.resolve(__dirname, VERCEL_OUTPUT_DIR + "/functions-manifest.json"),
  JSON.stringify(functionsManifest, null, 2)
);
brillout commented 2 years ago

Neat.

Let me know if you are up for creating a little Vite plugin wrapping this. We'll discuss the high-level design of it.

patryk-smc commented 2 years ago

I have a bit too much on my plate at the moment to work on this, but will let you know if I get some spare time. I guess we will be refining the deployment process anyway before going to production.

On Thu, Dec 02, 2021 at 10:02 PM, Rom Brillout @.***> wrote:

Neat.

Let me know if you are up for creating a little Vite plugin wrapping this. We'll discuss the high-level design of it.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/brillout/vite-plugin-ssr/issues/222#issuecomment-984998369, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJBUMS2QBAY42XIY3GJPXN3UO7NGTANCNFSM5JEIKW7A . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

brillout commented 2 years ago

👍

magne4000 commented 2 years ago

I'm currently beginning to work on this subject, mostly because I need this for ISR.

Should we have a underlying package not tied to vite ecosystem (like a vercel-fs-api package)?

Should vite-plugin-vercel work without vite-plugin-ssr? If so, how should it behave (I'm not familiar with vite internals yet)? If both are present, here is how I see things:

And regarding new ISR feature:

brillout commented 2 years ago

Should we have a underlying package not tied to vite ecosystem (like a vercel-fs-api package)?

I agree and vite-plugin-vercel would ony be a thin wrapper on top of it. FYI: https://github.com/vitejs/vite/issues/5936.

There is already some prior work to deploy vps with Vercel's FS api: https://github.com/brillout/vite-plugin-ssr/blob/master/examples/vercel/vercel/deploy.sh.

As for the rest, sounds good.

I'm very much looking forward to this. Feel free to show me a draft.

magne4000 commented 2 years ago

I realise that vite-plugin-ssr prerender should probably be moved into an option of the plugin itself like:

export default defineConfig({
    plugins: [ssr({
        prerender: true
    })],
});

vite-plugin-vercel should then be able to move generated html files to the right .output/server/pages directory.

brillout commented 2 years ago

Yes

magne4000 commented 2 years ago

Here is the repo with the first working tests. What does it do for now (or don't):

Deployed at https://test-vite-vercel-plugin.vercel.app/

patryk-smc commented 2 years ago

Hey @magne4000 that repo seems to be private ;)

magne4000 commented 2 years ago

oups :hand_over_mouth:. Fixed

brillout commented 2 years ago

How about this?

// vite.config.js

export default {
  // These settings can be set by the user or by another plugin such as vps
  vercel: {
    isr: {
      initialRevalidateSeconds: 30,
      prerender: /* prerender function called by vite-plugin-vercel */,
    },
    apiEndpoints: ['./api/foo.js'],
  }
}

vps can set some of these settings on behalf of the user, such as:

This allows vpc to be decoupled from vps.

It also enables decoupled development: what we can do is to first work on an example where these settings are all set by the user's vite.config.js. We then move the prerender CLI wrapper and the api endpoint into vps's source code.

Thoughts? Also thoughts about https://github.com/vitejs/vite/issues/5936?

I gave you full privilege for https://www.npmjs.com/package/vite-plugin-vercel.

magne4000 commented 2 years ago

Using vite.config.js is fine with me, it should ensure common API between all the vps plugins.

vite.config.js#vercel.prerender: vps provides a thin wrapper around its prerender CLI.

👍

vite.config.js#vercel.apiEndpoints. vps provides something like https://github.com/brillout/telefunc-vercel-ssr/blob/master/api/ssr.js, so that the user doesn't have to write it. Further tools, such as Telefunc, can also add themselves: https://github.com/brillout/telefunc-vercel-ssr/blob/master/api/telefunc.js.

I like the idea! We'll have to dig a little further, like how can I add specific behaviour (like redirect behaviour, setcookie, custom headers, etc. based on pageContext)? (Writing this I realize that in the handler, the only thing I want to customize are HTTP responses)

It also enables decoupled development

We should even be able to easily share logic between dev (e.g. express) and prod (e.g. vercel)

what we can do is to first work on an example where these settings are all set by the user's vite.config.js

I'll probably have time next week to try that

Also thoughts about https://github.com/vitejs/vite/issues/5936?

Yep, It would be easier to write this plugin if we had a single process, that's sure

I gave you full privilege for https://www.npmjs.com/package/vite-plugin-vercel.

🙇

brillout commented 2 years ago

The thin wrapper around vps's prerender CLI would provide page-specific configs, so that the user can do this:

// /pages/product.page.js

// Overrides the default
export const initialRevalidateSeconds = 10

// The usual stuff
export { Page }
// ...

share logic between dev (e.g. express) and prod (e.g. vercel)

@cyco130 is actually working on that (https://github.com/hattipjs/hattip — there isn't any documentation yet).

magne4000 commented 2 years ago

Prerendering code uses both vite-plugin-ssr and vite-plugin-vercel. The sensible way would be to put it in another repo (or monorepo package), but I think it would be better that if both vite-plugin-ssr and vite-plugin-vercel are installed, the user does not have to manually set the prerender function in vite.config.js. So the question is, where do we put those specific prerender functions? I lean a little towards storing those in their respective framework (i.e. in vite-plugin-ssr, telefunc, etc.)

brillout commented 2 years ago

I think it would be better that if both vite-plugin-ssr and vite-plugin-vercel are installed, the user does not have to manually set the prerender function in vite.config.js.

Yes. Ideally, and I think it's possible, everything would just work without the user having to do anything.

I lean a little towards storing those in their respective framework (i.e. in vite-plugin-ssr, telefunc, etc.)

I agree. For the current prototyping phase, if you want for the sake of dev speed, we can store this prerender function at vite-plugin-vercel.

rauchg commented 2 years ago

This sounds very exciting. Thanks for everyone who's helping out with this, and we look forward to supporting y'all and Vite from our side.

brillout commented 2 years ago

@rauchg Much appreciated 👍. We actually have some question over there https://github.com/vercel/vercel/discussions/7573.

magne4000 commented 2 years ago

I made some good progress:

Updated live demo with SSR/SSG/ISR: https://test-vite-vercel-plugin.vercel.app/

TODO

For a beta version

After beta

brillout commented 2 years ago

Neat 👌.

Let me know if you have some remarks on it

Little typo here https://github.com/magne4000/test-vite-vercel-plugin/blob/41af424c620b14c61697ea838404dfc952992474/packages/vercel/src/types.ts#L117 (ssr isr) but other than that looks good from first sight. I'm not all too familiar with Vercel so I can't say whether the naming makes sense.

You are using vps internals which is fine but we should add an integration test, like we already have for Cloudflare Workers (e.g. https://github.com/brillout/vite-plugin-ssr/blob/master/examples/cloudflare-workers/.test-wrangler.spec.ts). Is there a way to try a Vercel deploy locally without actually publishing the app?

magne4000 commented 2 years ago

Little typo here https://github.com/magne4000/test-vite-vercel-plugin/blob/41af424c620b14c61697ea838404dfc952992474/packages/vercel/src/types.ts#L117 (ssr isr) but other than that looks good from first sight. I'm not all too familiar with Vercel so I can't say whether the naming makes sense.

initialRevalidateSeconds is tied to ISR indeed, but itself ISR is tied to SSR + Static generation (it needs both). So it can make sense anywhere. prerender on the other is tied to Static Generation only (I reused the terms defined by Next.js). More generally, all those concepts are under the prerendering category. perhaps we should name that prerendering instead?

You are using vps internals which is fine but we should add an integration test, like we already have for Cloudflare Workers (e.g. https://github.com/brillout/vite-plugin-ssr/blob/master/examples/cloudflare-workers/.test-wrangler.spec.ts). Is there a way to try a Vercel deploy locally without actually publishing the app?

Sadly no. What I was thinking of doing is test the content of output generated files. Not the ideal test scenario, but it would ensure good regression tests at least. The other solution is to CI deploy the test app (like we have currently), then run tests on it, and trigger releases through the CI -> More complex and adds some friction to the dev experience, but its functional testing.

brillout commented 2 years ago

My thinking is that a user that does only SSG wouldn't use Vercel in the first place. So it seems to me that prerendering is always tied to ISR? And, on the flip side, if the user doesn't define a prerender function then he doesn't do ISR.

Ok, and yes, regression tests should be enough for now.

magne4000 commented 2 years ago

My thinking is that a user that does only SSG wouldn't use Vercel in the first place. So it seems to me that prerendering is always tied to ISR?

I can see use cases where you have a mix of SSG and SSR but not necessarily ISR. And for those cases you'll need a prerender function. It assumes less about user's intententions to put the prerender function in something more generic than isr IMO

And, on the flip side, if the user doesn't define a prerender function then he doesn't do ISR.

Correct, that's handled with the help of meaningful assert calls right now. It's in fact not that clear in Vercel's doc that you actually need to prerender a file for it to also be ISR capable

brillout commented 2 years ago

Makes sense.

How about flattening then? I.e. defining initialRevalidateSeconds and prerender directly on the config root.

magne4000 commented 2 years ago

Indeed. Simpler I don't see drawbacks as the configuration is quite minimal, this is probably the way to go :+1:

magne4000 commented 2 years ago

We need to define the behavior when using vite-plugin-vercel with .page.route functions.

Route functions are called on every request:

We could call route functions only when a route does not match any other (fallback route):

We could throw an error when using route functions with vite-plugin-vercel:

brillout commented 2 years ago

Route functions are called on every request:

Yes and no.

They are called for every URL in order to determine the pageContext._pageId.

And, yes, on the server-side they are also called for every request, but only to determine pageContext.routeParams. The entire mapping URL -> pageContext._pageId is already known at build-time.

So I don't think route functions are any problem for ISR. Note that route functions can be used today with SSG (by using the prerender() hook, see https://vite-plugin-ssr.com/prerender#for-providing-urls).

brillout commented 2 years ago

And, yes, on the server-side they are also called for every request, but only to determine pageContext.routeParams.

And this is actually only the case for SSR. For SSG, there is no server-side JavaScript execution.

magne4000 commented 2 years ago

Ok thanks, I still need some fallback rule then I guess

brillout commented 2 years ago

AFAICT, the only logic here is: is this a URL of a static asset? Then serve that static asset, otherwise do SSR.

So you somehow need to probe whether the URL was already pre-rendered.

magne4000 commented 2 years ago

That's indeed what the fallback rule is for, ignore /assets/*, and also /api/* in the case of vercel -> catch all the rest (^/((?!assets/)(?!api/).*)$). But for now I didn't even need this fallback rule, as I could determine everything from prerender callback (and mostly globalContext). So the behavior will likely be: If you do not have any .page.route function, no need for a fallback rule.

magne4000 commented 2 years ago

You can check all use cases I could think of https://test-vite-vercel-plugin.vercel.app/

brillout commented 2 years ago

If you do not have any .page.route function, no need for a fallback rule.

I don't see why route functions need special treatment.

I don't see a difference between a route string /product/:id and a route function pageContext => { const parts = pageContext.urlPathname.split('/'); if (parts[0]==='product') return { routeParams: { id: parts[1] }}}. Architecturally it's the same.

magne4000 commented 2 years ago

I see what you mean. I was mislead by the fact I had trouble having a working catch-all rule through routes-manifest.json. But now that I found a solution, it should be the only rule in this file.

magne4000 commented 2 years ago

I have another ISR specific use case. ISR allow me to prerender part of the files at build time, and other files on access.

Lets say I have the following prerender function:

export function prerender() {
  return ['/product/a', '/product/b']
}

If I access those, statically generated files are served, and potentially they are also updated on the background if initialRevalidateSeconds has passed.

But what happens if I access /product/c? Currently it's rendered via SSR. But Vercel has the possibility to serve the page through SSR while also saving it as a static file, so that next call to /product/c serves the static file (it's the actual use case that's of interest for me).

For that, I need a way to guess which URLs need to be ISR at build time, even when not prerendered (in this case /product/:productId).

I could leverage .page.route default export for that:

We could also have some new exported value just for ISR routes

We could let the user add another export next to the route function with a route hint (same as route string). That way one can still benefit from route functions for non fallback/complex cases ISR

Relevant for this use case

brillout commented 2 years ago

I assume that the page would be SSR'd before being added to the ISR cache.

In that case a custom export should do the trick:

// product.page.js

export const isr = true

You can then read pageContext.pageExports.isr to decide whether to populate the ISR cache.

brillout commented 2 years ago

While the prerender() hook would exclusively be used to render pages at build-time.

magne4000 commented 2 years ago

I assume that the page would be SSR'd before being added to the ISR cache.

Yes, but I need to generate a route regex at build time that describes all possible ISR routes.

I can't just tell Vervel to cache a page during SSR rendering

brillout commented 2 years ago

I'd suggest requiring the user to not use route functions when using ISR.

Anyways, route functions are usually used for dynamically determining which page should be rendered (e.g. auth header cookie) which doesn't make sense with ISR.

magne4000 commented 2 years ago

Agreed. So to be sure we're on the same page, here's how I see all of this working:

ISR enabled if:

brillout commented 2 years ago

Agreed.

One small nitpick:

  • .page exports initialRevalidateSeconds

How about if .page has export const isr = true or export const isr = { initialRevalidateSeconds: 10 }? Little bonus: it doesn't conflict with the global initialRevalidateSeconds value.

magne4000 commented 2 years ago

I think it would be better to remove the default initialRevalidateSeconds. Vercel ISR has no intention to be generic, so a default value does not really make sense IMO. Having a export const isr = { initialRevalidateSeconds: 10 } is kinda more portable though if any other provider does ISR, but I like the simplicity of just having initialRevalidateSeconds. What do you think?

brillout commented 2 years ago

The use case I've in mind for ISR is Stack Overflow or Reddit. Having millions of pages that cannot be pre-rendered at build-time.

For these websites I can see a default value to make sense and /user/:id and /product/:id would probably share the same default value.

magne4000 commented 2 years ago

Good point, let's go for export const isr then :+1:

magne4000 commented 2 years ago

v2 of Vercel's File System API has been deprecated, they are now working on a v3 https://github.com/vercel/community/discussions/530

brillout commented 2 years ago

Done https://github.com/magne4000/vite-plugin-vercel by @magne4000 💯. It even supports ISR.