Skip to content

Commit

Permalink
feat: ssg for react
Browse files Browse the repository at this point in the history
  • Loading branch information
Wroud committed Nov 13, 2024
1 parent dc97e25 commit 7413d99
Show file tree
Hide file tree
Showing 32 changed files with 1,940 additions and 2 deletions.
36 changes: 36 additions & 0 deletions packages/@wroud/tsconfig/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": [],
"compilerOptions": {
"target": "ES2022",
"lib": [
"esnext"
],
"types": [
"node"
],
"module": "nodenext",
"skipLibCheck": true,
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"importHelpers": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": false,
"verbatimModuleSyntax": true,
"checkJs": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
/* Source maps */
"sourceMap": true,
/* project references */
"declarationMap": true
}
}
139 changes: 139 additions & 0 deletions packages/@wroud/vite-plugin-ssg/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# @wroud/vite-plugin-ssg

[![ESM-only package][package]][package-url]
[![NPM version][npm]][npm-url]

[package]: https://img.shields.io/badge/package-ESM--only-ffe536.svg
[package-url]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[npm]: https://img.shields.io/npm/v/@wroud/vite-plugin-ssg.svg
[npm-url]: https://npmjs.com/package/@wroud/vite-plugin-ssg

`@wroud/vite-plugin-ssg` is a Vite plugin for rendering React applications statically to HTML. It allows for generating pre-rendered HTML files, improving SEO and initial loading times for client-side rendered apps.

## Features

- **Static Site Generation (SSG)**: Renders your React application to static HTML at build time.
- **Configurable Render Timeout**: Customize the render timeout for flexible rendering behavior.
- **React Refresh**: Supports React Refresh for a seamless development experience.
- **Hot Module Reload (HMR)**: Enables Hot Module Reload for faster iterative development.

## Requirements

- **React** 19 or higher
- **Vite** 6 or higher

## Installation

Install via npm:

```sh
npm install @wroud/vite-plugin-ssg
```

Install via yarn:

```sh
yarn add @wroud/vite-plugin-ssg
```

## Usage

### Options

`@wroud/vite-plugin-ssg` provides a configurable option:

```ts
interface SSGOptions {
renderTimeout?: number;
}
```

### Example Configuration

To add `@wroud/vite-plugin-ssg` to your Vite application, follow these steps:

1. Import the plugin:

```ts
import { ssgPlugin } from "@wroud/vite-plugin-ssg";
```

2. Add the plugin and configure the build entry point using `rollupOptions`:

```ts
import path from "path";
export default defineConfig({
build: {
rollupOptions: {
input: {
app: path.resolve("src/index.tsx") + "?ssg",
},
},
},
plugins: [ssgPlugin()],
});
```

Here, we specify the entry point with a `?ssg` query to signal SSG processing.

3. In `vite-env.d.ts` add:

```ts
/// <reference types="vite/client" />
/// <reference types="@wroud/vite-plugin-ssg/resolvers" />
```

4. Create your `index.tsx` file with a default export for the `Index` component:

```tsx
import type { IndexComponentProps } from "@wroud/vite-plugin-ssg";
import { Body, Head } from "@wroud/vite-plugin-ssg/react/components";
import main from "./index.tsx?ssg-main";
import indexStyles from "./index.css?url";
import { App } from "./App.js";
export default function Index(props: IndexComponentProps) {
return (
<html lang="en">
<Head {...props}>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<title>Grid</title>
<link rel="stylesheet" href={indexStyles} />
</Head>
<Body {...props} after={<script type="module" src={main} />}>
<App />
</Body>
</html>
);
}
```

- Use a default export for the `Index` component.
- Import styles with `?url` and add them as a `<link>` element.
- Import the main file (e.g., `index.tsx`) with the `?ssg-main` query and include it as a script element for client-side bootstrapping.

### Generated Output

When building the project, the following files will be generated:

- `app/index.js`: Contains the exported `render` function for SSG, which can be manually used to generate HTML.
- `app/index.html`: The statically generated HTML page.
- `assets/index-[hash].css`: The CSS file with styles.
- `assets/app/index-[hash].js`: The client bootstrap script.

## Documentation

