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

Single File Page. (Being able to define everything in a single file .page.js.) #53

Closed
brillout opened this issue May 3, 2021 · 82 comments
Labels
enhancement ✨ New feature or request

Comments

@brillout
Copy link
Member

brillout commented May 3, 2021

Instead of having several page files (.page.js, .page.route.js, .page.server.js, .page.client.js), we would define everything in .page.js (while vite-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.

@brillout
Copy link
Member Author

brillout commented May 3, 2021

When I stumbled across this project my immediate thought was "I could make a clone of Remix with this" https://remix.run/
I really like the clean API they have https://www.youtube.com/watch?v=4dOAFJUOi-s&t=1281s

@deckchairlabs what is it you like abour their API? Glazed over the first minutes of the video; didn't see anything special.

Yeah, being able to define everything in *.page.ts files would be so great.

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.

@brillout
Copy link
Member Author

brillout commented May 3, 2021

@chrisvariety

having it all in one file and shaking out the server-side dependencies (see https://next-code-elimination.vercel.app ) could be a nice enhancement.

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.

@brillout
Copy link
Member Author

brillout commented May 3, 2021

One thing we could do is to remove the .page.route.js file (instead, the user export { route } in .page.js). We then have:

  • .page.js => Node.js & Browser
  • .page.server.js => Node.js
  • .page.client.js => Browser

The user quickly and naturally understands that the page types .page.js VS .page.server.js VS .page.client.js are only about where code runs. (As things currently are, the .page.route.js adds semantics to what .page.* types are about and can lead to confusion.)

Isn't it annoying to have to create all these files?

Almost always the .page.client.js is written only once as _default.page.client.js. So the user ends up having to write a max of 2 files (.page.js and .page.server.js) per page, instead of only one file per page. Yes, it's slightly annoying to have to create 2 files instead of one, but it would, IMO, be worth it in terms of clarity.

@deckchairlabs
Copy link

@brillout

@deckchairlabs what is it you like abour their API? Glazed over the first minutes of the video; didn't see anything special.

I suppose what I like is the fact they separate the "Loading" of data (addContextProps) and handling of an incoming "Action" (form POST for instance). Although in saying that, I do like that with this plugin, there isn't as much "magic" as appears to be happening with Remix.

In my little playground I'm handling POST requests within a *.page.server.ts within addContextProps.

https://github.com/deckchairlabs/vite-remix-clone/blob/main/pages/posts.page.server.ts#L11

@brillout
Copy link
Member Author

brillout commented May 4, 2021

I do like that with this plugin, there isn't as much "magic" as appears to be happening with Remix.

Exactly: vite-plugin-ssr aims to be non-invasive and to let you having control over your stack architecture.

https://github.com/deckchairlabs/vite-remix-clone 😍 Are you doing it for yourself or do you want to make it an open source proejct?

In my little playground I'm handling POST requests within a *.page.server.ts within addContextProps.

It's actually a common practice to do all kinds of things in addContextProps(). An alternative is to do it before renderPage() is called.

@deckchairlabs
Copy link

https://github.com/deckchairlabs/vite-remix-clone 😍 Are you doing it for yourself or do you want to make it an open source proejct?

At the moment just playing around, but eventually!

@doeixd
Copy link

doeixd commented May 5, 2021

Exactly: vite-plugin-ssr aims to be non-invasive and to let you having control over your stack architecture.

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?

@brillout
Copy link
Member Author

brillout commented May 5, 2021

@doeixd We can start by removing the .page.route.js file and see how it goes. I'm not sure that we'll achieve to remove .page.server.js and .page.client.js in a 1. DX friendly way, and 2. without too much burden on the vite-plugin-ssr source code; things should stay simple. But I'm happy to explore this area. To get started you can make yourself familiar with Vite/rollup's architecture, e.g. https://vitejs.dev/guide/api-plugin.html#transforming-custom-file-types.

@gryphonmyers
Copy link
Contributor

gryphonmyers commented May 11, 2021

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.

@brillout
Copy link
Member Author

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.

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

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.

@gryphonmyers
Copy link
Contributor

gryphonmyers commented May 12, 2021

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.

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.

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.

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.

@doeixd
Copy link

doeixd commented May 12, 2021

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.

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.

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.

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.

@brillout
Copy link
Member Author

Agreed.

Personally, I think something like this is just as clear

I agree.

increases the complexity of the codebase a bit

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.

@gryphonmyers
Copy link
Contributor

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.

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.

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.

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.

It gets less clear when you have dependencies. How can I be guaranteed that node-fetch isn't making it into my client bundle? Magic?

@chrisvariety
Copy link
Contributor

@gryphonmyers pretty much yep, magic! see e.g. https://next-code-elimination.vercel.app

@gryphonmyers
Copy link
Contributor

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!

@brillout brillout changed the title Define everything in .page.js (remove .page.server.js, .page.client.js, .page.route.js) Single File Page. (Being able to define everything in a single file .page.js.) May 13, 2021
@brillout
Copy link
Member Author

I guess we can agree that:

  1. Docs would still be based on separate files to keep things clear.
  2. SFP (Single File Page) would be optional.

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.

@brillout
Copy link
Member Author

Interest for this has been steadily decreasing. Maybe people are starting to realize that the clarity benefits of having separate files is worth it :-).

@brillout
Copy link
Member Author

If you disagree, let me know and I'll re-open the ticket.

@brillout
Copy link
Member Author

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 Page and doNotPrerender are removed from spa.page.js.

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 acorn, so ideally we would use acorn as well for this (reducing the total number of dependencies).

@cyco130 I guess you used Babel because you were more familiar with it? Nothing speaks against Acorn, right?

@brillout brillout reopened this Jul 27, 2022
@cyco130
Copy link

cyco130 commented Jul 27, 2022

@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 headers, prerender, and action functions from the client bundle. I just pass this plugin to vite-plugin-react's babelPlugins option (more or less, an options.ssr test is needed which has been merged recently).

@brillout
Copy link
Member Author

Ok that makes sense. (Although Aaron is using Vue so he probably won't want Babel to clutter his node_modules.)

Thanks for the pointers.

@AaronBeaudoin
Copy link
Contributor

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.

@brillout
Copy link
Member Author

brillout commented Oct 5, 2022

I think maybe the two concepts could be merged in a tasteful way

Agreed.

I'll definitely be back here when I come up with something.

💯 Very much looking forward to it 👀.

@redbar0n
Copy link
Contributor

redbar0n commented Nov 4, 2022

Related, seems like React Server Components are moving away from .client.js and .server.js file conventions, in favor of a use 'client'; directive inside files: reactjs/rfcs#227

@brillout
Copy link
Member Author

brillout commented Nov 6, 2022

@redbar0n Yea I've seen. I think it's a great move.

@brillout
Copy link
Member Author

brillout commented Nov 9, 2022

How about making something like a "Header File" _define.js.

// _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 _define.js isn't allowed to have real JavaScript code: it ensures that _define.js is kept small and therefore easy to read.

@vikejs vikejs deleted a comment from shishkin Nov 10, 2022
@AaronBeaudoin
Copy link
Contributor

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:

  • Server-Side Rendering
  • Pre-Rendering
  • Client-Side Routing

Below, I've written out "pseudo-docs" for a potential VPS API redesign with the following principles at heart:

  • No more .page.route.js. Routing is important, but putting it in a separate file isn't ergonomic.
  • No more .page.server.js or .page.client.js. Instead, use a renderMode export in .page.js.
  • Two .server.js and .client.js files now somewhat replace .page.server.js and .page.client.js. Their purpose is not the same however. .page.js is now just the central file where pages' framework-specific root components live, with a few special VPS config exports. .server.js and .client.js are responsible for containing any "extra" code you want to run on the server or the client not related to your framework's page component itself. They are simply overrides for your _config.server.js and _config.client.js files.

Server-Side Rendering API

This section documents the VPS API for server-side rendering.

To pass data from the server to the client, add it under a _passToClient property on the return object, which will be merged with the context and serialized to the client.

_config.server.[js|ts]

Purpose: Contains configuration for the current directory and all subdirectories related to rendering on the server. Can be placed in a _config directory for better organization.

export onBeforeRenderHtml(context): Object

Return object is merged into the received context.

export onRenderHtml(context): Object

Return object is merged into the received context. It should have an _documentHtml property whose value is a string which should be sent as the response body to the client. This function is where you put your framework specific code for rendering your page component to an HTML string.

_config.client.[js|ts]

Purpose: Contains configuration for the current directory and all subdirectories related to rendering on the client. Can be placed in a _config directory for better organization.

export onBeforeRenderHead(context): Object

Return object is merged into the received context. Code from this function is included and run in the document <head>.

export onBeforeRenderBody(context): Object

Return object is merged into the received context. Code from this function is included and run at the top of the document <body>.

export onBeforeRenderBrowser(context): Object

Return object is merged into the received context. Code from this function is included and run at the bottom of the document <body>.

export onRenderBrowser(context): Object

Return object is merged into the received context. This function is where you put your framework specific code for rendering your page component to the DOM.

<name>.page.<ext>

Purpose: Contains a framework-specific export which can be rendered to HTML in onRenderHtml(). If <name> is _error, the page will be rendered whenever applicable based on the current VPS rules.

export renderMode: String

Sets the environment in which the page should be rendered. Default is server-and-client.

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.

@brillout
Copy link
Member Author

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 _define.js files are the only VPS files. That's the beauty of it. If you open all _define.js files then you're browsing the entire interface between the user and VPS.

// /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.ts isn't real JavaScript. It's a subset:

// _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
}

_define.js is like a header file: it links user code with VPS.

This is the crux of the idea as it enables VPS to load code in the right environemnt.

Single Route File

It 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 Landing/About/Jobs components. VPS knows that the Jobs component is only need for the route /jobs. (Behind the curtain, VPS generates a bunch of virtual files: one virtual file for the global settings that apply to all pages + one virtual file per each page X each environement. This all happens automatically without the user having to do anything.)

Nested Layouts

It 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

Frameworks

This design also further enables frameworks built on top of VPS.

IDE Plugin

Plugins 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:

As for the problem that the user doesn't know in what environments components are loaded (the uncanny valley problem), I think the "ultimate solution" would be IDE plugins showing in what env the component is loaded. For example a plugin changing the background color of the current opened file: green background => Server Component, blue background => Client Component, yellow background => Shared Component.

@AaronBeaudoin
Copy link
Contributor

AaronBeaudoin commented Dec 2, 2022

It seems like this should work, but how would I do the same thing as a current <name>.page.server.js and <name>.page.client.js combo? To me, this is super powerful because it allows me to have the server handle everything about rendering the page (in scenarios where such a setup is ideal, which in my case is actually quite a few) and then, as you say in the docs, use a <name>.page.client.js "to add minimal amount of JavaScript surgically injecting bits of interactivity".

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 <name>.page.client.js...

It represents the entire browser-side code. This means that if don't create any .page.client.js file, then our app has zero browser-side JavaScript.

...I think this is one of the most beautiful things about VPS.

@brillout
Copy link
Member Author

brillout commented Dec 3, 2022

@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

@brillout
Copy link
Member Author

brillout commented Dec 3, 2022

For enabling IntelliSense, we could even do this:

// _define.ts
export default {
  client: import('./client')
}

This works since _define.ts is never run but merely a meta file for VPS.

Using import() would align slightly better with the practice of importing all hooks. Let's see.

@brillout
Copy link
Member Author

brillout commented Dec 3, 2022

Just renamed clientCode to client. (Although clientCode has the advantage of being googleable and searchable in the docs.)

@brillout
Copy link
Member Author

brillout commented Dec 8, 2022

Update: _define.ts can be a plain normal JavaScript file.

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 _define.js not being real JavaScript.

It's now real JavaScript, it just happens to not import any code. (We can show a warning if _define.js imports code because this shouldn't be needed.)

