brillout / goldpage

Page Builder.
Creative Commons Zero v1.0 Universal
57 stars 3 forks source link

New major version [complete rewrite] #24

Open brillout opened 3 years ago

brillout commented 3 years ago

There are neat ideas out there waiting to be implemented such as:

brillout commented 3 years ago

@chriscalo Would you be up to join me in doing this? We could co-create a new GitHub Org

chriscalo commented 3 years ago

Apologies for the slow reply. I started to respond a few time times but wanted to give it some more thought and research.

I definitely am still having some struggles in my dev workflow and could be convinced to contribute, though it would need to be a good match for addressing the problems I’m facing.

Let me know if any of this sounds interesting. If this doesn’t turn out to be a good match, it’s nevertheless useful for me to get these thoughts down on paper.


My wish list:

Support modern Vue app development

Development: fast, no-bundle dev server

File-system based routing for static assets, dynamic pages, and API endpoints

The vision behind Zero Server feels perfect to me, though unfortunately I wasn’t able to get it to build:

Simple SSR

Production: optimized for serverless environments

With Nuxt I’m seeing response timeouts in Google App Engine after deployment or after the app is idle for a while. Not certain, but my guess is that when a new instance spins up to handle an incoming request, it runs npm install && npm run start, which times out. I failed at packaging all server-side run-time deps into a single bundle to see whether avoiding npm install fixes the issue.

Would be great to have:

JavaScript API for easy integration

In addition to a CLI command for starting a server, it’s also nice to have a JavaScript API when more control over the server is needed. Two examples:


Thoughts? Any of this a good match for what you’re hoping to create?

brillout commented 3 years ago

My overarching motivation is that I want a lean do-one-thing-do-it-well tool, instead of having to use a big rigid monolith à la Next.js or Nuxt.

I believe it's possible to implement a lightweight core that is highly flexible:

Core would implement:

Core can be used directly by the user or can be used with an "integrator". The role of the integrator is to stich core with a bundler/builder and a view library. There would be several integrators, for a example a Vue + Snowpack integrator, a Vue + Vite integrator, a React + React Server Components + Webpack integrator, etc.

The goal of the flexibility is actually that anyone with a wish list like yours can implement it. Core should not get in the way of your wish list.

So, when you ask whether your wish list matches my motivation, my answer is a clear yes since I want anyone, including you, to build whatever they want on top of core.

only load modules as needed for individual requests

Neat idea.

it’s also nice to have a JavaScript API

I agree, it's important for flexibility.

I'm increasingly liking Snowpack. And yes, it would be pretty neat to get rid of the build-step altogether. It's exciting to see Snowpack and Svelte starting using esbuild. It would be exciting to work on a Snowpack integrator together.

I'd suggest we take a shot at implementing a first rough prototype of core together where each LOC is reviewed by the other one.

What do you think?

How about a new name? RenderX? Other ideas for names?

brillout commented 3 years ago

@chriscalo Note that Vite is working on supporting SSR. This means that writing Goldpage/RenderX core + Vue/Vite integrator will be only a couple of kLOCs! This is an exciting opportunity to ship an interface we both deem user-friendly while not having to write much code. It's quite a comfy situation :-).

chriscalo commented 3 years ago

Well, that’s quite encouraging ☺️

I’d suggest we take a shot at implementing a first rough prototype of core together where each LOC is reviewed by the other one.

This seems reasonable, though even before that, how would you feel about starting with a few example projects that don’t actually work (yet) but instead attempt to illustrate how projects should work with Goldpage/RenderX? Essentially, this would help us capture the right developer experience before starting, but then those same example projects could later serve as a target to code against and even as automated tests. Thoughts?

I’m happy to give this an attempt, but I should be honest about my modest experience: I’ve only worked on fairly simple apps and packages/modules, so I would likely need a good amount of architectural guidance.


I want to call out this discussion of “buildless” SSR in the Vite repo: https://github.com/vitejs/vite/issues/1122

Essentially we could use require.extensions to convert .vue files to es modules, add a .ts extension and then handle them with ts-node... giving us the ability to use Vue components without the hassle of a build.

This is accomplished because the FIRST time any .ts file is run, it is transpiled and that transpiled version is cached via require.cache....

I believe that we could do the same with vue files. Transpile them and cache that result.

It seems pretty compelling to be able to just deploy source files and not have to worry about any build configuration. But if you want to you can do a manual build to “prime the cache.”

In the end I think buildless is compelling to the community because of a general fatigue of tooling and configuration, but it’s possible things are not quite there yet.

At the moment, all that really matters is that things Just Work without fussing with configuration or wiring things together. Maybe it’s too early for buildless, but I’m hopeful web tooling will eventually get there.

brillout commented 3 years ago

Well, that’s quite encouraging ☺️

