vite-react-ssg 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,24 @@ Static-site generation for React on Vite.
4
4
 
5
5
  [![NPM version](https://img.shields.io/npm/v/vite-react-ssg?color=a1b858&label=)](https://www.npmjs.com/package/vite-react-ssg)
6
6
 
7
- ## Install
7
+ # Table of contents
8
+ * [Usage](#usage)
9
+ * [Extra route options](#extra-route-options)
10
+ * [`entry`](#entry)
11
+ * [`getStaticPaths`](#getstaticpaths)
12
+ * [lazy](#lazy)
13
+ * [`<ClientOnly/>`](#clientonly)
14
+ * [Document head](#document-head)
15
+ * [Reactive head](#reactive-head)
16
+ * [CSS in JS](#css-in-js)
17
+ * [Critical CSS](#critical-css)
18
+ * [Configuration](#configuration)
19
+ * [Custom Routes to Render](#custom-routes-to-render)
20
+ * [Use CSR in development environment](#use-csr-in-development-environment)
21
+ * [Roadmap](#roadmap)
22
+ * [Credits](#credits)
23
+
24
+ ## Usage
8
25
 
9
26
  <pre>
10
27
  <b>npm i -D vite-react-ssg</b> <em>react-router-dom</em>
@@ -46,40 +63,91 @@ import React from 'react'
46
63
  import type { RouteRecord } from 'vite-react-ssg'
47
64
  import './App.css'
48
65
 
49
- const pages = import.meta.glob<any>('./pages/**/*.tsx')
50
-
51
- const children: RouteRecord[] = Object.entries(pages).map(([filepath, component]) => {
52
- let path = filepath.split('/pages')[1]
53
- path = path.split('.')[0].replace('index', '')
54
- const entry = `src${filepath.slice(1)}`
55
-
56
- if (path.endsWith('/')) {
57
- return {
58
- index: true,
59
- Component: React.lazy(component),
60
- entry,
61
- }
62
- }
63
- return {
64
- path,
65
- Component: React.lazy(component),
66
- // Used to obtain static resources through manifest
67
- entry,
68
- }
69
- })
70
-
71
66
  const Layout = React.lazy(() => import('./Layout'))
67
+
72
68
  export const routes: RouteRecord[] = [
73
69
  {
74
70
  path: '/',
75
71
  element: <Layout />,
76
- children,
77
- // Used to obtain static resources through manifest
78
72
  entry: 'src/Layout.tsx',
73
+ children: [
74
+ {
75
+ path: 'a',
76
+ Component: React.lazy(() => import('./pages/a')),
77
+ entry: 'src/pages/a.tsx',
78
+ },
79
+ {
80
+ index: true,
81
+ Component: React.lazy(() => import('./pages/index')),
82
+ // Used to obtain static resources through manifest
83
+ entry: 'src/pages/index.tsx',
84
+ },
85
+ {
86
+ path: 'nest/:b',
87
+ Component: React.lazy(() => import('./pages/nest/[b]')),
88
+ entry: 'src/pages/nest/[b].tsx',
89
+ // To determine which paths will be pre-rendered
90
+ getStaticPaths: () => ['nest/b1', 'nest/b2'],
91
+ },
92
+ ],
79
93
  },
80
94
  ]
81
95
  ```
82
96
 
97
+ ## Extra route options
98
+
99
+ The RouteObject of vite-react-ssg is based on react-router, and vite-react-ssg receives some additional properties.
100
+
101
+ #### `entry`
102
+ Used to obtain static resources.If you introduce static resources (such as css files) in that route and use lazy loading (such as React.lazy or route.lazy), you should set the entry field. It should be the path from root to the target file.
103
+
104
+ eg: `src/pages/page1.tsx`
105
+
106
+ #### `getStaticPaths`
107
+ The `getStaticPaths()` function should return an array of path
108
+ to determine which paths will be pre-rendered by vite-react-ssg.
109
+
110
+ This function is only valid for dynamic route.
111
+
112
+ ```ts
113
+ const route = {
114
+ path: 'nest/:b',
115
+ Component: React.lazy(() => import('./pages/nest/[b]')),
116
+ entry: 'src/pages/nest/[b].tsx',
117
+ // To determine which paths will be pre-rendered
118
+ getStaticPaths: () => ['nest/b1', 'nest/b2'],
119
+ },
120
+ ```
121
+
122
+ ## lazy
123
+ These options work well with the `lazy` field.
124
+
125
+ ```tsx
126
+ // src/pages/[page].tsx
127
+ export function Component() {
128
+ return (
129
+ <div>{/* your component */}</div>
130
+ )
131
+ }
132
+
133
+ export function getStaticPaths() {
134
+ return ['page1', 'page2']
135
+ }
136
+
137
+ export const entry = 'src/pages/[page].tsx'
138
+ ```
139
+ ```ts
140
+ // src/routes.ts
141
+ const routes = [
142
+ {
143
+ path: '/:page',
144
+ lazy: () => import('./pages/[page]')
145
+ }
146
+ ]
147
+ ```
148
+
149
+ See [example](./examples/lazy-pages/src/App.tsx).
150
+
83
151
  ## `<ClientOnly/>`
84
152
 
85
153
  If you need to render some component in browser only, you can wrap your component with `<ClientOnly>`.
@@ -227,6 +295,110 @@ export default {
227
295
  }
228
296
  ```
229
297
 
298
+ ```ts
299
+ interface ViteReactSSGOptions {
300
+ /**
301
+ * Set the scripts' loading mode. Only works for `type="module"`.
302
+ *
303
+ * @default 'sync'
304
+ */
305
+ script?: 'sync' | 'async' | 'defer' | 'async defer'
306
+ /**
307
+ * Build format.
308
+ *
309
+ * @default 'esm'
310
+ */
311
+ format?: 'esm' | 'cjs'
312
+ /**
313
+ * The path of the main entry file (relative to the project root).
314
+ *
315
+ * @default 'src/main.ts'
316
+ */
317
+ entry?: string
318
+ /**
319
+ * Mock browser global variables (window, document, etc...) from SSG.
320
+ *
321
+ * @default false
322
+ */
323
+ mock?: boolean
324
+ /**
325
+ * Apply formatter to the generated index file.
326
+ *
327
+ * **It will cause Hydration Failed.**
328
+ *
329
+ * @default 'none'
330
+ */
331
+ formatting?: 'minify' | 'prettify' | 'none'
332
+ /**
333
+ * Vite environmeng mode.
334
+ */
335
+ mode?: string
336
+ /**
337
+ * Directory style of the output directory.
338
+ *
339
+ * flat: `/foo` -> `/foo.html`
340
+ * nested: `/foo` -> `/foo/index.html`
341
+ *
342
+ * @default 'flat'
343
+ */
344
+ dirStyle?: 'flat' | 'nested'
345
+ /**
346
+ * Generate for all routes, including dynamic routes.
347
+ * If enabled, you will need to configGure your serve
348
+ * manually to handle dynamic routes properly.
349
+ *
350
+ * @default false
351
+ */
352
+ includeAllRoutes?: boolean
353
+ /**
354
+ * Options for the critters packages.
355
+ *
356
+ * @see https://github.com/GoogleChromeLabs/critters
357
+ */
358
+ crittersOptions?: CrittersOptions | false
359
+ /**
360
+ * Custom function to modify the routes to do the SSG.
361
+ *
362
+ * Works only when `includeAllRoutes` is set to false.
363
+ *
364
+ * Defaults to a handler that filters out all the dynamic routes.
365
+ * When passing your custom handler, you should also take care of the dynamic routes yourself.
366
+ */
367
+ includedRoutes?: (paths: string[], routes: Readonly<RouteRecord[]>) => Promise<string[]> | string[]
368
+ /**
369
+ * Callback to be called before every page render.
370
+ *
371
+ * It can be used to transform the project's `index.html` file before passing it to the renderer.
372
+ *
373
+ * To do so, you can change the 'index.html' file contents (passed in through the `indexHTML` parameter), and return it.
374
+ * The returned value will then be passed to renderer.
375
+ */
376
+ onBeforePageRender?: (route: string, indexHTML: string, appCtx: ViteReactSSGContext<true>) => Promise<string | null | undefined> | string | null | undefined
377
+ /**
378
+ * Callback to be called on every rendered page.
379
+ *
380
+ * It can be used to transform the current route's rendered HTML.
381
+ *
382
+ * To do so, you can transform the route's rendered HTML (passed in through the `renderedHTML` parameter), and return it.
383
+ * The returned value will be used as the HTML of the route.
384
+ */
385
+ onPageRendered?: (route: string, renderedHTML: string, appCtx: ViteReactSSGContext<true>) => Promise<string | null | undefined> | string | null | undefined
386
+
387
+ onFinished?: () => Promise<void> | void
388
+ /**
389
+ * The application's root container `id`.
390
+ *
391
+ * @default `root`
392
+ */
393
+ rootContainerId?: string
394
+ /**
395
+ * The size of the SSG processing queue.
396
+ *
397
+ * @default 20
398
+ */
399
+ concurrency?: number
400
+ }
401
+ ```
230
402
  See [src/types.ts](./src/types.ts). for more options available.
231
403
 
232
404
  ### Custom Routes to Render
@@ -301,8 +473,12 @@ Then, you can start the application with CSR in the development environment.
301
473
  - [x] Preload assets
302
474
  - [x] Document head
303
475
  - [x] SSR in dev environment
476
+ - [x] More Client components, such as `<ClientOnly />`
477
+ - [x] `getStaticPaths` for dynamic routes
304
478
  - [ ] Initial State
305
- - [ ] More Client components, such as `<ClientOnly />`
479
+
480
+ ## Credits
481
+ This project inspired by [vite-ssg](https://github.com/antfu/vite-ssg), thanks to [@antfu](https://github.com/antfu) for his awesome work.
306
482
 
307
483
  ## License
308
484
 
package/dist/index.cjs CHANGED
@@ -157,6 +157,17 @@ function ViteReactSSG(routerOptions, fn, options = {}) {
157
157
  root.render(app);
158
158
  });
159
159
  } else {
160
+ const lazeMatches = reactRouterDom.matchRoutes(routerOptions.routes, window.location)?.filter(
161
+ (m) => m.route.lazy
162
+ );
163
+ if (lazeMatches && lazeMatches?.length > 0) {
164
+ await Promise.all(
165
+ lazeMatches.map(async (m) => {
166
+ const routeModule = await m.route.lazy();
167
+ Object.assign(m.route, { ...routeModule, lazy: void 0 });
168
+ })
169
+ );
170
+ }
160
171
  React__default.startTransition(() => {
161
172
  client.hydrateRoot(container, app);
162
173
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RouterOptions, V as ViteReactSSGContext, a as ViteReactSSGClientOptions } from './types-408e974a.js';
2
- export { I as IndexRouteRecord, N as NonIndexRouteRecord, c as RouteRecord, S as StyleCollector, b as ViteReactSSGOptions } from './types-408e974a.js';
1
+ import { R as RouterOptions, V as ViteReactSSGContext, a as ViteReactSSGClientOptions } from './types-c03d6a58.js';
2
+ export { I as IndexRouteRecord, N as NonIndexRouteRecord, c as RouteRecord, S as StyleCollector, b as ViteReactSSGOptions } from './types-c03d6a58.js';
3
3
  import React, { ReactNode } from 'react';
4
4
  import { HelmetProps } from 'react-helmet-async';
5
5
  import { LinkProps, NavLinkProps } from 'react-router-dom';
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect, isValidElement, forwardRef } from 'react';
2
2
  import { createRoot, hydrateRoot } from 'react-dom/client';
3
3
  import { HelmetProvider } from 'react-helmet-async';
4
- import { useLinkClickHandler, Link as Link$1, NavLink as NavLink$1, createBrowserRouter, RouterProvider } from 'react-router-dom';
4
+ import { useLinkClickHandler, Link as Link$1, NavLink as NavLink$1, matchRoutes, createBrowserRouter, RouterProvider } from 'react-router-dom';
5
5
  import { d as deserializeState, S as SiteMetadataDefaults } from './shared/vite-react-ssg.9d005d5e.mjs';
6
6
  export { H as Head } from './shared/vite-react-ssg.9d005d5e.mjs';
7
7
 
@@ -152,6 +152,17 @@ function ViteReactSSG(routerOptions, fn, options = {}) {
152
152
  root.render(app);
153
153
  });
154
154
  } else {
155
+ const lazeMatches = matchRoutes(routerOptions.routes, window.location)?.filter(
156
+ (m) => m.route.lazy
157
+ );
158
+ if (lazeMatches && lazeMatches?.length > 0) {
159
+ await Promise.all(
160
+ lazeMatches.map(async (m) => {
161
+ const routeModule = await m.route.lazy();
162
+ Object.assign(m.route, { ...routeModule, lazy: void 0 });
163
+ })
164
+ );
165
+ }
155
166
  React.startTransition(() => {
156
167
  hydrateRoot(container, app);
157
168
  });
package/dist/node/cli.cjs CHANGED
@@ -4,7 +4,7 @@ const kolorist = require('kolorist');
4
4
  const yargs = require('yargs');
5
5
  const helpers = require('yargs/helpers');
6
6
  const webFetch = require('@remix-run/web-fetch');
7
- const dev = require('../shared/vite-react-ssg.b0fd1823.cjs');
7
+ const dev = require('../shared/vite-react-ssg.9cf631b2.cjs');
8
8
  require('node:path');
9
9
  require('node:module');
10
10
  require('fs-extra');
package/dist/node/cli.mjs CHANGED
@@ -2,7 +2,7 @@ import { gray, bold, red, reset, underline } from 'kolorist';
2
2
  import yargs from 'yargs';
3
3
  import { hideBin } from 'yargs/helpers';
4
4
  import { Request, Response, fetch as fetch$1, Headers, FormData } from '@remix-run/web-fetch';
5
- import { b as build, d as dev } from '../shared/vite-react-ssg.b83f6ed3.mjs';
5
+ import { b as build, d as dev } from '../shared/vite-react-ssg.f12aa7be.mjs';
6
6
  import 'node:path';
7
7
  import 'node:module';
8
8
  import 'fs-extra';
package/dist/node.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const dev = require('./shared/vite-react-ssg.b0fd1823.cjs');
3
+ const dev = require('./shared/vite-react-ssg.9cf631b2.cjs');
4
4
  require('node:path');
5
5
  require('node:module');
6
6
  require('kolorist');
package/dist/node.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { InlineConfig } from 'vite';
2
- import { b as ViteReactSSGOptions } from './types-408e974a.js';
2
+ import { b as ViteReactSSGOptions } from './types-c03d6a58.js';
3
3
  import 'critters';
4
4
  import 'react';
5
5
  import 'react-router-dom';
package/dist/node.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { b as build, d as dev } from './shared/vite-react-ssg.b83f6ed3.mjs';
1
+ export { b as build, d as dev } from './shared/vite-react-ssg.f12aa7be.mjs';
2
2
  import 'node:path';
3
3
  import 'node:module';
4
4
  import 'kolorist';
@@ -872,6 +872,8 @@ function routesToPaths(routes) {
872
872
  function addEntry(path, entry) {
873
873
  if (!entry)
874
874
  return;
875
+ if (entry[0] === "/")
876
+ entry = entry.slice(1);
875
877
  if (pathToEntry[path])
876
878
  pathToEntry[path].add(entry);
877
879
  else
@@ -880,20 +882,22 @@ function routesToPaths(routes) {
880
882
  if (!routes)
881
883
  return { paths: ["/"], pathToEntry };
882
884
  const paths = /* @__PURE__ */ new Set();
885
+ const lazyPaths = /* @__PURE__ */ new Set();
883
886
  const getPaths = (routes2, prefix = "") => {
884
- const parentPath = prefix;
885
887
  prefix = prefix.replace(/\/$/g, "");
886
888
  for (const route of routes2) {
887
889
  let path = route.path;
888
- if (route.path != null) {
889
- path = prefix && !route.path.startsWith("/") ? `${prefix}${route.path ? `/${route.path}` : ""}` : route.path;
890
- paths.add(path);
891
- addEntry(path, route.entry);
892
- if (pathToEntry[parentPath]) {
893
- const pathCopy = path;
894
- pathToEntry[parentPath].forEach((entry) => addEntry(pathCopy, entry));
890
+ path = handlePath(path, prefix, route.entry);
891
+ if (route.getStaticPaths && path?.includes(":")) {
892
+ const staticPaths = route.getStaticPaths();
893
+ for (let staticPath of staticPaths) {
894
+ staticPath = handlePath(staticPath, prefix, route.entry);
895
+ if (Array.isArray(route.children))
896
+ getPaths(route.children, staticPath);
895
897
  }
896
898
  }
899
+ if (route.lazy)
900
+ lazyPaths.add(route.index ? prefix : path ?? "");
897
901
  if (route.index)
898
902
  addEntry(prefix, route.entry);
899
903
  if (Array.isArray(route.children))
@@ -901,7 +905,19 @@ function routesToPaths(routes) {
901
905
  }
902
906
  };
903
907
  getPaths(routes);
904
- return { paths: Array.from(paths), pathToEntry };
908
+ return { paths: Array.from(paths), pathToEntry, lazyPaths: Array.from(lazyPaths) };
909
+ function handlePath(path, prefix, entry) {
910
+ if (path != null) {
911
+ path = prefix && !path.startsWith("/") ? `${prefix}${path ? `/${path}` : ""}` : path;
912
+ paths.add(path);
913
+ addEntry(path, entry);
914
+ if (pathToEntry[prefix]) {
915
+ const pathCopy = path;
916
+ pathToEntry[prefix].forEach((entry2) => addEntry(pathCopy, entry2));
917
+ }
918
+ }
919
+ return path;
920
+ }
905
921
  }
906
922
  function createFetchRequest(req) {
907
923
  const origin = `${req.protocol}://${req.get("host")}`;
@@ -934,8 +950,15 @@ async function resolveAlias(config, entry) {
934
950
  return result || node_path.join(config.root, entry);
935
951
  }
936
952
  const { version } = JSON.parse(
937
- node_fs.readFileSync(new URL("../../package.json", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('shared/vite-react-ssg.b0fd1823.cjs', document.baseURI).href)))).toString()
953
+ node_fs.readFileSync(new URL("../../package.json", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('shared/vite-react-ssg.9cf631b2.cjs', document.baseURI).href)))).toString()
938
954
  );
955
+ function createRequest(path) {
956
+ const url = new URL(path, "http://vite-react-ssg.com");
957
+ url.search = "";
958
+ url.hash = "";
959
+ url.pathname = path;
960
+ return new Request(url.href);
961
+ }
939
962
 
940
963
  async function getCritters(outDir, options = {}) {
941
964
  try {
@@ -1020,7 +1043,17 @@ async function render(routes, request, styleCollector) {
1020
1043
  ];
1021
1044
  const styleTag = styleCollector?.toString?.(appHTML) ?? "";
1022
1045
  const metaAttributes = metaStrings.filter(Boolean);
1023
- return { appHTML, htmlAttributes, bodyAttributes, metaAttributes, styleTag };
1046
+ return { appHTML, htmlAttributes, bodyAttributes, metaAttributes, styleTag, routerContext: context };
1047
+ }
1048
+ async function preLoad(routes, paths) {
1049
+ if (!paths || paths.length === 0)
1050
+ return routes;
1051
+ const { dataRoutes, query } = server_js.createStaticHandler(routes);
1052
+ await Promise.all(paths.map(async (path) => {
1053
+ const request = createRequest(path);
1054
+ return query(request);
1055
+ }));
1056
+ return dataRoutes;
1024
1057
  }
1025
1058
 
1026
1059
  function renderPreloadLinks(document, modules, ssrManifest) {
@@ -1200,12 +1233,15 @@ async function build(ssgOptions = {}, viteConfig = {}) {
1200
1233
  const prefix = format === "esm" && process.platform === "win32" ? "file://" : "";
1201
1234
  const ext = format === "esm" ? ".mjs" : ".cjs";
1202
1235
  const serverEntry = node_path.join(prefix, ssgOut, node_path.parse(ssrEntry).name + ext);
1203
- const _require = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('shared/vite-react-ssg.b0fd1823.cjs', document.baseURI).href)));
1236
+ const _require = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (document.currentScript && document.currentScript.src || new URL('shared/vite-react-ssg.9cf631b2.cjs', document.baseURI).href)));
1204
1237
  const { createRoot, includedRoutes: serverEntryIncludedRoutes } = format === "esm" ? await import(serverEntry) : _require(serverEntry);
1205
1238
  const includedRoutes = serverEntryIncludedRoutes || configIncludedRoutes;
1206
1239
  const { routes } = await createRoot(false);
1207
- const { paths, pathToEntry } = routesToPaths(routes);
1240
+ const { lazyPaths } = routesToPaths(routes);
1241
+ const dataRoutes = await preLoad([...routes], lazyPaths);
1242
+ const { paths, pathToEntry } = routesToPaths(dataRoutes);
1208
1243
  let routesPaths = includeAllRoutes ? paths : await includedRoutes(paths, routes || []);
1244
+ routesPaths = DefaultIncludedRoutes(routesPaths);
1209
1245
  routesPaths = Array.from(new Set(routesPaths));
1210
1246
  buildLog("Rendering Pages...", routesPaths.length);
1211
1247
  const critters = crittersOptions !== false ? await getCritters(outDir, crittersOptions) : void 0;
@@ -1216,20 +1252,17 @@ async function build(ssgOptions = {}, viteConfig = {}) {
1216
1252
  let indexHTML = await fs__default.readFile(node_path.join(out, "index.html"), "utf-8");
1217
1253
  indexHTML = rewriteScripts(indexHTML, script);
1218
1254
  const queue = new PQueue({ concurrency });
1219
- for (const route of routesPaths) {
1255
+ const crittersQueue = new PQueue({ concurrency: 1 });
1256
+ for (const path of routesPaths) {
1220
1257
  queue.add(async () => {
1221
1258
  try {
1222
- const appCtx = await createRoot(false, route);
1223
- const { routes: routes2, initialState, triggerOnSSRAppRendered, transformState = SiteMetadataDefaults.serializeState, getStyleCollector } = appCtx;
1259
+ const appCtx = await createRoot(false, path);
1260
+ const { initialState, routes: routes2, triggerOnSSRAppRendered, transformState = SiteMetadataDefaults.serializeState, getStyleCollector } = appCtx;
1224
1261
  const styleCollector = getStyleCollector ? await getStyleCollector() : null;
1225
- const transformedIndexHTML = await onBeforePageRender?.(route, indexHTML, appCtx) || indexHTML;
1226
- const url = new URL(route, "http://vite-react-ssg.com");
1227
- url.search = "";
1228
- url.hash = "";
1229
- url.pathname = route;
1230
- const request = new Request(url.href);
1262
+ const transformedIndexHTML = await onBeforePageRender?.(path, indexHTML, appCtx) || indexHTML;
1263
+ const request = createRequest(path);
1231
1264
  const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag } = await render([...routes2], request, styleCollector);
1232
- await triggerOnSSRAppRendered?.(route, appHTML, appCtx);
1265
+ await triggerOnSSRAppRendered?.(path, appHTML, appCtx);
1233
1266
  const renderedHTML = await renderHTML({
1234
1267
  rootContainerId,
1235
1268
  appHTML,
@@ -1240,24 +1273,24 @@ async function build(ssgOptions = {}, viteConfig = {}) {
1240
1273
  initialState: null
1241
1274
  });
1242
1275
  const jsdom = new JSDOM.JSDOM(renderedHTML);
1243
- const modules = collectModulesForEntrys(manifest, pathToEntry?.[route]);
1276
+ const modules = collectModulesForEntrys(manifest, pathToEntry?.[path]);
1244
1277
  renderPreloadLinks(jsdom.window.document, modules, ssrManifest);
1245
1278
  const html = jsdom.serialize();
1246
- let transformed = await onPageRendered?.(route, html, appCtx) || html;
1279
+ let transformed = await onPageRendered?.(path, html, appCtx) || html;
1247
1280
  if (critters)
1248
- transformed = await critters.process(transformed);
1281
+ transformed = await crittersQueue.add(() => critters.process(transformed));
1249
1282
  if (styleTag)
1250
1283
  transformed = transformed.replace("<head>", `<head>${styleTag}`);
1251
1284
  const formatted = await formatHtml(transformed, formatting);
1252
- const relativeRouteFile = `${(route.endsWith("/") ? `${route}index` : route).replace(/^\//g, "")}.html`;
1253
- const filename = dirStyle === "nested" ? node_path.join(route.replace(/^\//g, ""), "index.html") : relativeRouteFile;
1285
+ const relativeRouteFile = `${(path.endsWith("/") ? `${path}index` : path).replace(/^\//g, "")}.html`;
1286
+ const filename = dirStyle === "nested" ? node_path.join(path.replace(/^\//g, ""), "index.html") : relativeRouteFile;
1254
1287
  await fs__default.ensureDir(node_path.join(out, node_path.dirname(filename)));
1255
1288
  await fs__default.writeFile(node_path.join(out, filename), formatted, "utf-8");
1256
1289
  config.logger.info(
1257
1290
  `${kolorist.dim(`${outDir}/`)}${kolorist.cyan(filename.padEnd(15, " "))} ${kolorist.dim(getSize(formatted))}`
1258
1291
  );
1259
1292
  } catch (err) {
1260
- throw new Error(`${kolorist.gray("[vite-react-ssg]")} ${kolorist.red(`Error on page: ${kolorist.cyan(route)}`)}
1293
+ throw new Error(`${kolorist.gray("[vite-react-ssg]")} ${kolorist.red(`Error on page: ${kolorist.cyan(path)}`)}
1261
1294
  ${err.stack}`);
1262
1295
  }
1263
1296
  });
@@ -1427,14 +1460,17 @@ async function dev(ssgOptions = {}, viteConfig = {}) {
1427
1460
  const { routes, getStyleCollector } = appCtx;
1428
1461
  const transformedIndexHTML = await onBeforePageRender?.(url, indexHTML, appCtx) || indexHTML;
1429
1462
  const styleCollector = getStyleCollector ? await getStyleCollector() : null;
1430
- const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag } = await render([...routes], createFetchRequest(req), styleCollector);
1463
+ const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag, routerContext } = await render([...routes], createFetchRequest(req), styleCollector);
1431
1464
  metaAttributes.push(styleTag);
1432
- const mod = await viteServer.moduleGraph.getModuleByUrl(entry);
1465
+ const matchesEntries = routerContext.matches.map((match) => match.route.entry).filter((entry2) => !!entry2).map((entry2) => entry2[0] === "/" ? entry2 : `/${entry2}`);
1466
+ const mods = await Promise.all(
1467
+ [entry, ...matchesEntries].map(async (entry2) => await viteServer.moduleGraph.getModuleByUrl(entry2))
1468
+ );
1433
1469
  const assetsUrls = /* @__PURE__ */ new Set();
1434
- const collectAssets = async (mod2) => {
1435
- if (!mod2?.ssrTransformResult)
1470
+ const collectAssets = async (mod) => {
1471
+ if (!mod || !mod?.ssrTransformResult)
1436
1472
  return;
1437
- const { deps = [], dynamicDeps = [] } = mod2?.ssrTransformResult;
1473
+ const { deps = [], dynamicDeps = [] } = mod?.ssrTransformResult;
1438
1474
  const allDeps = [...deps, ...dynamicDeps];
1439
1475
  for (const dep of allDeps) {
1440
1476
  if (dep.endsWith(".css")) {
@@ -1445,7 +1481,7 @@ async function dev(ssgOptions = {}, viteConfig = {}) {
1445
1481
  }
1446
1482
  }
1447
1483
  };
1448
- await collectAssets(mod);
1484
+ await Promise.all(mods.map(async (mod) => collectAssets(mod)));
1449
1485
  const preloadLink = [...assetsUrls].map((item) => createLink(item));
1450
1486
  metaAttributes.push(...preloadLink);
1451
1487
  const renderedHTML = await renderHTML({
@@ -864,6 +864,8 @@ function routesToPaths(routes) {
864
864
  function addEntry(path, entry) {
865
865
  if (!entry)
866
866
  return;
867
+ if (entry[0] === "/")
868
+ entry = entry.slice(1);
867
869
  if (pathToEntry[path])
868
870
  pathToEntry[path].add(entry);
869
871
  else
@@ -872,20 +874,22 @@ function routesToPaths(routes) {
872
874
  if (!routes)
873
875
  return { paths: ["/"], pathToEntry };
874
876
  const paths = /* @__PURE__ */ new Set();
877
+ const lazyPaths = /* @__PURE__ */ new Set();
875
878
  const getPaths = (routes2, prefix = "") => {
876
- const parentPath = prefix;
877
879
  prefix = prefix.replace(/\/$/g, "");
878
880
  for (const route of routes2) {
879
881
  let path = route.path;
880
- if (route.path != null) {
881
- path = prefix && !route.path.startsWith("/") ? `${prefix}${route.path ? `/${route.path}` : ""}` : route.path;
882
- paths.add(path);
883
- addEntry(path, route.entry);
884
- if (pathToEntry[parentPath]) {
885
- const pathCopy = path;
886
- pathToEntry[parentPath].forEach((entry) => addEntry(pathCopy, entry));
882
+ path = handlePath(path, prefix, route.entry);
883
+ if (route.getStaticPaths && path?.includes(":")) {
884
+ const staticPaths = route.getStaticPaths();
885
+ for (let staticPath of staticPaths) {
886
+ staticPath = handlePath(staticPath, prefix, route.entry);
887
+ if (Array.isArray(route.children))
888
+ getPaths(route.children, staticPath);
887
889
  }
888
890
  }
891
+ if (route.lazy)
892
+ lazyPaths.add(route.index ? prefix : path ?? "");
889
893
  if (route.index)
890
894
  addEntry(prefix, route.entry);
891
895
  if (Array.isArray(route.children))
@@ -893,7 +897,19 @@ function routesToPaths(routes) {
893
897
  }
894
898
  };
895
899
  getPaths(routes);
896
- return { paths: Array.from(paths), pathToEntry };
900
+ return { paths: Array.from(paths), pathToEntry, lazyPaths: Array.from(lazyPaths) };
901
+ function handlePath(path, prefix, entry) {
902
+ if (path != null) {
903
+ path = prefix && !path.startsWith("/") ? `${prefix}${path ? `/${path}` : ""}` : path;
904
+ paths.add(path);
905
+ addEntry(path, entry);
906
+ if (pathToEntry[prefix]) {
907
+ const pathCopy = path;
908
+ pathToEntry[prefix].forEach((entry2) => addEntry(pathCopy, entry2));
909
+ }
910
+ }
911
+ return path;
912
+ }
897
913
  }
898
914
  function createFetchRequest(req) {
899
915
  const origin = `${req.protocol}://${req.get("host")}`;
@@ -928,6 +944,13 @@ async function resolveAlias(config, entry) {
928
944
  const { version } = JSON.parse(
929
945
  readFileSync(new URL("../../package.json", import.meta.url)).toString()
930
946
  );
947
+ function createRequest(path) {
948
+ const url = new URL(path, "http://vite-react-ssg.com");
949
+ url.search = "";
950
+ url.hash = "";
951
+ url.pathname = path;
952
+ return new Request(url.href);
953
+ }
931
954
 
932
955
  async function getCritters(outDir, options = {}) {
933
956
  try {
@@ -1012,7 +1035,17 @@ async function render(routes, request, styleCollector) {
1012
1035
  ];
1013
1036
  const styleTag = styleCollector?.toString?.(appHTML) ?? "";
1014
1037
  const metaAttributes = metaStrings.filter(Boolean);
1015
- return { appHTML, htmlAttributes, bodyAttributes, metaAttributes, styleTag };
1038
+ return { appHTML, htmlAttributes, bodyAttributes, metaAttributes, styleTag, routerContext: context };
1039
+ }
1040
+ async function preLoad(routes, paths) {
1041
+ if (!paths || paths.length === 0)
1042
+ return routes;
1043
+ const { dataRoutes, query } = createStaticHandler(routes);
1044
+ await Promise.all(paths.map(async (path) => {
1045
+ const request = createRequest(path);
1046
+ return query(request);
1047
+ }));
1048
+ return dataRoutes;
1016
1049
  }
1017
1050
 
1018
1051
  function renderPreloadLinks(document, modules, ssrManifest) {
@@ -1196,8 +1229,11 @@ async function build(ssgOptions = {}, viteConfig = {}) {
1196
1229
  const { createRoot, includedRoutes: serverEntryIncludedRoutes } = format === "esm" ? await import(serverEntry) : _require(serverEntry);
1197
1230
  const includedRoutes = serverEntryIncludedRoutes || configIncludedRoutes;
1198
1231
  const { routes } = await createRoot(false);
1199
- const { paths, pathToEntry } = routesToPaths(routes);
1232
+ const { lazyPaths } = routesToPaths(routes);
1233
+ const dataRoutes = await preLoad([...routes], lazyPaths);
1234
+ const { paths, pathToEntry } = routesToPaths(dataRoutes);
1200
1235
  let routesPaths = includeAllRoutes ? paths : await includedRoutes(paths, routes || []);
1236
+ routesPaths = DefaultIncludedRoutes(routesPaths);
1201
1237
  routesPaths = Array.from(new Set(routesPaths));
1202
1238
  buildLog("Rendering Pages...", routesPaths.length);
1203
1239
  const critters = crittersOptions !== false ? await getCritters(outDir, crittersOptions) : void 0;
@@ -1208,20 +1244,17 @@ async function build(ssgOptions = {}, viteConfig = {}) {
1208
1244
  let indexHTML = await fs.readFile(join(out, "index.html"), "utf-8");
1209
1245
  indexHTML = rewriteScripts(indexHTML, script);
1210
1246
  const queue = new PQueue({ concurrency });
1211
- for (const route of routesPaths) {
1247
+ const crittersQueue = new PQueue({ concurrency: 1 });
1248
+ for (const path of routesPaths) {
1212
1249
  queue.add(async () => {
1213
1250
  try {
1214
- const appCtx = await createRoot(false, route);
1215
- const { routes: routes2, initialState, triggerOnSSRAppRendered, transformState = serializeState, getStyleCollector } = appCtx;
1251
+ const appCtx = await createRoot(false, path);
1252
+ const { initialState, routes: routes2, triggerOnSSRAppRendered, transformState = serializeState, getStyleCollector } = appCtx;
1216
1253
  const styleCollector = getStyleCollector ? await getStyleCollector() : null;
1217
- const transformedIndexHTML = await onBeforePageRender?.(route, indexHTML, appCtx) || indexHTML;
1218
- const url = new URL(route, "http://vite-react-ssg.com");
1219
- url.search = "";
1220
- url.hash = "";
1221
- url.pathname = route;
1222
- const request = new Request(url.href);
1254
+ const transformedIndexHTML = await onBeforePageRender?.(path, indexHTML, appCtx) || indexHTML;
1255
+ const request = createRequest(path);
1223
1256
  const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag } = await render([...routes2], request, styleCollector);
1224
- await triggerOnSSRAppRendered?.(route, appHTML, appCtx);
1257
+ await triggerOnSSRAppRendered?.(path, appHTML, appCtx);
1225
1258
  const renderedHTML = await renderHTML({
1226
1259
  rootContainerId,
1227
1260
  appHTML,
@@ -1232,24 +1265,24 @@ async function build(ssgOptions = {}, viteConfig = {}) {
1232
1265
  initialState: null
1233
1266
  });
1234
1267
  const jsdom = new JSDOM(renderedHTML);
1235
- const modules = collectModulesForEntrys(manifest, pathToEntry?.[route]);
1268
+ const modules = collectModulesForEntrys(manifest, pathToEntry?.[path]);
1236
1269
  renderPreloadLinks(jsdom.window.document, modules, ssrManifest);
1237
1270
  const html = jsdom.serialize();
1238
- let transformed = await onPageRendered?.(route, html, appCtx) || html;
1271
+ let transformed = await onPageRendered?.(path, html, appCtx) || html;
1239
1272
  if (critters)
1240
- transformed = await critters.process(transformed);
1273
+ transformed = await crittersQueue.add(() => critters.process(transformed));
1241
1274
  if (styleTag)
1242
1275
  transformed = transformed.replace("<head>", `<head>${styleTag}`);
1243
1276
  const formatted = await formatHtml(transformed, formatting);
1244
- const relativeRouteFile = `${(route.endsWith("/") ? `${route}index` : route).replace(/^\//g, "")}.html`;
1245
- const filename = dirStyle === "nested" ? join(route.replace(/^\//g, ""), "index.html") : relativeRouteFile;
1277
+ const relativeRouteFile = `${(path.endsWith("/") ? `${path}index` : path).replace(/^\//g, "")}.html`;
1278
+ const filename = dirStyle === "nested" ? join(path.replace(/^\//g, ""), "index.html") : relativeRouteFile;
1246
1279
  await fs.ensureDir(join(out, dirname(filename)));
1247
1280
  await fs.writeFile(join(out, filename), formatted, "utf-8");
1248
1281
  config.logger.info(
1249
1282
  `${dim(`${outDir}/`)}${cyan(filename.padEnd(15, " "))} ${dim(getSize(formatted))}`
1250
1283
  );
1251
1284
  } catch (err) {
1252
- throw new Error(`${gray("[vite-react-ssg]")} ${red(`Error on page: ${cyan(route)}`)}
1285
+ throw new Error(`${gray("[vite-react-ssg]")} ${red(`Error on page: ${cyan(path)}`)}
1253
1286
  ${err.stack}`);
1254
1287
  }
1255
1288
  });
@@ -1419,14 +1452,17 @@ async function dev(ssgOptions = {}, viteConfig = {}) {
1419
1452
  const { routes, getStyleCollector } = appCtx;
1420
1453
  const transformedIndexHTML = await onBeforePageRender?.(url, indexHTML, appCtx) || indexHTML;
1421
1454
  const styleCollector = getStyleCollector ? await getStyleCollector() : null;
1422
- const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag } = await render([...routes], createFetchRequest(req), styleCollector);
1455
+ const { appHTML, bodyAttributes, htmlAttributes, metaAttributes, styleTag, routerContext } = await render([...routes], createFetchRequest(req), styleCollector);
1423
1456
  metaAttributes.push(styleTag);
1424
- const mod = await viteServer.moduleGraph.getModuleByUrl(entry);
1457
+ const matchesEntries = routerContext.matches.map((match) => match.route.entry).filter((entry2) => !!entry2).map((entry2) => entry2[0] === "/" ? entry2 : `/${entry2}`);
1458
+ const mods = await Promise.all(
1459
+ [entry, ...matchesEntries].map(async (entry2) => await viteServer.moduleGraph.getModuleByUrl(entry2))
1460
+ );
1425
1461
  const assetsUrls = /* @__PURE__ */ new Set();
1426
- const collectAssets = async (mod2) => {
1427
- if (!mod2?.ssrTransformResult)
1462
+ const collectAssets = async (mod) => {
1463
+ if (!mod || !mod?.ssrTransformResult)
1428
1464
  return;
1429
- const { deps = [], dynamicDeps = [] } = mod2?.ssrTransformResult;
1465
+ const { deps = [], dynamicDeps = [] } = mod?.ssrTransformResult;
1430
1466
  const allDeps = [...deps, ...dynamicDeps];
1431
1467
  for (const dep of allDeps) {
1432
1468
  if (dep.endsWith(".css")) {
@@ -1437,7 +1473,7 @@ async function dev(ssgOptions = {}, viteConfig = {}) {
1437
1473
  }
1438
1474
  }
1439
1475
  };
1440
- await collectAssets(mod);
1476
+ await Promise.all(mods.map(async (mod) => collectAssets(mod)));
1441
1477
  const preloadLink = [...assetsUrls].map((item) => createLink(item));
1442
1478
  metaAttributes.push(...preloadLink);
1443
1479
  const renderedHTML = await renderHTML({
@@ -92,7 +92,7 @@ interface ViteReactSSGOptions {
92
92
  onPageRendered?: (route: string, renderedHTML: string, appCtx: ViteReactSSGContext<true>) => Promise<string | null | undefined> | string | null | undefined;
93
93
  onFinished?: () => Promise<void> | void;
94
94
  /**
95
- * The application's root container `class`.
95
+ * The application's root container `id`.
96
96
  *
97
97
  * @default `root`
98
98
  */
@@ -142,9 +142,18 @@ interface CommonRouteOptions {
142
142
  /**
143
143
  * Used to obtain static resources through manifest
144
144
  *
145
- * @example `src/pages/home.tsx
145
+ * @example `src/pages/home.tsx`
146
146
  */
147
147
  entry?: string;
148
+ /**
149
+ * The getStaticPaths() function should return an array of path
150
+ * to determine which paths will be pre-rendered by vite-react-ssg.
151
+ *
152
+ * This function is only valid for dynamic route.
153
+ *
154
+ * @example () => ['path1', 'path2']
155
+ */
156
+ getStaticPaths?: () => string[];
148
157
  }
149
158
  type NonIndexRouteRecord = Omit<NonIndexRouteObject, 'children'> & {
150
159
  children?: RouteRecord[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vite-react-ssg",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.1",
5
5
  "packageManager": "pnpm@8.6.6",
6
6
  "description": "",
7
7
  "author": "Riri <Daydreamerriri@outlook.com>",
@@ -118,7 +118,6 @@
118
118
  "react-dom": "^18.2.0",
119
119
  "react-router-dom": "^6.14.1",
120
120
  "rimraf": "5.0.1",
121
- "simple-git-hooks": "^2.8.1",
122
121
  "styled-components": "6.0.5",
123
122
  "typescript": "5.1.6",
124
123
  "unbuild": "^1.2.1",