@brillout
Copy link
Member Author

brillout commented Dec 12, 2022

I'm thinking of allowing users to skip creating _define.js:

/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 _define.js +exports.js file is still useful to define default values and for Single Route Files and Nested Layouts:

// /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

@brillout
Copy link
Member Author

Custom Exports can then be defined with exportables:

// /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'

@AaronBeaudoin
Copy link
Contributor

AaronBeaudoin commented Dec 13, 2022

I actually really love those ideas!

The +whatever.xyz file naming scheme reminds me of all the Svelte stuff I was reading about, and I think it's pretty nice due to how it marks the files as being "meta-framework" concerns.

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?

http://site.com/example

pages/
  example.jsx
pages/
  example/
    index.jsx
pages/
  example/
    +Page.jsx
pages/
  example/
    +exports.js # No `pages` property.
    index.jsx
pages/
  example/
    +exports.js # No `pages` property.
    +Page.jsx
pages/
  example/
    +exports.js # pages: [{ route: "/", Page: "./custom.jsx" }]
    custom.jsx

A huge value for me of that last option is the fact that you can give your "page" files unique names so your IDE tabs don't look like <index.jsx> <index.jsx> <index.jsx> or <+Page.jsx> <+Page.jsx> <+Page.jsx>. Also, I put / as the route because I'm hoping that the routes in the pages of the config would be relative to the current directory. I can't imagine a circumstance where it would ever be a good idea to need to define a route up a level (..).