Yep. And, even if we fail to popularize it, we will have built something beautiful we can be proud of :-).

starting with a few example projects that don’t actually work (yet) but instead attempt to illustrate how projects should work with Goldpage/RenderX

Yes, that's actually my thinking as well, I just forgot to mention it.

I’m happy to give this an attempt, but I should be honest about my modest experience: I’ve only worked on fairly simple apps and packages/modules, so I would likely need a good amount of architectural guidance.

With automated tests with good coverage, we'll be able to easily change the internal architecture of Goldpage/RenderX. TypeScript + automated tests make refactoring astonishingly easy.

We both care about exposing a minimal interface to the user which means good coverage will be easy to achieve. (We don't have to test all languages PostCSS/SASS/TypeScrip/JSX/SFC/etc. since these are already covered by Vite; we merely need to test our interface.)

So basically my answer here is that we can first implement things in a quick way and eventually make internal improvements.

buildless

Interesting idea - it would make building completely transparent. Neat.

At the moment, all that really matters is that things Just Work without fussing with configuration or wiring things together.

Yes same here. To me, what's important is that the user has to think only what's strictly necessary when using Goldpage/RenderX. In itself, that the user has to run a build step before deploying, although less aesthetically pleasing than buildless, is ok for now.

One reason why I value the core+integrator desgin: it will allow us to experiment and implement all kinds of neat things in the future while keeping the same DX. We can progressively implement these niceties with minimal breaking change for the user.

Let's think about the DX we want, and an exciting future will be ahead of us :). I'll be thinking about it this Weekend.

brillout commented 3 years ago

Started to think about the interface, but I'm not done.

chriscalo commented 3 years ago

Sounds like some interesting developments in Vite SSR (as you mentioned above).

https://twitter.com/youyuxi/status/1351225446902464516?s=21

The only thing I’m a little worried about is how long it will take to become stable enough for use in production. Although Vite is at 2.x, things don’t Just Work yet.

brillout commented 3 years ago

It seems more and more clear that SSR with good DX without framework is only a matter of time.

What I forsee is that Vite will be a low-level SSR solution. Goldpage/RenderX would be a higher-level solution (without being a framework).

But if Vite covers most DX/features we want, then it could replace what we are doing here.

I'll think more about the interface later today. If Vite ends up replacing Goldpage/RenderX then we would have contributed to Vite's SSR interface :). (We should open a ticket about our intentions on the Vite repo at some point.)

Thanks for the tweet pointer, it's great to see Evan caring and pushing the envelope.

brillout commented 3 years ago

The Interface

Basic usage:

That's all the user would have to do: the basic usage has zero config.

But config is possible for advanced "full control" usage.

Furthermore, we can use the render function idea to enable the user full control over app integration. This will enable all kinds of advanced use cases.

// Full control over server integration

const { renderx } = require("goldpage");
const express = require("express");
const app = express();

app.render("/", async (req, res) => {
  const renderedHtml = await renderx("landing.page.vue");
  res.send(`<html><body>${renderedHtml}</div></html>`);
});
// Full control over SSG

const { renderx, saveToHtmlFile } = require("goldpage");
const Todo = require("./path/to/orm-model/Todo");

generateStaticPages();

