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

vite-plugin-vercel #222

Closed
brillout opened this issue Dec 1, 2021 · 46 comments
Closed

vite-plugin-vercel #222

brillout opened this issue Dec 1, 2021 · 46 comments

Comments

@brillout
Copy link
Member

brillout commented Dec 1, 2021

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.

@brillout brillout added the enhancement ✨ New feature or request label Dec 1, 2021
@brillout brillout changed the title vite-plugin-vecel (making Vercel support rock-solid) vite-plugin-vercel (making Vercel support rock-solid) Dec 1, 2021
@patryk-smc
Copy link
Contributor

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
Copy link
Member Author

brillout commented Dec 2, 2021

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
Copy link
Contributor

patryk-smc commented Dec 8, 2021 via email

@brillout brillout changed the title vite-plugin-vercel (making Vercel support rock-solid) vite-plugin-vercel Dec 11, 2021
@brillout
Copy link
Member Author

👍

@brillout brillout reopened this Dec 11, 2021
@magne4000
Copy link
Member

magne4000 commented Mar 9, 2022

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)?

  • It would store types definitions for manifest files
  • It could have helpers (not sure if even useful)
    Beside types (that should be store inside @vercel/node IMO), I'm not seeing any real use case for this, do someone think of one?

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:

  • Add api/* to compilation stages
  • Move/copy dist/client to .output/server/pages + generate .output/routes-manifest.json
  • Move/copy dist/client/assets to .output/static
  • Move/copy dist/client/img to .output/static/img + generate optional .output/images-manifest.json
  • Generate .output/server/pages/**/[function].nft.json files for dependencies if any
  • Generate optional .output/functions-manifest.json if specified in config somewhere?

And regarding new ISR feature:

  • Generate .output/prerender-manifest.json for *.page.server.[tj]sx? that exports prerender with new params (could be same params as in next.js)

@brillout
Copy link
Member Author

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: vitejs/vite#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
Copy link
Member

magne4000 commented Mar 10, 2022

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
Copy link
Member Author

Yes

@magne4000
Copy link
Member

magne4000 commented Mar 11, 2022

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

  • Needs vite-plugin-ssr to work
    • make vite-plugin-ssr optional
    • dynamic injection of ssr endpoint
  • prerender is called programmatically to generates .output/server/pages
    • NOTE: Handy to be able to directly generate to .output/server/pages directory, but let's discuss if this should be handled by vite-plugin-ssr through config instead
    • make this configurable
  • Copy of vite-plugin-ssr client files to .output/assets
  • Generate a unique bundle through esbuild for each /api endpoint into .output/server/pages
    • logic/config to choose between .output/server/pages and .output/server/pages/api
    • support .output/server/pages/**/[function].nft.json if endpoints are sharing code
  • dynamicRoutes must be configured directly in the plugin instead of rewrites in vercel.json
    • with an additional ssr?: boolean prop to determine which endpoints are linked to vite-plugin-ssr
  • Following manifests are generated:
    • functions-manifest.json: tweak functions config, like maxDuration
      • make this configurable
      • handle functions with parameters
    • routes-manifest.json: copy user provided dynamicRoutes here
      • add more user provided config
      • handle functions with parameters
    • prerender-manifest.json: generates routes for each page, and dynamicRoutes for each ssr endpoint
      • handle functions with parameters
      • does not handle dataRoute for now
      • All pages are SSG and ISR for now (working for the demo, check /about, it should regenerate after 30 seconds 🎉):
        • should be able to ingest config (like "is ISR enable for this page?" or "what is the initialRevalidateSeconds value, globally or for each specific page")
        • ISR does not work if static page is not generated by SSG. This is currently not handled
  • asserts needs to be added
  • support .output/images-manifest.json

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

@patryk-smc
Copy link
Contributor

Hey @magne4000 that repo seems to be private ;)

@magne4000
Copy link
Member

oups 🤭. Fixed