Also, what do you think of the idea of using the name renderable or component instead of Page? Because semantically a "page" might have other files that are related to it like for example a client.js file for custom JS.


Also, is there some option for nesting all of the +whatever.xyz files under some subdirectory for organization, sort of like how the current _default directory works under pages/? Here's sort of a "kitchen sink" example:

pages/
  example/
    +exports/ # Not sure if this is the right name for this...
      +exports.js # client: "../client.js"
      +onBeforeRender.js
      +onRenderClient.js
    index.jsx
    client.js

If this isn't an option, I think it probably should be. Because I think now we're probably looking at the addition of a lot of +whatever.js files everywhere, and without a "drawer" to stuff them in, the filesystem is going to get a bit chaotic. Also, it makes things pretty clean because I can by convention choose to always put all VPS-related stuff in a predictable place.


Also, just my two cents, but the name +exports makes me think semantically at first glance that the file is only for defining custom exports like title as you described, but not for VPS exports such as onBeforeRender. I think the most intuitive name is something else like +config or +setup or +base or +default or +path or +pages (trying to just dump a lot of ideas here). My favorite is actually +pages, which seems to convey the intent to define some config/exports for all the pages at or below the current level, aligning with the name of the root directory pages/ itself. Alternatively, I also think +config is quite clear.