async function generateStaticPages() {
  const items = await Todo.getAll();
  await Promise.all(
    items.map(async ({ id, text, isCompleted }) => {
      const renderedHtml = await renderx('todo/item.page.vue', { initialProps: { text, isCompleted } });
      await saveToHtmlFile(`<html><body>renderedHtml</body></html>', { route: `/item/${id}` });
    })
  )
}

This level of control is unseen before. This would be a wonderful do-one-thing-do-it-well tool for companies that need/want control over their app. (I've seen many companies relunctant to use frameworks such as Nuxt/Next.js.)

Thanks to Vite, we can keep the whole source code small.

Basic usage is and should be zero config.

brillout commented 3 years ago

@chriscalo Let me know if there is anything you don't like about the interface. I'd be curious to know what you think about it.

Vite SSR has landed https://github.com/vitejs/vite/issues/1290#issuecomment-765010883.

I'll start playing with a rough implementation.

chriscalo commented 3 years ago

I hadn't seen, that's a big deal. 👍 super curious to hear what it's like coding against the Vite SSR implementation.

I'll send a more detailed response this weekend, but an initial reaction is:

regarding the proposed interface: the way my brain works, I feel like I need to think through things in this order, so it's a little difficult to talk through the interface before going through the use cases, though they're admittedly similar:

  1. examples / use cases / test cases
  2. interface
  3. implementation

Where's the best place to start capturing these use cases? Should I create a one-off repo to gather my thoughts?

chriscalo commented 3 years ago

Also wanted to point out webpack's new lazyCompilation experiment: another step towards going fully buildless in development.

brillout commented 3 years ago

super curious to hear what it's like coding against the Vite SSR

Super curious as well 👍. I'll have a deeper look at it this Weekend.

what I love about Vue is that it feels native to the web platform

👍 I also like it when things are close to "bare metal".

I'm curious whether .html files can be the entry points.

Interesting idea. Although I'm thinking it can be tricky to share common HTML between pages. For example, pages usually have the same footer and header; how would you then share this same header and footer between all pages?

I need to think through things in this order,

Makes sense. One thing we could as well do is to ask ourselves what use cases would the interface not cover? It may be an efficient way to get intimate with the interface and develop use cases. Similar to writing tests that aim to bring code to fail. I find myself learning a lot when I write tests. Instead of tests, it would be uses case that try to fail the interface.

Where's the best place to start capturing these use cases? Should I create a one-off repo to gather my thoughts?

I guess we can write everything in this ticket for now.

Also wanted to point out webpack's new (lazyCompilation](https://github.com/webpack/webpack/releases/tag/v5.17.0) experiment: another step towards going fully buildless in development.

Neat. They only need a zero-config thing, then Webpack is back in the game :).

brillout commented 3 years ago

@chriscalo https://github.com/brillout/vite-plugin-ssr :)

brillout commented 3 years ago

@chriscalo I published a beta. I'd be curious to know what you think! :-)

chriscalo commented 3 years ago

Fast progress!

I've been hoping to try this but haven't found some free time just yet. I have a few ideas that might simplify the developer experience, but will collect my thoughts and share.

Also check out recent developments in Nuxt 3: https://nuxtjs.slides.com/atinux/nuxt-3-in-action

brillout commented 3 years ago

I have a few ideas that might simplify the developer experience

Curious to ear about them!

but will collect my thoughts and share.

Feel free to share them raw and unpolished :)

Also check out recent developments in Nuxt 3: https://nuxtjs.slides.com/atinux/nuxt-3-in-action

Thanks for these pointers, I was actually looking for these slides.

chriscalo commented 3 years ago

A few raw thoughts here, but a big caveat is that I haven't tried things out yet:


The .page.js / .page.vue file-naming convention feels quite nice to me 👍 it's a little verbose, causing slightly longer file names, but it's nicely explicit, which makes it really easy to put index.page.vue right next to MyHomePageTableComponent.vue and not have to do anything special to prevent the latter from being served as a page (which is what happens in Nuxt).

For example, here's the .nuxtignore file I have to use in a Nuxt project so I can keep both page components and sub-page components next to each other in the pages/ directory:

# ignore .js and .comp.vue files in the pages directory
pages/**/*.js
pages/**/*.comp.vue

Of course, I put shared components in the components/ directory following Nuxt conventions, but it's also really common to break functionality out of a page component into its own separate component file not for re-use, but instead purely for organizational purposes when the page component file gets too big or complex.

I expect some people to not love that naming convention, so I wonder if it would be good to offer an option to specify the glob pattern for pages (but also YAGNI).


I'd personally prefer not to use .route.js files at all and instead just use file naming conventions à la Nuxt 3:

pages/[slug].vue
pages/blog/[...all].vue

And you aren't required to write your own render() and hydrate() functions, right? Will things still work if you don't?


In a Nuxt project I ended up embracing the pattern of creating an API and fetching data from within Vue page (and sub-page) components in both Node (via SSR) and the browser. It uses Vue Apollo, and I have no idea how it works, but love that it seems to just work.

<script>
  import { Goals } from "./goals.graphql";

  export default {
    apollo: {
      goals: {
        query: Goals,
        prefetch: true,
      },
    },
  };
</script>

<template>
  <table>
    <thead>
      <tr>
        <td>Description</td>
        <td>Target Date</td>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(goal, i) in goals">
      <td>{{ goal.description }}</td>
      <td>{{ goal.targetdate }}</td>
    </tr>
    </tbody>
  </table>
</template>

It does require the following nuxt.config.js to tell Vue Apollo what the API endpoint is:

const { HOST, PORT } = process.env;

export default {
  modules: [
    "@nuxtjs/apollo",
  ],
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: `http://${HOST}:${PORT}/graphql`,
        browserHttpEndpoint: `/graphql`,
      },
    },
  },
};

I want to try some similar things with Vite SSR and see if I'm able to get a similar setup working without too much fuss.

In particular, I wonder if the .page.server.js and .page.client.js files make it simple to cleanly segregate server and client logic 🤔.


Thinking aloud, I feel like the fundamental issue is being able to create a "soft" dependency from a .page.vue file that needs to run in Node and in the browser to some Node-only logic for accessing a database. I say soft because you don't want to import and therefore bundle the server logic into your client-side code, but you want some really simple way of calling it.

I kinda wish a very similar pattern like .page.vue files could be used for API endpoints, too. Here's what I'm imagining:

import query from "~/util/db.js";

export default async (req, res) => {
  res.json(await query(`select * from Foo`));
}

Then, in a index.page.vue file that's in the same directory as foo.api.js above, calling that endpoint whether from Node or the browser is perhaps as simple as:

<script>
  import http from "~/util/http"; // (axios instance)

  export default {
    async data() {
      return {
        // In the browser, the use of a relative path below just works.
        // I wonder what it would take to make the same work on the server.
        foo: await http.get("./foo"),
      };
    },
  };
</script>

<template>
  …
</template>

Here's a somewhat related tweet on making file-based API endpoints, though I don't love the (event, context) signature and would prefer to stick to the Node.js convention of the connect (req, res, next) signature.


I'm also wondering if there's an easy way to set <head> content from within a .page.vue file. There's a really nice pattern in @egoist/vue-head that sadly appears to have been abandoned. That's a shame because it's the only solution to this problem I've seen that resembled plain HTML (all other approaches ask you to write JS/JSON objects for setting <head> HTML, which feels like a step backwards).

<template>
  <Head>
    <title>Hello Vue</title>
    <meta name="description" content="Do you like it?" />
  </Head>
</template>

<script>
import { Head } from '@egoist/vue-head'

export default {
  components: {
    Head,
  },
}
</script>

Another big question: Vite seems really intent on providing only a dev server and not a production server, so I'm curious how you run this in production? I'd really love to not have to write my own server, to be honest. This is the PHP file-system-routing experience: I want to author page and API files, not write serving logic.

brillout commented 3 years ago

Hi Chris,

I like your thoughts, as usual :blush:.

I wonder if it would be good to offer an option to specify the glob pattern for pages

I share the sentiment but I also greatly care about minimal configuration and simplicty. Although the user has slightly more effort and has to type these .page.* suffixes, he'll appreciate a simple tool that has minimal configuration.

I expect some people to not love that naming convention

They'll eventually come to realize that "this is the way".

I'd personally prefer not to use .route.js files

I did think about supporting pages/[slug].vue and pages/blog/[...all].vue but I came to the conclusion not to.

Route srings are more powerful and such path trickery ultimately leads to a loss of dev time when the user ends up thinking "I wonder if I can cram that route string as a FS path?", "Will my FS fail with these non-ASCII characters?", "Will my deploy environemnt fail with that FS path?". Even if know the answers to these question, the user will likely not. All these questions vanish by forcing the user to use proper route strings defined in .page.route.js.

Clarity is more important than having a few extra files.

And you aren't required to write your own render() and hydrate() functions, right?

You actually are required to... and you have to write _default.page.client.js and _default.page.server.js. The idea of vite-plugin-ssr is to give you control over how your pages are rendered. You may loose some time setting up vite-plugin-ssr at first, but you'll eventually win a lot of time when trying to integrate tools. For example, good luck integrating a store tool (that is not Vuex) with Nuxt...

However, I am thinking of writing a framework that is basically a tiny ejectable wrapper on top of vite-plugin-ssr. A la CRA but on top of Vite instead.

It uses Vue Apollo [...] it seems to just work.

Neat, that is indeed convenient. I'll have a look at it.

without too much fuss.

With vite-plugin-ssr you implement all integrations. But the aforementioned framework would take care of integrating the most common tools.

In particular, I wonder if the .page.server.js and .page.client.js files make it simple to cleanly segregate server and client logic 🤔.

Yes exactly: .page.server.js always runs in Node.js while .page.client.js always runs in the browser.

I kinda wish a very similar pattern like .page.vue files could be used for API endpoints, too. Here's what I'm imagining:

Check out https://github.com/brillout/wildcard-api (Which I'm btw going to rename it to Telefunc, "Wildcard API" is a misnomer.)

Using vite-plugin-ssr (or even better the aforementioned framework) with Telefunc will provide a superb DX, similar to what you're describing.

I'm also wondering if there's an easy way to set content from within a .page.vue file

Yes, check out https://github.com/brillout/vite-plugin-ssr#html-head

Vite seems really intent on providing only a dev server and not a production server, so I'm curious how you run this in production?

You integrate vite-plugin-ssr with your server manually: https://github.com/brillout/vite-plugin-ssr/blob/master/create-vite-plugin-ssr/template-vue/server/index.js

I'd really love to not have to write my own server, to be honest. This is the PHP file-system-routing experience: I want to author page and API files, not write serving logic.

I share the sentiment but that's not vite-plugin-ssr's goal. That said, the aforementioned framework would give you what you want: a zero-config SSR framework that is progressively ejectable all the way down to do-one-thing-do-it-well libraries.

Now imagine this with Telefunc. The long term goal of Telefunc is to completely replace Express.js: define your server as functions, that's it. (I'm currently working on file uploads and I've already a design for real-time, which would make Telefunc pretty much feature-complete.)