ericclemmons / mdx-site

Static site generator powered by TypeScript, MDX, & React.
51 stars 3 forks source link

Client-side bundle #8

Open ericclemmons opened 5 years ago

ericclemmons commented 5 years ago

To support client-side rendering, I'm thinking:

  1. Generate ${folder}/index.html from public/index.html.
  2. Generate ${folder}/index.js from the index.mdx + index.tsx
  3. Run parcel against it to replace it.
ericclemmons commented 5 years ago

I tried hard to make https://parceljs.org/ work, but the reality is that server-rendering this is easy (done!), but Parcel makes it very difficult to do a server+client build.

diff --git a/package.json b/package.json
index b1eb7c7..bb5ab4a 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
     "@types/microrouter": "^3.1.0",
     "@types/node": "^12.0.4",
     "@types/ora": "^3.2.0",
+    "@types/parcel-bundler": "^1.12.0",
     "@types/webpack-env": "^1.13.9",
     "np": "^5.0.3"
   },
@@ -42,6 +43,7 @@
     "micro": "^9.3.4",
     "microrouter": "^3.1.3",
     "ora": "^3.4.0",
+    "parcel-bundler": "^1.12.3",
     "prism-react-renderer": "^0.1.6",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
diff --git a/src/app.tsx b/src/app.tsx
index cdf5b8f..b7c5c63 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,13 +1,15 @@
+// import fse from "fs-extra";
 import { ServerResponse } from "http";
 import { send } from "micro";
 import { router, get } from "microrouter";
+import Bundler from "parcel-bundler";
 import path from "path";
 // @ts-ignore
 import serve from "serve-handler";

-import { defaultContentDir } from "./utils/defaults";
+import { defaultContentDir, defaultOutputDir } from "./utils/defaults";
 import getPage from "./utils/getPage";
