Closed brillout closed 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)
);
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.
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.
👍
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)?
@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:
api/*
to compilation stagesdist/client
to .output/server/pages
+ generate .output/routes-manifest.json
dist/client/assets
to .output/static
dist/client/img
to .output/static/img
+ generate optional .output/images-manifest.json
.output/server/pages/**/[function].nft.json
files for dependencies if any.output/functions-manifest.json
if specified in config somewhere?And regarding new ISR feature:
.output/prerender-manifest.json
for *.page.server.[tj]sx?
that exports prerender with new params (could be same params as in next.js)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.
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.
Yes
Here is the repo with the first working tests. What does it do for now (or don't):
vite-plugin-ssr
to work
vite-plugin-ssr
optionalprerender
is called programmatically to generates .output/server/pages
.output/server/pages
directory, but let's discuss if this should be handled by vite-plugin-ssr
through config insteadvite-plugin-ssr
client files to .output/assets
esbuild
for each /api
endpoint into .output/server/pages
.output/server/pages
and .output/server/pages/api
.output/server/pages/**/[function].nft.json
if endpoints are sharing codedynamicRoutes
must be configured directly in the plugin instead of rewrites
in vercel.json
ssr?: boolean
prop to determine which endpoints are linked to vite-plugin-ssr
functions-manifest.json
: tweak functions config, like maxDuration
routes-manifest.json
: copy user provided dynamicRoutes
hereprerender-manifest.json
: generates routes
for each page, and dynamicRoutes
for each ssr
endpointdataRoute
for nowinitialRevalidateSeconds
value, globally or for each specific page").output/images-manifest.json
Deployed at https://test-vite-vercel-plugin.vercel.app/
Hey @magne4000 that repo seems to be private ;)
oups :hand_over_mouth:. Fixed
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:
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.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.
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.
🙇
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).
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.)
I think it would be better that if both
vite-plugin-ssr
andvite-plugin-vercel
are installed, the user does not have to manually set the prerender function invite.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
.
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.
@rauchg Much appreciated 👍. We actually have some question over there https://github.com/vercel/vercel/discussions/7573.
I made some good progress:
vite-plugin-ssr
integration available (to be moved to vite-plugin-ssr
repo)
/api/ssr
endpoint, the plugin adds one by defaultvercel.buildApiEndpoints
config. To ease the process, you can reuse the exported buildApiEndpoints function, and pass it a custom source instead.../dist/server/importBuild
anymoreUpdated live demo with SSR/SSG/ISR: https://test-vite-vercel-plugin.vercel.app/
dynamicRoutes
)buildApiEndpoints
option. Also, it should be able to support multiple callbacks like this (i.e. one for vite-plugin-ssr
, one for telefunc
, etc.)
buildApiEndpoints
are currently calling esbuild themselves. I should probably reuse a build function exported by vite-plugin-vercel
instead, to ensure homogeneous build artifactsassert
svite-plugin-ssr
+ rename repo.output/images-manifest.json
.output/server/pages/**/[function].nft.json
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?
Little typo here https://github.com/magne4000/test-vite-vercel-plugin/blob/41af424c620b14c61697ea838404dfc952992474/packages/vercel/src/types.ts#L117 (
ssrisr) 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.
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.
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
Makes sense.
How about flattening then? I.e. defining initialRevalidateSeconds
and prerender
directly on the config root.
Indeed. Simpler I don't see drawbacks as the configuration is quite minimal, this is probably the way to go :+1:
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
:
const ignoreVercelRouteFunctionError = true
next to the route function to bypass the error for a specific routevite-plugin-vercel
, but not bothRoute 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).
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.
pageContext.routeParams
is determined dynamically at run-time.pageContext.routeParams
is determined statically at build-time.Ok thanks, I still need some fallback rule then I guess
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.
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.
You can check all use cases I could think of https://test-vite-vercel-plugin.vercel.app/
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.
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.
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:
initialRevalidateSeconds
is exported in .page
-> ISR urlWe 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 casesISR
Relevant for this use case
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.
While the prerender()
hook would exclusively be used to render pages at build-time.
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
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.
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
.page.route
file)Agreed.
One small nitpick:
.page
exportsinitialRevalidateSeconds
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.
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?
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.
Good point, let's go for export const isr
then :+1:
v2
of Vercel's File System API has been deprecated, they are now working on a v3
https://github.com/vercel/community/discussions/530
Done https://github.com/magne4000/vite-plugin-vercel by @magne4000 💯. It even supports ISR.
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.