For detailed usage and API reference, visit the [documentation site](https://wroud.dev).

## Changelog

All notable changes to this project will be documented in the [CHANGELOG](./CHANGELOG.md) file.

## License

This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
75 changes: 75 additions & 0 deletions packages/@wroud/vite-plugin-ssg/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@wroud/vite-plugin-ssg",
"description": "A Vite plugin for static site generation (SSG) with React. Renders React applications to static HTML for faster load times and improved SEO.",
"version": "0.0.0",
"type": "module",
"sideEffects": [],
"exports": {
".": "./lib/ssgPlugin.js",
"./*": "./lib/*.js",
"./resolvers": {
"types": "./resolvers.d.ts"
}
},
"scripts": {
"ci:release": "yarn ci release --prefix vite-plugin-ssg-v",
"ci:git-tag": "yarn ci git-tag --prefix vite-plugin-ssg-v",
"ci:release-github": "yarn ci release-github --prefix vite-plugin-ssg-v",
"build": "tsc -b",
"watch:tsc": "tsc -b -w",
"clear": "rimraf lib"
},
"files": [
"package.json",
"LICENSE",
"README.md",
"CHANGELOG.md",
"lib",
"!lib/**/*.d.ts.map",
"!lib/**/*.test.js",
"!lib/**/*.test.d.ts",
"!lib/**/*.test.d.ts.map",
"!lib/**/*.test.js.map",
"!lib/tests",
"!.tsbuildinfo"
],
"packageManager": "yarn@4.5.0",
"devDependencies": {
"@types/node": "^20",
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"@wroud/tsconfig": "workspace:^",
"rimraf": "^6",
"rollup": "^4",
"typescript": "^5",
"vite": ">=6.0.0-beta"
},
"dependencies": {
"change-case": "^5",
"magic-string": "^0",
"react": ">=19.0.0-rc",
"react-dom": ">=19.0.0-rc",
"style-to-object": "^1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"vite": ">=6.0.0-beta"
},
"overrides": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc"
},
"keywords": [
"vite",
"vite-plugin",
"static-site-generation",
"ssg",
"react",
"static-rendering",
"html-generation",
"vite-ssg",
"pre-rendering",
"SEO"
]
}
4 changes: 4 additions & 0 deletions packages/@wroud/vite-plugin-ssg/resolvers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module "*?ssg-main" {
const src: string;
export default src;
}
8 changes: 8 additions & 0 deletions packages/@wroud/vite-plugin-ssg/src/IEntryDescriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { HtmlTagDescriptor } from "vite";

export interface IEntryDescriptor {
chunk: string;
entry: string;
main?: IEntryDescriptor;
htmlTags: HtmlTagDescriptor[];
}
50 changes: 50 additions & 0 deletions packages/@wroud/vite-plugin-ssg/src/isTagDescriptorEquals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { HtmlTagDescriptor } from "vite";

export function isTagDescriptorEquals(
a: HtmlTagDescriptor,
b: HtmlTagDescriptor,
) {
if (a.tag !== b.tag) {
return false;
}

if ((a.injectTo || "head-prepend") !== (b.injectTo || "head-prepend")) {
return false;
}

const aKeys = Object.keys(a.attrs || {});
const bKeys = Object.keys(b.attrs || {});

if (aKeys.length !== bKeys.length) {
return false;
}

for (const key of aKeys) {
if (a.attrs?.[key] !== b.attrs?.[key]) {
return false;
}
}

if (a.children !== b.children) {
if (typeof a.children !== typeof b.children) {
return false;
}
if (typeof a.children === "string" && a.children !== b.children) {
return false;
}

if (Array.isArray(a.children) && Array.isArray(b.children)) {
if (a.children.length !== b.children.length) {
return false;
}

for (let i = 0; i < a.children.length; i++) {
if (!isTagDescriptorEquals(a.children[i]!, b.children[i]!)) {
return false;
}
}
}
}

return true;
}
35 changes: 35 additions & 0 deletions packages/@wroud/vite-plugin-ssg/src/parseHtmlTagsFromHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { HtmlTagDescriptor } from "vite";
import { unescapeHtml } from "./utils/unescapeHtml.js";

// Main regex to match tag, attributes, and content
const mainRegex = /<(\w+)(\s+[^>]*?)?>([\s\S]*?)<\/\1>|<(\w+)(\s+[^>]*?)?\/?>/g;
// Secondary regex to capture each attribute within the attributes string
const attributeRegex = /(\w+)(?:="([^"]*)")?/g;

export function parseHtmlTagsFromHtml(html: string): HtmlTagDescriptor[] {
const matches = [...html.matchAll(mainRegex)];

const tags = matches.map<HtmlTagDescriptor>((match) => {
const tagName = match[1] || match[4]; // Tag name
const attributesString = match[2] || match[5] || ""; // Attributes as string
const content = match[3] || ""; // Content

// Extract individual attributes as an array of { name, value } pairs
const attrs = [...attributesString.matchAll(attributeRegex)].reduce(
(acc, [, name, value]) => ({
...acc,
[name!]: unescapeHtml(value),
}),
{},
);

return {
tag: tagName,
attrs,
children: content,
injectTo: "head-prepend",
} as HtmlTagDescriptor;
});

return tags;
}
15 changes: 15 additions & 0 deletions packages/@wroud/vite-plugin-ssg/src/react/IndexComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type React from "react";

export interface IndexComponentContext {
cspNonce?: string;
base?: string;
}

export interface IndexComponentProps {
renderTags: (
injectTo?: "head" | "body" | "head-prepend" | "body-prepend",
) => React.ReactElement;
context: IndexComponentContext;
}

export type IndexComponent = React.FC<IndexComponentProps>;
Loading

0 comments on commit 7413d99

Please sign in to comment.