The + naming convention itself signals "this file is a VPS thing" as you mentioned, which I think is a good thing, but then with the way you're doing "exportables", now anyone can add a new +anything.js file. So then new users later are probably Googling +title.js or whatever and trying to figure out why it isn't documented anywhere. Not ideal in my opinion.

So I think maybe you should have some convention +export.title.js or +export.server.title.js or +export.client.title.js, all of which would automatically get picked up by VPS and defined in exportables with no extra work required. I know the typical order feels like it should be something like title.export.js, but I actually think doing it "backwards" like this with a + at the start results in:

  • Superior readability and sorting in the filesystem.
  • Retaining the signaling of the file as a "VPS thing".
  • Simplicity when defining custom exports (no +exports file needed).

The + is already at the start of the file anyways (as opposed to <whatever>.vps.js or something like that). But as you mentioned a while back, in the end naming isn't a dealbreaker. I do personally feel like excellent developer ergonomics tend to draw a crowd of dedicated users though, so that's why I love providing feedback in this area.


Lastly, I do want to mention that at first I looked at this and thought "oh this will make integrating with Svelte more difficult", but then I realized that the + thing is a SvelteKit thing, not a Svelte thing, and obviously you're not going to be trying to use SvelteKit and VPS at the same time, so I think we're fine. The way I understand it, VPS is compatible with any JavaScript UI framework, but incompatible with every JavaScript meta-framework. Hopefully I'm understanding that correctly. We'll have to see what happens if more UI frameworks start to implement "meta-framework dependent" features like React server components and those kinds of features start to get more popular.

@brillout
Copy link
Member Author

The +whatever.xyz file naming scheme [...] it's pretty nice due to how it marks the files as being "meta-framework" concerns.

Exactly! The + is very clear that something is being added/defined.

Are all of these options below valid?

http://site.com/example

pages/
  example.jsx
pages/
  example/
    index.jsx

No: if you don't define any + file then your repository is empty from VPS's persective. The entire interface between VPS and the user happens over + files.

pages/
  example/
    +Page.jsx

Yes

pages/
  example/
    +exports.js # No `pages` property.
    index.jsx

No: +exports.js doesn't define any page and index.jsx isn't referenced by any +exports.js.

pages/
  example/
    +exports.js # No `pages` property.
    +Page.jsx

Yes

pages/
  example/
    +exports.js # pages: [{ route: "/", Page: "./custom.jsx" }]
    custom.jsx

Yes

so your IDE tabs don't look like <index.jsx> <index.jsx> <index.jsx> or <+Page.jsx> <+Page.jsx> <+Page.jsx>.

Yes. Also I'm thinking to allow this:

/pages/index.+page.vue
/pages/about.+page.tsx
/pages/product/@id.+page.tsx
/pages/product/@id.+onBeforeRender.tsx

Also, I put / as the route because I'm hoping that the routes in the pages of the config would be relative to the current directory. I can't imagine a circumstance where it would ever be a good idea to need to define a route up a level (..).

I'm actually leaning towards enforcing route strings to always be absolute. I think the extra verbosity is worth it.

Also, what do you think of the idea of using the name renderable or component instead of Page? Because semantically a "page" might have other files that are related to it like for example a client.js file for custom JS.