-import renderPage from "./utils/renderPage";
+// import renderPage from "./utils/renderPage";

 export default router(
   get("(:folder)", async req => {
@@ -19,16 +21,77 @@ export default router(
     }

     const { folder } = req.params;
-    const page = await getPage(
-      path.join(defaultContentDir, folder, "index.mdx")
-    );
+    const pagePath = path.join(defaultContentDir, folder, "index.mdx");
+    const page = await getPage(pagePath);

     // Ignore folders without markup
     if (!page) {
       return;
     }

-    return renderPage(page);
+    const bundler = new Bundler(pagePath, {
+      cache: false,
+      hmr: false,
+      outDir: defaultOutputDir,
+      watch: true
+    });
+
+    // ! This is to test i it's a valid plugin
+    // require("./parcel-plugin");
+    bundler.addAssetType("mdx", require.resolve("./parcel-plugin"));
+
+    // ! This is to ensure we serve the file, not the folder
+    await bundler.bundle();
+
+    // ! This is a fallback for testing Parcel's output
+    await bundler.serve();
+
+    return;
+
+    // return renderPage(page);
+
+    // const targetHTML = path.join(
+    //   defaultOutputDir,
+    //   "__src",
+    //   folder,
+    //   "index.html"
+    // );
+
+    // const targetJS = path.join(defaultOutputDir, "__src", folder, "page.js");
+    // const output = await renderPage(page);
+
+    // await fse.mkdirp(path.dirname(targetHTML));
+    // await fse.writeFile(targetHTML, output, "utf8");
+
+    // await fse.copyFile(
+    //   targetPage,
+    //   path.join(defaultOutputDir, "__src", folder, "index.mdx")
+    // );
+
+    // fse.writeFile(targetJS, `console.log("${folder}");`, "utf8");
+
+    // const bundler = new Bundler(targetHTML, {
+    //   outDir: defaultOutputDir
+    // });
+
+    // console.log(await bundler.bundle());
+
+    // bundler.addAssetType("mdx", require.resolve("./parcel-plugin"));
+    // // Included for HRM
+    // require("./parcel-plugin");
+
+    // return new Promise((resolve, reject) => {
+    //   // @ts-ignore
+    //   bundler.middleware()(req, res, (err: Error, data: any) => {
+    //     if (err) {
+    //       return reject(err);
+    //     }
+
+    //     console.log({ data });
+
+    //     resolve(data);
+    //   });
+    // });
   }),

   get("/*", async (req, res) => {
@@ -37,7 +100,7 @@ export default router(
       res,
       {
         directoryListing: false,
-        public: "public",
+        public: defaultOutputDir,
         renderSingle: true
       },
       {
diff --git a/src/bin/mdx-site.tsx b/src/bin/mdx-site.tsx
index 572ae29..65cfc81 100644
--- a/src/bin/mdx-site.tsx
+++ b/src/bin/mdx-site.tsx
@@ -63,7 +63,19 @@ switch (command) {
     require("../build");
     break;
   case "start":
-    require("../server");
+    const Bundler = require("parcel-bundler");
+
+    const bundler = new Bundler("content/**/*.mdx", {
+      cache: false,
+      hmr: true,
+      outDir: defaultOutputDir,
+      watch: true
+    });
+
+    bundler.addAssetType("mdx", require.resolve("../parcel-plugin"));
+    bundler.serve();
+
+    // require("../server");
     break;
   default:
     throw new Error(
diff --git a/src/parcel-plugin.js b/src/parcel-plugin.js
new file mode 100644
index 0000000..fcb3835
--- /dev/null
+++ b/src/parcel-plugin.js
@@ -0,0 +1,87 @@
+// @ts-check
+
+const mdx = require("@mdx-js/mdx");
+const fse = require("fs-extra");
+
+const { default: getPage } = require("./utils/getPage");
+const { default: renderPage } = require("./utils/renderPage");
+
+const Asset = require("parcel-bundler/src/Asset");
+const HTMLAsset = require("parcel-bundler/src/assets/HTMLAsset");
+const path = require("path");
+
+module.exports = class MDXAsset extends HTMLAsset {
+  constructor(name, options) {
+    super(name, options);
+
+    // console.log(this);
+
+    //   this.type = "html";
+    //   this.hmrPageReload = true;
+  }
+
+  async parse(code) {
+    const page = await getPage(this.name);
+    const html = await renderPage(page);
+
+    return super.parse(html);
+  }
+
+  async generate() {
+    const parts = await super.generate();
+
+    // console.log({ parts });
+
+    console.log(this);
+    return parts;
+  }
+
+  // @ts-ignore
+  // async generate() {
+  //   const { options, resolver, ...rest } = this;
+  //   console.log("COnstructed", rest);
+
+  //   this.parentBundle = this;
+
+  //   const page = await getPage(this.name);
+
+  //   // console.log(this);
+
+  //   // this.addURLDependency(this.name, this.name, {
+  //   //   includedInParent: true
+  //   // });
+
+  //   return super.generate(await renderPage(page));
+  // if (this.type === "html") {
+  // }
+
+  // const pageJS = path.relative(
+  //   process.cwd(),
+  //   path.join(this.options.outDir, "index.js")
+  // );
+
+  // await fse.writeFile(
+  //   pageJS,
+  //   `
+  //   console.log(${JSON.stringify(folder)})
+  //   `,
+  //   "utf8"
+  // );
+
+  // this.addDependency(this.name, {
+  //   includedInParent: true
+  // });
+
+  // await fse.writeFile(
+  //   path.join(folder, "page.js"),
+  //   `
+  //   /* @jsx mdx */
+  //   import React from "react";
+  //   import { mdx } from "@mdx-js/react"
+
+  //   ${compiled}
+  //   `,
+  //   "utf8"
+  // );
+  // }
+};
diff --git a/src/utils/renderPage.tsx b/src/utils/renderPage.tsx
index 1ff9abb..37b6816 100644
--- a/src/utils/renderPage.tsx
+++ b/src/utils/renderPage.tsx
@@ -20,8 +20,8 @@ interface Page {
   readonly raw: string;
 }

-export default async function renderPage(page: Page) {
-  const { attributes, body, props } = page;
+export default async function renderPage(page: Page, variables = {}) {
+  const { attributes = {}, body, props } = page;
   const { title } = attributes;
   const components = await getComponents(defaultComponentsDir);

@@ -49,7 +49,8 @@ export default async function renderPage(page: Page) {
   const html = await renderTemplate({
     title: title || defaultTitle,
     description,
-    markup
+    markup,
+    ...variables
   });

   if (process.env.NODE_ENV !== "production") {
diff --git a/template/public/index.html b/template/public/index.html
index 4abedc5..4775fe4 100644
--- a/template/public/index.html
+++ b/template/public/index.html
@@ -14,6 +14,7 @@
   %MARKUP%
 </body>

+<script src="./index.mdx" type="application/javascript"></script>
 <script src="https://unpkg.com/quicklink@1.0.0/dist/quicklink.umd.js"></script>

 </html>

Based on my research, I think the best possible solution would be similar to https://github.com/zeit/next.js/tree/canary/packages/next-mdx that would, instead:

Put another way, blog/[post].mdx would append the contents of blog/[post].ts.

ericclemmons commented 5 years ago

The bad news with this approach is that the intelligent usage of layouts & components would go away.

The good news is, this could possibly be an example in the Next.js repo, which could then be created via:

npx create-next-app --example mdx-site my-site

https://github.com/zeit/create-next-app#starting-from-nextjs-examples

ericclemmons commented 5 years ago

I did this as a Next.js app, but the JS payload was huge and creating <Snippet /> within a getInitialProps isn't JS-serializeable, so it doesn't work.

I think the next step would be to use webpack and bundle a page.js.