-
-
Notifications
You must be signed in to change notification settings - Fork 341
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
Single File Page. (Being able to define everything in a single file .page.js
.)
#53
Comments
@deckchairlabs what is it you like abour their API? Glazed over the first minutes of the video; didn't see anything special.
|
|
One thing we could do is to remove the
The user quickly and naturally understands that the page types Isn't it annoying to have to create all these files?Almost always the |
I suppose what I like is the fact they separate the "Loading" of data ( In my little playground I'm handling POST requests within a https://github.com/deckchairlabs/vite-remix-clone/blob/main/pages/posts.page.server.ts#L11 |
Exactly: https://github.com/deckchairlabs/vite-remix-clone 😍 Are you doing it for yourself or do you want to make it an open source proejct?
It's actually a common practice to do all kinds of things in |
At the moment just playing around, but eventually! |
This plugin is great! And I think the default behavior is very clear and easy to reason about, especially with the well written docs. However, I would love the ability to hook into the plugin's get page functionality, so that if someone wanted to have more control (and maybe implement a single file page, or their own framework with out this plugin's defaults), then they have the possibility of doing so at their own risk. This could be a dumb idea, but I would love to know what you think? |
@doeixd We can start by removing the |
I think it's actually clearer having the route in a separate file. There's no confusion as to whether it's isomorphic avoiding questions like "does the route information go in .server or .client?", "can it go in both?" I also definitely prefer the clarity of having separate server and client files. This has been so much nicer to work with than other frameworks that blur the line between server and client. |
Yes, that's something I highly value. This actually has been my main reason for pushing back against the many people who expressed a longing for having everything defined in a single file. But I can see that there is a solution here. For example, if the default is to have separate files, the docs always use sperate files, and having everything defined in a single file would merely be a convenience for "pro" users that know what they are doing.
That's a good point. Although strong docs can go a long way in clearing this kind of confusion out: simple and strong contracts between user and vite-plugin-ssr, API docs that are clear and complete, and guides that reflect the path of least resistance for 90% of use cases, while those 10% complex use cases are expected to (and usually inherently) require brain power from the user. |
Supporting both sounds like a good call. It of course increases the complexity of the codebase a bit, but if it's serving the preferences of a significant body of users, I suppose it is a relatively harmless addition.
Yeah I suppose if we make it super clear that routing information MUST go into the isomorphic page file and will simply be ignored in the other files, then maybe there's not that much room for error. |
This is a good point! Although I think it is possible to make a single file page, where it is clear where client/server functionality takes place. Just look at the success of projects like Next.js, Remix.js, and SvelteKit. They all handle this by exporting functions that clearly describe where the functionality takes place. Personally, I think something like this is just as clear as having separate files. All the functionality of the page is easily accessible and doesn't require 4 separate files for a simple page.
This is a great point! This functionality would require significant additional effort which may be out of scope for this project. And not adding it makes a lot of sense. |
Agreed.
I agree.
It may not increase the complexity much if it ends up only being a preprocessing step that is independent of the rest of the code. |
It gets less clear when you have dependencies. How can I be guaranteed that |
@gryphonmyers pretty much yep, magic! see e.g. https://next-code-elimination.vercel.app ✨ |
I personally hate magic 😂. Anyway, my opinions aside, if @brillout isn't worried about supporting this as an additional supported project structure then I guess we can all have our preferred project structures! |
.page.js
(remove .page.server.js
, .page.client.js
, .page.route.js
).page.js
.)
I guess we can agree that:
I will not implement SFP myself but I will happily accept a PR. If anyone wants to work on SFP talk to me (on Discord or here), I'm happy to give guidance. @doeixd let me know if you are still interested on working on this. |
Interest for this has been steadily decreasing. Maybe people are starting to realize that the clarity benefits of having separate files is worth it :-). |
If you disagree, let me know and I'll re-open the ticket. |
Re-opening because I see situations (especially with vps frameworks) where this would be quite nice. E.g. being able to seamlessly opt-in between SPA and SSR pages. @AaronBeaudoin WDYT of following design? // /pages/spa.page.js
// We mark `doNotPrerender` as server-side (vps generates a virtual module
// `spa.page.server.js` and moves `doNotPrerender` over there).
// @server
export const doNotPrerender = true
// We mark `Page` as client-side (vps generates a virtual module
// `spa.page.client.js` and moves `Page` over there).
// @client
export { Page }
// We don't mark this export => it stays in this `spa.page.js` module.
export const title = "An SPA page" This means that The whole thing is fairly easy to achieve. (Vite is designed to easily implement things like this, e.g. Vue's SFCs.) The only thing that is non-trivial is code pruning: // /pages/spa.page.js
// This import also needs to be pruned
import something from 'some-dependency'
// `Page` is moved to a virtual module `spa.page.client.js`. In other words: it's
// pruned from `spa.page.js`.
// @client
export function Page() {
// `something()` is only used by `Page`
something()
} But it's definitely doable and has been done before (e.g. Next.js or @cyco130's Rakkas). Vite uses @cyco130 I guess you used Babel because you were more familiar with it? Nothing speaks against Acorn, right? |
I would think so. But Vite's React plugin already uses Babel, that's why I started there. Here's my implementation and its tests. It strips |
Ok that makes sense. (Although Aaron is using Vue so he probably won't want Babel to clutter his Thanks for the pointers. |
He brings up some good points. I'm also considering what you've mentioned recently as well. I think the one-file approach has it's pros and cons vs the current multiple-file approach. I think maybe the two concepts could be merged in a tasteful way, but I'm not sure how I would do it yet. I'll definitely be back here when I come up with something. |
Agreed.
💯 Very much looking forward to it 👀. |
Related, seems like React Server Components are moving away from |
@redbar0n Yea I've seen. I think it's a great move. |
How about making something like a "Header File" // _define.js
const prerender = true
export default {
// ❌ Forbidden: functions cannot be defined in `_define.js`
onRenderHtml() {
/* ... */
},
// ❌ Forbidden: assignment to a variable
prerender
} // _define.js
import { onRenderHtml } from './onRenderHtml'
export default {
// ✅ Allowed: function can be imported and then exported in `_define.js`
onRenderHtml,
// ✅ Allowed: assignment to a value
prerender: true
} I believe this solves all problems. I would even argue that it's a good thing that |
Warning: This comment might be a bit of a mess. I figured rather than taking forever to think through every little detail it would be better to just get something out there so we can move the conversation forward. From my perspective, it seems that VPS has 3 primary concerns, which are all related but separate:
Below, I've written out "pseudo-docs" for a potential VPS API redesign with the following principles at heart:
Server-Side Rendering APIThis section documents the VPS API for server-side rendering. To pass data from the server to the client, add it under a
|
Value | Behavior |
---|---|
server-and-client |
The page should be rendered to an HTML string, inserted under some root element on the server, then rendered to the DOM in the browser again on the client, replacing the HTML under the root element. |
server-only |
The page should be rendered to an HTML string on the server only. |
client-only |
The page should be rendered to the DOM in the browser on the client only. |
export routing: Object
Holds the configuration for routing.
<name>.server.[js|ts]
Purpose: Override the currently applicable _config.server.[js|ts]
file for <name>.page.<ext>
.
<name>.client.[js|ts]
Purpose: Override the currently applicable _config.server.[js|ts]
file for <name>.page.<ext>
.
Pre-Rendering API
This section documents the VPS API for pre-rendering.
_config.server.[js|ts]
Additions
export onPreRender(context): Object
Return object is merged into the received context.
<name>.page.<ext>
Additions
export prerender: Boolean
Sets whether the page should be prerendered at build time. Default is false
.
Client-Side Routing API
Not sure about this part, this comment is already long enough, and I'm not sure anything I'd write under this section would change the core philosophy I'm going for here.
Hopefully my root idea here makes sense. I know I'm missing some things and probably made some errors here and there, but my core mindset is to make it such that you have some "base" configuration that can be overridden, but that whole mechanism is separate from the concern of defining your pages' framework-specific components.
So, basically, the definitions of framework-specific components are "single file" as discussed in this issue, but everything else that happens around them are in separate .server
or .client
files. This makes my new proposal a bit closer to how VPS is currently designed, while (hopefully) providing a clearer separation of concerns.
Maybe there are technical or logical reasons this won't work that I haven't considered. That's why I'm interest to hear what your thoughts are on this whole idea, and whether you think it even makes sense to begin with.
It seems to me that we have similar goals and I think that the design of my previous comment does cover your aspirations. Let me clarify. The // /pages/product/_define.ts
import type { Define } from 'vite-plugin-ssr'
import { Page } from './Page'
import { onBeforeRender } from './onBeforeRender'
import { onPrerender } from './onPrerender'
// This file replaces:
// - /pages/product/index.page.js
// - /pages/product/index.page.server.js
// - /pages/product/index.page.client.js
// - /pages/product/index.page.route.js
export default {
renderMode: 'spa', // => `Page` is loaded only in client
// renderMode: 'ssr' => `Page` is loaded in client & server
Page, // (would normally live in `.page.client.js`)
route: '/product/@id', // (would normally live in `.page.route.js`)
onBeforeRender, // (would normally live in `.page.js`)
onPrerender, // (would normally live in `.page.server.js`)
exports: {
onBeforeRender: {
// By default, onBeforeRender() runs only on the server-side.
// But we can configure to run it also on the client-side:
env: ['server', 'client']
}
},
prerender: false
} satisfies Define // (New TypeScript 4.9 operator) // /pages/_define.ts
// This file replaces:
// - /renderer/_default.page.server.js
// - /renderer/_default.page.client.js
// - VPS config defined in vite.config.js
import type { Define } from 'vite-plugin-ssr'
import { onRenderHtml } from './onRenderHtml'
import { onRenderClient } from './onRenderClient'
export default {
onRenderHtml, // Is loaded only on the server-side
onRenderClient, // Is loaded only on the client-side
prerender: true, // Default value for all pages
renderMode: 'ssr', // Default value for all pages
includeAssetsImportedByServera: true, // (would normally live in vite.config.js)
// Custom exports:
exports: {
title: {
env: ['client', 'server']
},
description: {
env: ['server']
}
}
} satisfies Define The magic here is that // _define.js
export default {
// ❌ Forbidden: functions cannot be defined in `_define.js`
onRenderHtml() {
/* ... */
}
} // _define.js
import { onRenderHtml } from './onRenderHtml'
export default {
// ✅ Allowed: functions are imported and then re-exported over `_define.js#default`
onRenderHtml
}
This is the crux of the idea as it enables VPS to load code in the right environemnt. Single Route FileIt enables a natural way to define a "Single Route File": // /pages/_define.ts
import { Landing } from './Landing'
import { About } from './About'
import { Jobs } from './Jobs'
import type { Define } from 'vite-plugin-ssr'
export default {
pages: [
{
Page: Landing,
route: '/'
},
{
Page: About,
route: '/about'
},
{
Page: Jobs,
route: '/jobs'
}
]
} satisfies Define The beauty here is that VPS can automatically code-split the Nested LayoutsIt even enables a natural way to define nested Layouts: // /pages/product/_define.ts
import { Overview } from './Overview'
import { onBeforeRender } from './onBeforeRender'
import { Reviews } from './reviews'
import { onBeforeRenderReviews } from './reviews/onBeforeRender'
import { Details } from './details'
import type { Define } from 'vite-plugin-ssr'
export default {
Page: Overview,
route: '/product/@id',
onBeforeRender,
nested: [
{
Page: Details,
route: '/product/@id/details',
},
{
Page: Reviews,
route: '/product/@id/reviews',
onBeforeRender: onBeforeRenderReviews
}
]
} satisfies Define FrameworksThis design also further enables frameworks built on top of VPS. IDE PluginPlugins can easily be written to tell the user which file is loaded in what environment. This is easy to implement as it can be done by statically anlayzing code. From React's Server Components RFC:
|
It seems like this should work, but how would I do the same thing as a current Basically, in the case that I have a "server-only" page, where would my "minimal JavaScript" go? When you say this in the docs regarding
...I think this is one of the most beautiful things about VPS. |
@AaronBeaudoin Good point. How about this? // /pages/product/_define.ts
import type { Define } from 'vite-plugin-ssr'
export default {
route: '/product/@id',
renderMode: 'HTML',
client: './client.ts'
} as Define // /pages/product/client.ts
// When `renderMode` is 'HTML' then this reprents the *entire* clent-side code |
For enabling IntelliSense, we could even do this: // _define.ts
export default {
client: import('./client')
} This works since Using |
Just renamed |
Update: The trick is to set hook properties to a string that represent the path to the hook file: // /pages/_define.ts
import type { Define } from 'vite-plugin-ssr'
- import { onRenderHtml } from './onRenderHtml'
- import { onRenderClient } from './onRenderClient'
export default {
- onRenderHtml,
+ onRenderHtml: './onRenderHtml',
- onRenderClient,
+ onRenderClient: './onRenderClient',
} satisfies Define That way, we can skip this whole thing of It's now real JavaScript, it just happens to not import any code. (We can show a warning if |
I'm thinking of allowing users to skip creating /renderer/+onRenderHtml.ts # The '+' sign denotes a VPS property
/renderer/+onRenderClient.ts
/pages/index/+Page.ts
/pages/product/+Page.ts
/pages/product/+route.ts
/pages/product/+onBeforeRender.ts A // /pages/+exports.ts
import type { Exports } from 'vite-plugin-ssr'
export default {
// Pages are SSR'd by default
ssr: true
} satisfies Exports // /pages/admin/+exports.ts
import type { Exports } from 'vite-plugin-ssr'
export default {
// The admin panel doesn't need SSR
ssr: false,
// Single Route File + Nested Layouts
pages: [
{
route: '/admin',
Page: './Dashboard.ts'
},
{
route: '/admin/invoices',
Page: './Invoices.ts'
},
{
route: '/admin/db',
Page: './DataViewer.ts',
nested: [
{
route: '/admin/db/users',
View: './Users'
},
{
route: '/admin/db/products',
View: './Products'
}
]
}
]
} satisfies Exports |
Custom Exports can then be defined with // /renderer/+exports.ts
import type { Exports } from 'vite-plugin-ssr'
export default {
exportables: [
{
name: 'title',
env: 'SERVER_ONLY' // | 'CLIENT_ONLY' | 'CLIENT_AND_SERVER'
}
]
} satisfies Exports // /pages/index/+title.ts
// The file is loaded only in Node.js (at server run-time, or at build-time while pre-rendering)
export default 'Welcome to the new VPS design' |
I actually really love those ideas! The First, I just want to make sure I understand what all the options are here for structuring a page's code. Are all of these options below valid?
|
Exactly! The
No: if you don't define any
Yes
No:
Yes
Yes
Yes. Also I'm thinking to allow this:
I'm actually leaning towards enforcing route strings to always be absolute. I think the extra verbosity is worth it.
In my expereince, this doesn't really happen in practice. Over 99% of the time, I expect
What you can do is this: /pages/product.+config.js # { page: './product/ProductPage.vue', onBeforeRender: './product/onBeforeProductRender.js' }
/pages/product/ProductPage.vue
/pages/product/onBeforeProductRender.js
Good idea! I actally love the
The thing is that In the following example, naming /pages/index/+config.js # { page: '/Page.vue', onBeforeRender: './onBeforeRender.js' }
/pages/index/Page.vue
/pages/index/onBeforeRender
/pages/about/+config.js # { page: '/Page.vue', onBeforeRender: './onBeforeRender.js' }
/pages/about/Page.vue
/pages/about/onBeforeRender
I think we can settle on I really like the
The idea is that the end-user almost never defines custom exports. It's almost always going to be the VPS framework. (I'm going to release a prototype showcasing how such VPS framework will look like.) That's why I think it's a good thing to keep the same
Exactly. (FYI Telefunc's Vite plugin needs to work with any UI Framework and with any meta framework.) |
I changed my mind. I don't think A much cleaner approach is a new option // /pages/+config.ts
import type { Config } from 'vite-plugin-ssr'
const marketingPagesConfig = {
Layout: './layouts/LayoutMarketingPages.vue',
ssr: true,
}
const adminPagesConfig = {
title: 'Admin Panel',
Layout: './layouts/LayoutAdminPages.vue',
ssr: false
}
export default {
// If `singleConfigFile: true` then only one `+` file is allowed (this file). If there is
// anothoer `+` file, then VPS shows a warning.
singleConfigFile: true,
pages: [
{ ...marketingPagesConfig, Page: './LandingPage.vue', route: '/' , title: 'Awesome Startup' },
{ ...marketingPagesConfig, Page: './JobsPage.vue', route: '/jobs' , title: 'We're hiring!' },
{ ...marketingPagesConfig, Page: './VisionPage.vue', route: '/vision' , title: 'Our mission <3' },
{ ...adminPagesConfig, Page: './AdminPage.vue', route: '/admin' , onBeforeRender: './AdminPage-onBeforeRender.ts' },
{ ...adminPagesConfig, Page: './AdminDbPage.vue', route: '/admin/db', onBeforeRender: './AdminDbPage-onBeforeRender.ts' }
]
} satisfies Config The whole thing ends up being quite simple:
That's it. I foresee Alright, I think this is ripe for an RFC. |
Edit: renamed |
Closing in favor of #578. |
could you show me how it works in the sandbox? |
@champ7champ There isn't an example for this yet. |
|
Yes, soon, the overall V1 architecture is already implemented. It's now about implementing the long tail of details. |
Instead of having several page files (
.page.js
,.page.route.js
,.page.server.js
,.page.client.js
), we would define everything in.page.js
(whilevite-plugin-ssr
automatically statically extracts the relevant parts like in https://next-code-elimination.vercel.app.)Many people have expressed a longing for this, but I'm on the fence. Simply because I highly value clarity and simplicity: it's obvious and simple what the
.page.js
,.page.route.js
,.page.server.js
, and.page.client.js
files are about, whereas if we merge everything in.page.js
it becomes less clear what is run in what environment.The text was updated successfully, but these errors were encountered: