Skip to content

Commit

Permalink
feat: Upgrades @buttery/docs to React Router v7 (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
drewdecarme authored Dec 3, 2024
1 parent b251e7d commit 0386bd8
Show file tree
Hide file tree
Showing 76 changed files with 701 additions and 5,281 deletions.
15 changes: 15 additions & 0 deletions .monoweave/1df5ee59.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@buttery/builtins": patch
"@buttery/cli": patch
"@buttery/commands": patch
"@buttery/components": patch
"@buttery/core": patch
"@buttery/docs": patch
"@buttery/icons": patch
"@buttery/logs": patch
"@buttery/meta": patch
"@buttery/tokens": patch
"@buttery/tsconfig": patch
---

Upgrades `@buttery/docs` to use ReactRouter v7. This changeset is important in order to support the ongoing major releases of React Router and eventually React 19.
Binary file modified .yarn/install-state.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/buttery-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@vitejs/plugin-react-swc": "3.7.2",
"@wyw-in-js/vite": "0.5.5",
"typescript": "5.7.2",
"vite": "6.0.1"
"vite": "6.0.2"
},
"dependencies": {
"@buttery/logs": "workspace:^0.1.5",
Expand Down
19 changes: 0 additions & 19 deletions packages/buttery-docs/app/entry.app.tsx

This file was deleted.

6 changes: 2 additions & 4 deletions packages/buttery-docs/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { ButteryDocsClient } from "@buttery/docs/client";
import ReactDOMClient from "react-dom/client";
import { ButteryDocsApp } from "./entry.app";
import { routes } from "./routes";

ReactDOMClient.hydrateRoot(
document.getElementById("root") as HTMLElement,
<ButteryDocsClient>
<ButteryDocsApp />
</ButteryDocsClient>
<ButteryDocsClient routes={routes} />
);
45 changes: 3 additions & 42 deletions packages/buttery-docs/app/entry.server.cloudflare-pages.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,6 @@
// add the beginning of your app entry
import "vite/modulepreload-polyfill";
import { createRenderCloudflarePages } from "@buttery/docs/server.cloudflare-pages";
import { routes } from "./routes";

import {
ButteryDocsServer,
type HandleRequestCloudflarePagesRenderFunction,
} from "@buttery/docs/server";
import type { ReactDOMServerReadableStream } from "react-dom/server";
import { renderToReadableStream } from "react-dom/server";
import { ButteryDocsApp } from "./entry.app";

const ABORT_DELAY = 5000;

export const render: HandleRequestCloudflarePagesRenderFunction = async (
context,
responseStatusCode
) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY);

// Render the app to a ReadableStream using React's server renderer
const stream = (await renderToReadableStream(
<ButteryDocsServer {...context}>
<ButteryDocsApp />
</ButteryDocsServer>,
{
signal: controller.signal,
onError(error: unknown) {
if (!controller.signal.aborted) {
// Log streaming rendering errors from inside the shell
console.error(error);
}
responseStatusCode = 500;
},
}
)) as ReactDOMServerReadableStream;

stream.allReady.then(() => clearTimeout(timeoutId));

// regardless of streaming, we're going to wait on everything since it's just
// a documentation site. We don't need to over engineer this... yanno?
await stream.allReady;

return stream;
};
export const render = createRenderCloudflarePages(routes);
31 changes: 3 additions & 28 deletions packages/buttery-docs/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,6 @@
// add the beginning of your app entry
import "vite/modulepreload-polyfill";
import { createRenderDev } from "@buttery/docs/server.dev";
import { routes } from "./routes";

import type { ButteryMeta } from "@buttery/meta";
import { ButteryMetaProvider } from "@buttery/meta/react";
import { StrictMode } from "react";
import {
type RenderToPipeableStreamOptions,
renderToPipeableStream,
} from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import { ButteryDocsApp } from "./entry.app";

export async function render(
url: string,
ButteryMeta: ButteryMeta,
_ssrManifest?: string,
options?: RenderToPipeableStreamOptions
) {
// Render the app to a ReadableStream using React's server renderer
return renderToPipeableStream(
<StrictMode>
<ButteryMetaProvider ButteryMeta={ButteryMeta}>
<StaticRouter location={url}>
<ButteryDocsApp />
</StaticRouter>
</ButteryMetaProvider>
</StrictMode>,
options
);
}
export const render = createRenderDev(routes);
6 changes: 3 additions & 3 deletions packages/buttery-docs/app/functions/_middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ButteryDocsRouteManifest } from "@buttery/core/config";
import {
type CFContext,
handleRequestCloudflarePages,
} from "@buttery/docs/server";
} from "@buttery/docs/server.cloudflare-pages";
import type { Manifest } from "vite";
// @ts-ignore This will only exist when the app is built
import butteryManifest from "../build/client/.buttery/buttery.manifest.json";
Expand All @@ -11,9 +11,9 @@ import viteManifest from "../build/client/.vite/manifest.json";
// @ts-ignore This will only exist when the app is built
import { render } from "../build/server/server.js";

export async function onRequest(context: CFContext) {
export async function onRequest(cfContext: CFContext) {
const response = await handleRequestCloudflarePages(render, {
context,
cfContext,
bManifest: butteryManifest as ButteryDocsRouteManifest,
vManifest: viteManifest as Manifest,
});
Expand Down
11 changes: 11 additions & 0 deletions packages/buttery-docs/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "@buttery/docs/css";
import { header } from "virtual:data";
import { routeDocs, routeGraph, routeIndex } from "virtual:routes";
import { createButteryDocsRoutes } from "@buttery/docs/app";

export const routes = createButteryDocsRoutes({
header,
routeGraph,
routeDocs,
routeIndex,
});
17 changes: 9 additions & 8 deletions packages/buttery-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,17 @@
"homepage": "https://github.com/drewdecarme/buttery-tools#readme",
"exports": {
"./cli/*": "./dist/cli-scripts/*.js",
"./server": "./dist/server/index.js",
"./client": "./dist/client/index.js",
"./app": "./dist/app/index.js",
"./components": "./dist/components/index.js",
"./css": "./dist/docs.css"
"./server": "./dist/lib/server/index.js",
"./server.dev": "./dist/lib/server.dev/index.js",
"./server.cloudflare-pages": "./dist/lib/server.cloudflare-pages/index.js",
"./client": "./dist/lib/client/index.js",
"./app": "./dist/lib/app/index.js",
"./css": "./dist/lib/docs.css"
},
"devDependencies": {
"@buttery/icons": "workspace:*",
"@buttery/tokens": "workspace:*",
"@cloudflare/workers-types": "4.20241127.0",
"@cloudflare/workers-types": "4.20241202.0",
"@storybook/react": "8.4.6",
"@types/babel__core": "7.20.5",
"@types/express": "5.0.0",
Expand All @@ -69,7 +70,7 @@
"typescript": "5.7.2",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vite": "6.0.1"
"vite": "6.0.2"
},
"dependencies": {
"@babel/core": "7.26.0",
Expand All @@ -94,7 +95,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet-async": "2.0.5",
"react-router-dom": "7.0.1",
"react-router": "^7.0.2",
"rehype-autolink-headings": "7.1.0",
"rehype-slug": "6.0.0",
"remark-frontmatter": "5.0.0",
Expand Down
11 changes: 9 additions & 2 deletions packages/buttery-docs/scripts/lib.build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ async function buildLibrary() {
"Building the @buttery/docs library for consumption in the SSR app..."
);

const entry = ["client", "server", "app"].reduce((accum, entryName) => {
const entry = [
"client",
"app",
"server",
"server.dev",
"server.cloudflare-pages",
].reduce((accum, entryName) => {
const entryPath = path.resolve(
import.meta.dirname,
`../src/lib/${entryName}/index.ts`
Expand Down Expand Up @@ -54,10 +60,11 @@ async function buildLibrary() {
"react-dom/server",
"virtual:data",
"virtual:routes",
"node:stream",
/node_modules/,
],
output: {
dir: path.resolve(import.meta.dirname, "../dist"),
dir: path.resolve(import.meta.dirname, "../dist/lib"),
// preserveModules: true,
},
},
Expand Down
3 changes: 3 additions & 0 deletions packages/buttery-docs/src/cli-scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "../options";
import { LOG } from "../utils";

process.env.NODE_ENV = "production";

export async function build(options?: ButteryDocsBuildOptions) {
const parsedOptions = parseAndValidateOptions(
butteryDocsBuildOptionsSchema,
Expand Down Expand Up @@ -48,6 +50,7 @@ export async function build(options?: ButteryDocsBuildOptions) {
},
});
LOG.debug("Building client bundle for production... done");
console.log({ appEntryServer: dirs.app.appEntryServer });

LOG.debug("Building server bundle for production...");
await viteBuild({
Expand Down
109 changes: 12 additions & 97 deletions packages/buttery-docs/src/cli-scripts/dev.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Transform } from "node:stream";
import { parseAndValidateOptions } from "@buttery/core/utils/node";
import { ButteryMeta } from "@buttery/meta";
import express from "express";
import open from "open";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { createServer } from "vite";
import { getButteryDocsConfig } from "../getButteryDocsConfig";
import { getButteryDocsDirectories } from "../getButteryDocsDirectories";
import { getButteryDocsViteConfig } from "../getButteryDocsViteConfig";
import { generateHTMLTemplate } from "../lib/server/generateHTMLTemplate";
import { handleRequestDev } from "../lib/server.dev";
import {
type ButteryDocsDevOptions,
butteryDocsDevOptionsSchema,
Expand Down Expand Up @@ -40,7 +37,6 @@ export async function dev(options?: Partial<ButteryDocsDevOptions>) {
const viteConfig = getButteryDocsViteConfig(config, dirs);

// Set some constants
const ABORT_DELAY = 10_000;
const PORT = parsedOptions.port;
const HOSTNAME = `http://${parsedOptions.host}`;
const HOSTNAME_AND_PORT = `${HOSTNAME}:${PORT}`;
Expand Down Expand Up @@ -71,98 +67,17 @@ export async function dev(options?: Partial<ButteryDocsDevOptions>) {
// will be hydrated and any subsequent applications will be managed
// by the react router
app.use("*", async (req, res) => {
// instantiate a new instances of Meta
// which will track any meta tags or json used in the
// doc files for SEO
LOG.debug(
"Instantiating ButteryMeta to SSR meta, title, description and og tags"
);
const Meta = new ButteryMeta();

try {
const url = req.originalUrl;

// Load the server-entry file as a module
LOG.debug(`Loading the server entry file "${dirs.app.appEntryServer}"`);
const ssrEntryModule = await vite.ssrLoadModule(dirs.app.appEntryServer);

// create the HTML template
LOG.debug("Generating HTML template...");
const { htmlDev } = generateHTMLTemplate({
cssLinks: [dirs.app.css.tokens, dirs.app.css.docsUI],
jsScripts: [dirs.app.appEntryClient],
Meta,
});
LOG.debug(htmlDev);
LOG.debug("Generating HTML template... done.");

// allow vite to inject the necessary scripts
LOG.debug("Injecting scripts into HTML base");
const htmlTemplate = await vite.transformIndexHtml(url, htmlDev);

const ssrManifest = undefined;
let didError = false;

const { pipe, abort } = await ssrEntryModule.render(
url,
Meta,
ssrManifest,
{
onShellError() {
res.status(500);
res.set({ "Content-Type": "text/html" });
res.send("<h1>Something went wrong</h1>");
},
onAllReady() {
res.status(didError ? 500 : 200);
res.set({ "Content-Type": "text/html" });

// Split the HTML into two parts
const [htmlStart, htmlEnd] =
htmlTemplate.split("<!--ssr-outlet-->");

// inject critical css (Hydration issues at the moment)
// const docsUiCssContent = readFileSync(dirs.app.css.docsUI, "utf8");
// const { critical } = collect(htmlTemplate, docsUiCssContent);
// htmlStart = htmlTemplate.replace("<!--ssr-critical-->", critical);

// Start writing the first part with the headers
res.write(htmlStart);

// Stream the chunks in one at a time
const transformStream = new Transform({
transform(chunk, encoding, callback) {
res.write(chunk, encoding);
callback();
},
});

// When the stream is complete, tack on the end of
// the HTML
transformStream.on("finish", () => {
res.end(htmlEnd);
});

// pipe the stream back into the response
pipe(transformStream);
},
onError(error: Error) {
didError = true;
console.error(error);
},
} as RenderToPipeableStreamOptions
);

setTimeout(() => {
abort();
}, ABORT_DELAY); // 10 seconds
} catch (e) {
const error = e as Error;
// Handle errors with Vite's SSR stack trace
vite.ssrFixStacktrace(error);
LOG.fatal(error);
res.status(500).end(error.stack);
}
// Load the server-entry file as a module using vite
LOG.debug(`Loading the server entry file "${dirs.app.appEntryServer}"`);
const ssrEntryModule = await vite.ssrLoadModule(dirs.app.appEntryServer);

// Run the dev handler which is a combination of node and express
await handleRequestDev(ssrEntryModule.render, {
req,
res,
dirs,
vite,
});
});

app.listen(PORT, () => {
Expand Down
Loading

0 comments on commit 0386bd8

Please sign in to comment.