question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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

See original GitHub issue

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.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:6
  • Comments:77 (66 by maintainers)

github_iconTop GitHub Comments

3reactions
brilloutcommented, 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.)

2reactions
AaronBeaudoincommented, Jul 29, 2022

Buckle up, this is a long one.

I think we’re definitely moving in the right direction here. 👍

The last part is my favorite. You’re kind of starting to move in the direction of my “perfect” framework. Some sort of single export that “automagically” sets the configuration for the page, but doesn’t prevent the user from going deeper if they want. All that’s really happening is sensible defaults are being set.

That really aligns with my personal philosophy about simplifying configuration: Zero-config doesn’t mean no-config, it means optional-config. It just means sensible defaults. The config is just for when you need to override those defaults.

So if you’ll bear with me, I’m going to go wild and just describe my “perfect” config scenario for VPS. With complete imaginary freedom, for me it looks something like this:

// ————————————————————————————————————————————————————————————
// [name].page.js
// ————————————————————————————————————————————————————————————


// A single `rendering` export holds all of the VPS config for this
// page. It is totally optional. Export it from a `_rendering.page.js`
// file in the same directory instead if you want to keep it totally
// separate from the page, and that file will be used as the defaults
// for all pages in the directory. Or, pass it to the VPS plugin in
// `vite.config.js` and never create a `_rendering.page.js` file.
// No more `_default.page.server.js` and `_default.page.client.js`.
// (This means we could create VPS "presets" that set it up to work
// with frameworks without adding a single extra file to the project.)
export const rendering = {


  // If you set this to `server-and-client` (SSR), then `Page` is
  // available in both the `renderServer` and `renderClient` functions,
  // if `client-only` (SPA), just the `renderClient` function, and
  // if `server-only` (HTML), just the `renderServer` function.
  mode: "server-and-client" || "client-only" || "server-only",


  // Same as old `doNotPrerender`.
  // Don't panic, the old `prerender` is coming further down.
  prerender: false,


  // This is a list of exports from pages that will be made available
  // at `context.exports`. Each export will be made available in the
  // environment specified by the key value.
  exports: {
    title: "server-and-client",

    // You'll see what this is for further down.
    route: "server-only"
  },


  // This is a new optional function that helps to make the two functions below
  // a bit more "single concern". The return value will overwrite the `Page`
  // property in `renderServer` and `renderClient` below, so it's "real-world"
  // purpose is to perform the wrapping of your page with a layout.
  transformPage(context) {

    // For Vue.
    return {
      render() {
        return h(Layout, {}, {
          default: () => h(context.Page, context.props || {})
        });
      }
    };
  
    // For React.
    // (From what I can tell in the examples, I don't use React.)
    return (
      <Layout context={context}>
        <Page {...context.props} />
      </Layout>
    );
  },


  // This is equivalent to the old VPS `render` export from `[name].page.server.js`.
  renderServer(context) {

    // For Vue.
    const page = createSSRApp(context.Page);
    const pageStream = await renderToNodeStream(page);

    // Return value is "dedented", wrapped in `escapeInject`,
    // and minified all automatically by VPS.
    return `
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1">

          <!-- Get title from 'export const title = "...";' in our pages.-->
          <title>${context.exports.title}</title>
        </head>
        <body>
          <div id="page">${pageStream}</div>
        </body>
      </html>
    `;
  },


  // Same as old `passToClient`.
  passContext: [ ... ],


  // This is equivalent to the old VPS `render` export from `[name].page.client.js`.
  renderClient(context) {
    const page = createSSRApp(context.Page);
    page.mount("#page");
  },


  // This object contains all of the routing specific configuration.
  routing: {

    // Route as a string.
    route: "/film/@id",

    // Or route as a function.
    route: context => { ... },

    // Or route using a `export const route = "...";` in our pages.
    // This is what `exports.route` up above was for.
    route: context => context.exports.route,

    // Same as old `filesystemRoutingRoot`.
    root: "/",

    // Same as old `clientRouting`.
    client: true,

    // Same as old `prefetchStaticAssets`.
    prefetch: "on-hover" || "on-visible" || false
  },


  // Any VPS function that would start with `on` is a hook.
  // Any future hooks would just be added in here.
  hooks: {
    onBeforeServerRender(context) { ... },
    onBeforeRoute(context) { ... },
    onClientPageTransitionStart() { ... },
    onClientPageTransitionEnd() { ... },

    // Same as old `prerender()`.
    onPrerender() { ... }
  },


  // If a user wants to change the name of the export that VPS will look
  // for to get the config or the page component, they can do that with
  // these config properties. They might do this if they or their framework
  // already uses the `Page` or `rendering` export for something else.
  // All potential export naming conflicts are now solvable!
  configExport: "rendering",
  pageExport: "Page"
};


// The `rendering` export and this `Page` export are the ONLY exports
// from the page that VPS will consider as "belonging to the framework".
// All old VPS exports like `render`, `prerender`, `doNotPrerender`,
// `onBeforeRender` `passToClient` and whatever else are now properties
// inside of the `rendering` export above.
export { Page };


// Exports other than `rendering` or `Page` are ignored, unless they
// are listed under `rendering.exports`, in which case they will be
// available in the environment specified there.
export const title = "My Page Title";

Context Object

{
  env: "server" || "client" || "hydration",
  Page: FrameworkComponent,
  exports: {},

  route: {
    // Same as old `routeParams`.
    params: ...,

    // Room for potential future stuff...
  },

  url: {
    // Same as old `url`.
    original: ...,

    // Same as old `urlPathname`.
    path: ...,

    // Same as old `urlParsed.search`.
    params: ...,

    // Other stuff in here I don't want to detail, but you get the idea.
  },

  // Same purpose as old `is404`.
  error: null || {
    status: 404 || 500,

    // Arbitrary data user can pass to error when
    // doing `throw RenderErrorPage(...)`? Just an idea.
    data: {}
  }
}

Some Discussion Points

  • Why didn’t you use SSR, SPA and HTML for the modes?
    Those terms are commonly used to describe general approaches to rendering a web application, and there also seems to be no standard at all for them. Some people use the term CSR instead of SPA. Technically all pages use HTML. At first impression, “server-side rendering” sounds like exactly what PHP or any other regular server with a templating engine does. But here in VPS, we’re always trying to render a page using a JS framework that can render on both the client and the server, so the key question is: Do we want to render it on the server, the client, or both with hydration? I vote we throw the arcane acronyms out the window entirely (except in the docs, for clarity, of course).
    I’ve changed my mind on this point. I’m probably going to start a separate discussion where we can go deeper into my thoughts on this particular issue. I think those acronyms have their place, but they’re often used to mean the wrong thing. Here are some articles I’ve been reading that I want to use as a basis to hash out a “standard” for VPS to potentially use.
    https://docs.astro.build/en/concepts/mpa-vs-spa/
    https://dev.to/snickdx/understanding-rendering-in-web-apps-spa-vs-mpa-49ef

  • Why no @server or @server-only or whatever comments?
    I wasn’t in love with this style to begin with. Relying on comments to do actual logic just seems like a bad idea from the start, in my humble opinion. Using the config above, they’re no longer necessary.

  • What about import statements?
    Instead of using comments to control the target environment for import statements, I propose a more “Vite-ish” style by using query parameters in the URL string. So like import "/styles/index.css?env=[server|client]"; to scope the import. Without the parameter it is by default in the client for the server-and-client and client-only modes, and by default in the server for server-only mode. I don’t know if that would work, though? What are your thoughts?

  • Didn’t you basically just describe a whole new plugin?
    Kind of. I did say I was going to go wild. 😅

  • Why such an extreme change?
    VPS’s way of looking at page exports is super cool, but it clutters the page with a bunch of exports and has the adverse effect of unescapable potential naming conflicts and verbose export names. Putting the entire config under a single export is so much cleaner IMO. Also it ensure VPS won’t conflict with any future framework… I think?

  • Why did you rename everything??
    I just felt like most of the names are more verbose than necessary. Don’t hate me! I still think the current VPS is absolutely amazing and I totally love it.

Read more comments on GitHub >

github_iconTop Results From Across the Web

One JS File for Multiple Pages [closed] - Stack Overflow
The answer for this is surely 'depends on the situation' (we know that). My question is, assuming all my JS logic is placed...
Read more >
Use JavaScript within a webpage - Learn web development
Take your webpages to the next level by harnessing JavaScript. Learn in this article how to trigger JavaScript right from your HTML documents....
Read more >
How to package everything into single HTML file? · Issue #1704
I have a fairly simple site, it is a single page React app. When I do a build of the site I end...
Read more >
Single-page application vs. multiple-page application - Medium
Its content is loaded by AJAX (Asynchronous JavaScript and XML) — a method of exchanging data and updating in the application without refreshing...
Read more >
Single-File Components - Vue.js
Vue Single-File Components (a.k.a. *.vue files, abbreviated as SFC) is a special file ... Single-Page Applications (SPA); Static Site Generation (SSG) ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found