@brillout
Copy link
Member Author

brillout commented Mar 12, 2022

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 vitejs/vite#5936?

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

@magne4000
Copy link
Member

magne4000 commented Mar 12, 2022

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 vitejs/vite#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
Copy link
Member Author

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
Copy link
Member

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
Copy link
Member Author

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
Copy link

rauchg commented Mar 16, 2022

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
Copy link
Member Author

@rauchg Much appreciated 👍. We actually have some question over there vercel/vercel#7573.

@magne4000
Copy link
Member

magne4000 commented Mar 16, 2022

I made some good progress:

  • Optional vite-plugin-ssr integration available (to be moved to vite-plugin-ssr repo)
    • No more need to manually create /api/ssr endpoint, the plugin adds one by default
    • It's possible to override it though. Just override the vercel.buildApiEndpoints config. To ease the process, you can reuse the exported buildApiEndpoints function, and pass it a custom source instead.
    • Some helpers are also available to help create your own ssr handler (see usage in template).
    • No need to manually import ../dist/server/importBuild anymore
  • Vercel specific configuration done and documented. Let me know if you have some remarks on it

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

TODO

For a beta version

  • Support functions with parameterized URLs (i.e. dynamicRoutes)
  • Probably find a better name for buildApiEndpoints option. Also, it should be able to support multiple callbacks like this (i.e. one for vite-plugin-ssr, one for telefunc, etc.)
    • custom buildApiEndpoints are currently calling esbuild themselves. I should probably reuse a build function exported by vite-plugin-vercel instead, to ensure homogeneous build artifacts
  • Add asserts
  • Add tests
  • Cleanup repo and move relevant code to vite-plugin-ssr + rename repo

After beta

  • Support .output/images-manifest.json
  • Support .output/server/pages/**/[function].nft.json

@brillout
Copy link
Member Author

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
Copy link
Member

magne4000 commented Mar 16, 2022

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
Copy link
Member Author

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
Copy link
Member

magne4000 commented Mar 16, 2022

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
Copy link
Member Author

Makes sense.

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

@magne4000
Copy link
Member

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

@magne4000
Copy link
Member

magne4000 commented Mar 21, 2022

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

Route functions are called on every request:

  • Not compatible with SSG or ISR

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

  • Fallback routes doesn't seem to work in Vercel
  • With current state of route functions, it would not take precedence into account with this solution

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

  • Easy solution
  • 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
  • We could export a const ignoreVercelRouteFunctionError = true next to the route function to bypass the error for a specific route
  • We could also warn that one should either use route functions or vite-plugin-vercel, but not both

@brillout
Copy link
Member Author

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
Copy link
Member Author

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.

  • SSR -> pageContext.routeParams is determined dynamically at run-time.
  • SSG -> pageContext.routeParams is determined statically at build-time.

@magne4000
Copy link
Member

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

@brillout
Copy link
Member Author

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
Copy link
Member

magne4000 commented Mar 21, 2022

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
Copy link
Member

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

@brillout
Copy link
Member Author

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
Copy link
Member

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
Copy link
Member

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:

  • If route is a string and initialRevalidateSeconds is exported in .page -> ISR url
  • else -> SSR url

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
Copy link
Member Author

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
Copy link
Member Author

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

@magne4000
Copy link
Member

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
Copy link
Member Author

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
Copy link
Member

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

ISR enabled if:

  • .page exports initialRevalidateSeconds
  • and one of the following is true:
    • page is prerendered
    • page is using filesystem routing (no .page.route file)
    • page is using route string

@brillout
Copy link
Member Author

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
Copy link
Member

magne4000 commented Mar 23, 2022

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
Copy link
Member Author

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
Copy link
Member

Good point, let's go for export const isr then 👍

@magne4000
Copy link
Member

v2 of Vercel's File System API has been deprecated, they are now working on a v3 vercel/community#530

@brillout
Copy link
Member Author

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

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

No branches or pull requests

4 participants