In my expereince, this doesn't really happen in practice. Over 99% of the time, I expect +page.ts to actually denote a page. For other kinds of exports, you can define a new "exportable".

Also, is there some option for nesting all of the +whatever.xyz files under some subdirectory for organization, sort of like how the current _default directory works under pages/? Here's sort of a "kitchen sink" example:

pages/
  example/
    +exports/ # Not sure if this is the right name for this...
      +exports.js # client: "../client.js"
      +onBeforeRender.js
      +onRenderClient.js
    index.jsx
    client.js

If this isn't an option, I think it probably should be. Because I think now we're probably looking at the addition of a lot of +whatever.js files everywhere, and without a "drawer" to stuff them in, the filesystem is going to get a bit chaotic. Also, it makes things pretty clean because I can by convention choose to always put all VPS-related stuff in a predictable place.

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

I think the most intuitive name is something else like +config or +setup or +base or +default or +path or +pages (trying to just dump a lot of ideas here).

Good idea! I actally love the +config.js name. Very clear.

My favorite is actually +pages, which seems to convey the intent to define some config/exports for all the pages at or below the current level, aligning with the name of the root directory pages/ itself.

The thing is that +config.js may define only one page.

In the following example, naming +pages.js instead of +config.js would be confusing.

/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

Alternatively, I also think +config is quite clear.

I think we can settle on +config then 👌.

I really like the +config.js name. I was actually quite unhappy with +export.js. I'm glad it's off the table now.

The + naming convention itself signals "this file is a VPS thing" as you mentioned, which I think is a good thing, but then with the way you're doing "exportables", now anyone can add a new +anything.js file. So then new users later are probably Googling +title.js or whatever and trying to figure out why it isn't documented anywhere. Not ideal in my opinion.

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 + naming for custom exports.

VPS is compatible with any JavaScript UI framework, but incompatible with every JavaScript meta-framework.

Exactly. (FYI Telefunc's Vite plugin needs to work with any UI Framework and with any meta framework.)

@brillout
Copy link
Member Author

brillout commented Dec 17, 2022

Yes. Also I'm thinking to allow this:

/pages/index.+page.vue
/pages/about.+page.tsx
/pages/product/@id.+page.tsx
/pages/product/@id.+onBeforeRender.tsx

I changed my mind. I don't think index.+page.vue is a good idea anymore.

A much cleaner approach is a new option singleConfigFile: true.

// /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:

  • With singleConfigFile: true, there is only a single +config.ts file (and no other "export files" such as +onBeforeRender.ts), i.e. +config.ts is the only + file in the entire user's repository.
  • With singleConfigFile: false (the default), there can be multiple +conifg.ts files and multiple "export files" such as +onBeforeRender.ts.

That's it.

I foresee singleConfigFile: true to be used for large-scale complex apps, while singleConfigFile: false is convenient to quickly get started like Next.js.

Alright, I think this is ripe for an RFC.

@brillout
Copy link
Member Author

Edit: renamed singleFileConvention to singleConfigFile.

@brillout brillout mentioned this issue Dec 27, 2022
@brillout
Copy link
Member Author

Closing in favor of #578.

@brillout brillout closed this as not planned Won't fix, can't repro, duplicate, stale Dec 27, 2022
@champ7champ
Copy link

Yes. Also I'm thinking to allow this:

/pages/index.+page.vue
/pages/about.+page.tsx
/pages/product/@id.+page.tsx
/pages/product/@id.+onBeforeRender.tsx

I changed my mind. I don't think index.+page.vue is a good idea anymore.

A much cleaner approach is a new option singleConfigFile: true.

// /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:

  • With singleConfigFile: true, there is only a single +config.ts file (and no other "export files" such as +onBeforeRender.ts), i.e. +config.ts is the only + file in the entire user's repository.
  • With singleConfigFile: false (the default), there can be multiple +conifg.ts files and multiple "export files" such as +onBeforeRender.ts.

That's it.

I foresee singleConfigFile: true to be used for large-scale complex apps, while singleConfigFile: false is convenient to quickly get started like Next.js.

Alright, I think this is ripe for an RFC.

could you show me how it works in the sandbox?

@brillout
Copy link
Member Author

@champ7champ There isn't an example for this yet.

@champ7champ
Copy link

@brillout Will it be possible to try it soon?

@brillout
Copy link
Member Author

Yes, soon, the overall V1 architecture is already implemented. It's now about implementing the long tail of details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement ✨ New feature or request
Projects
None yet
Development

No branches or pull requests

9 participants