~repos /edge-city

#react#js#ssr

git clone https://pyrossh.dev/repos/edge-city.git

edge-city is a next level meta-framework for react that runs only on edge runtimes


fde21b09 Peter John

2 years ago
init hydration
Files changed (2) hide show
  1. packages/cli/index.js +61 -25
  2. packages/runtime/index.js +45 -50
packages/cli/index.js CHANGED
@@ -40,10 +40,6 @@ if (!globalThis.firstRun) {
40
40
  console.log(`server reloading`);
41
41
  }
42
42
  const isProd = process.env.NODE_ENV === "production";
43
- const routes = walkdir.sync(path.join(process.cwd(), "pages"))
44
- .filter((p) => p.includes("page.jsx"));
45
- const services = walkdir.sync(path.join(process.cwd(), "services"))
46
- .filter((s) => s.includes(".service.js"))
47
43
 
48
44
  const mapDeps = (dir) => {
49
45
  return walkdir.sync(path.join(process.cwd(), dir))
@@ -108,11 +104,11 @@ const buildImportMap = async () => {
108
104
  fs.writeFileSync(outfile, JSON.stringify(importmap, null, 2));
109
105
  }
110
106
 
111
- const buildRouteMap = () => {
107
+ const buildRouteMap = (routes) => {
112
108
  const routemap = routes.reduce((acc, p) => {
113
109
  const r = p.replace(process.cwd(), "");
114
110
  const key = r.replace("/pages", "").replace("/page.jsx", "")
115
- acc[key === "" ? "/" : key] = r;
111
+ acc[key === "" ? "/" : key] = "/js" + (r.replace("/pages", "").replace("/page.jsx", "") || "/index") + ".js";
116
112
  return acc
117
113
  }, {});
118
114
  const outfile = path.join(staticDir, "routemap.json");
@@ -121,7 +117,7 @@ const buildRouteMap = () => {
121
117
 
122
118
  let generatedCss = ``;
123
119
  const cssCache = [];
124
- const buildServer = async (options, src, dest, plg) => {
120
+ const bundleJs = async (options, src, dest, plg) => {
125
121
  const result = await esbuild.build({
126
122
  bundle: true,
127
123
  target: ['es2022'],
@@ -143,8 +139,8 @@ const buildServer = async (options, src, dest, plg) => {
143
139
  },
144
140
  plugins: [
145
141
  resolve({
146
- "/static/routemap.json": `${staticDir}/routemap.json`,
142
+ "/routemap.json": `${staticDir}/routemap.json`,
147
- "/static/importmap.json": `${staticDir}/importmap.json`
143
+ "/importmap.json": `${staticDir}/importmap.json`
148
144
  }),
149
145
  plg,
150
146
  ]
@@ -175,24 +171,15 @@ const buildServer = async (options, src, dest, plg) => {
175
171
  // }
176
172
  // }
177
173
 
178
- const bundleCss = async () => {
174
+ const bundlePages = async () => {
179
- const result = await postcss([
180
- autoprefixer(),
181
- postcssCustomMedia(),
182
- postcssNesting,
183
- ]).process(generatedCss, { from: "app.css", to: "app.css" });
175
+ const routes = walkdir.sync(path.join(process.cwd(), "pages"))
184
- fs.writeFileSync(`${process.cwd()}/build/static/app.css`, result.toString());
176
+ .filter((p) => p.includes("page.jsx"));
185
- }
186
-
187
- const main = async () => {
188
- createDirs();
189
- buildImportMap();
190
- buildRouteMap();
177
+ buildRouteMap(routes);
191
178
  for (const r of routes) {
192
179
  const buildStart = Date.now();
193
180
  const dest = r.replace(process.cwd(), "").replace("/pages", "").replace("/page.jsx", "") || "/index";
194
181
  const outfile = `build/functions${dest}.js`;
195
- await buildServer({}, r, outfile, {
182
+ await bundleJs({}, r, outfile, {
196
183
  name: "parotta-page-plugin",
197
184
  setup(build) {
198
185
  build.onLoad({ filter: /\\*.page.jsx/, namespace: undefined }, (args) => {
@@ -225,12 +212,44 @@ const main = async () => {
225
212
  });
226
213
  recordSize(buildStart, outfile);
227
214
  }
215
+ for (const r of routes) {
216
+ const buildStart = Date.now();
217
+ const dest = r.replace(process.cwd(), "").replace("/pages", "").replace("/page.jsx", "") || "/index";
218
+ const outfile = `build/static/js${dest}.js`;
219
+ await bundleJs({}, r, outfile, {
220
+ name: "parotta-page-js-plugin",
221
+ setup(build) {
222
+ build.onLoad({ filter: /\\*.page.jsx/, namespace: undefined }, (args) => {
223
+ const data = fs.readFileSync(args.path);
224
+ const newSrc = `
225
+ import { hydrateApp } from "parotta-runtime";
226
+ ${data.toString()}
227
+
228
+ const searchParams = new URL(import.meta.url).searchParams;
229
+ if (searchParams.get("hydrate") === "true") {
230
+ hydrateApp(Page)
231
+ }
232
+ `
233
+ return {
234
+ contents: newSrc,
235
+ loader: "jsx",
236
+ }
237
+ });
238
+ }
239
+ });
240
+ recordSize(buildStart, outfile);
241
+ }
242
+ }
243
+
244
+ const bundleServices = async () => {
245
+ const services = walkdir.sync(path.join(process.cwd(), "services"))
246
+ .filter((s) => s.includes(".service.js"));
228
247
  for (const s of services) {
229
248
  const dest = s.replace(process.cwd(), "").replace("/services", "").replace(".service.js", "")
230
249
  const pkg = await import(s);
231
250
  for (const p of Object.keys(pkg)) {
232
251
  const buildStart = Date.now();
233
- const result = await buildServer({ write: false }, s, `build/functions/_rpc${dest}.js`, {
252
+ const result = await bundleJs({ write: false }, s, `build/functions/_rpc${dest}.js`, {
234
253
  name: "parotta-service-plugin",
235
254
  setup(build) {
236
255
  build.onLoad({ filter: /\\*.service.js/, namespace: undefined }, async (args) => {
@@ -273,7 +292,24 @@ const main = async () => {
273
292
  recordSize(buildStart, outfile);
274
293
  }
275
294
  }
295
+ }
296
+
297
+ const bundleCss = async () => {
298
+ const result = await postcss([
299
+ autoprefixer(),
300
+ postcssCustomMedia(),
301
+ postcssNesting,
302
+ ]).process(generatedCss, { from: "app.css", to: "app.css" });
303
+ ensureDir(`build/static/css`)
304
+ fs.writeFileSync(`${process.cwd()}/build/static/css/app.css`, result.toString());
305
+ }
306
+
307
+ const main = async () => {
308
+ createDirs();
309
+ buildImportMap();
310
+ await bundlePages();
311
+ // await bundleServices();
276
- bundleCss();
312
+ await bundleCss();
277
313
  }
278
314
 
279
315
  main();
packages/runtime/index.js CHANGED
@@ -4,15 +4,13 @@ import React, {
4
4
  import { jsx as _jsx } from "react/jsx-runtime";
5
5
  import { jsxs as _jsxs } from "react/jsx-runtime";
6
6
  import { Fragment as _Fragment } from "react/jsx-runtime";
7
- import { renderToReadableStream } from "react-dom/server";
8
- import { hydrateRoot } from "react-dom/client";
9
7
  import { HelmetProvider } from 'react-helmet-async';
10
8
  import { ErrorBoundary } from "react-error-boundary";
11
9
  import { createMemoryHistory, createBrowserHistory } from "history";
12
10
  import { createRouter } from "radix3";
13
11
  import nProgress from "nprogress";
14
- import importmap from '/static/importmap.json' assert {type: 'json'};
12
+ import importmap from '/importmap.json' assert {type: 'json'};
15
- import routemap from '/static/routemap.json' assert {type: 'json'};
13
+ import routemap from '/routemap.json' assert {type: 'json'};
16
14
 
17
15
  /**
18
16
  * CSR related functions
@@ -125,27 +123,27 @@ export const useMutation = (fn) => {
125
123
 
126
124
  export const RouterContext = createContext(undefined);
127
125
 
128
- const getMatch = (radixRouter, pathname) => {
126
+ const getMatch = (router, pathname) => {
129
- const matchedPage = radixRouter.lookup(pathname);
127
+ const matchedPage = router.lookup(pathname);
130
128
  if (!matchedPage) {
131
- return radixRouter.lookup("/static/js/_404.js")
129
+ return router.lookup("/js/_404.js")
132
130
  }
133
131
  return matchedPage;
134
132
  }
135
133
 
136
134
  const getCssUrl = (pathname) => `/pages${pathname === "/" ? "" : pathname}/page.css`;
137
135
 
138
- export const App = ({ nProgress, history, radixRouter, rpcCache, helmetContext, PageComponent }) => {
136
+ export const App = ({ nProgress, history, router, rpcCache, helmetContext, PageComponent }) => {
139
137
  const [isPending, startTransition] = useTransition();
140
138
  const [match, setMatch] = useState(() => {
141
139
  if (PageComponent) {
142
140
  return PageComponent;
143
141
  }
144
- return getMatch(radixRouter, history.location.pathname)
142
+ return getMatch(router, history.location.pathname)
145
143
  });
146
144
  useEffect(() => {
147
145
  return history.listen(({ location }) => {
148
- const href = getCssUrl(location.pathname);
146
+ // const href = getCssUrl(location.pathname);
149
147
  // const isLoaded = Array.from(document.getElementsByTagName("link"))
150
148
  // .map((link) => link.href.replace(window.origin, "")).includes(href);
151
149
  // if (!isLoaded) {
@@ -155,24 +153,25 @@ export const App = ({ nProgress, history, radixRouter, rpcCache, helmetContext,
155
153
  // link.onload = () => {
156
154
  // nProgress.start();
157
155
  // startTransition(() => {
158
- // setMatch(getMatch(radixRouter, location.pathname));
156
+ // setMatch(getMatch(router, location.pathname));
159
157
  // })
160
158
  // };
161
159
  // link.setAttribute("href", href);
162
160
  // document.getElementsByTagName("head")[0].appendChild(link);
163
161
  // } else {
164
- const link = document.createElement('link');
162
+ // const link = document.createElement('link');
165
- link.setAttribute("rel", "stylesheet");
163
+ // link.setAttribute("rel", "stylesheet");
166
- link.setAttribute("type", "text/css");
164
+ // link.setAttribute("type", "text/css");
167
- link.setAttribute("href", href);
165
+ // link.setAttribute("href", href);
168
- document.getElementsByTagName("head")[0].appendChild(link);
166
+ // document.getElementsByTagName("head")[0].appendChild(link);
169
167
  nProgress.start();
170
168
  startTransition(() => {
171
- setMatch(getMatch(radixRouter, location.pathname));
169
+ setMatch(getMatch(router, location.pathname));
172
170
  })
173
171
  // }
174
172
  });
175
173
  }, []);
174
+ console.log("match", match)
176
175
  useEffect(() => {
177
176
  if (!isPending) {
178
177
  nProgress.done();
@@ -228,7 +227,7 @@ export const Link = (props) => {
228
227
  if (props && props.onClick) {
229
228
  props.onClick(e);
230
229
  }
231
- router.push(props.href)
230
+ router.push(props.href);
232
231
  },
233
232
  })
234
233
  }
@@ -247,17 +246,12 @@ export const NavLink = ({ children, className, activeClassName, ...props }) => {
247
246
  * SSR related functions
248
247
  */
249
248
  export const renderPage = async (PageComponent, req) => {
249
+ const { renderToReadableStream } = await import("react-dom/server");
250
250
  const url = new URL(req.url);
251
- const router = createRouter({
252
- strictTrailingSlash: true,
253
- routes: Object.keys(routemap).reduce((acc, r) => {
254
- acc[r] = React.lazy(() => import(`/pages${r}/page.jsx`));
255
- return acc;
256
- }, {}),
257
- });
258
251
  const history = createMemoryHistory({
259
252
  initialEntries: [url.pathname + url.search],
260
253
  });
254
+ const jsScript = url.pathname === "/" ? "index" : url.pathname;
261
255
  const helmetContext = {}
262
256
  const stream = await renderToReadableStream(
263
257
  _jsxs("html", {
@@ -266,7 +260,7 @@ export const renderPage = async (PageComponent, req) => {
266
260
  children: [
267
261
  _jsx("link", {
268
262
  rel: "stylesheet",
269
- href: "/app.css"
263
+ href: "/css/app.css"
270
264
  }),
271
265
  _jsx("script", {
272
266
  type: "importmap",
@@ -274,26 +268,24 @@ export const renderPage = async (PageComponent, req) => {
274
268
  __html: JSON.stringify(importmap),
275
269
  }
276
270
  })]
277
- }), _jsxs("body", {
271
+ }), _jsx("body", {
272
+ children: _jsxs("div", {
273
+ id: "root",
278
- children: [_jsx(App, {
274
+ children: [_jsx(App, {
279
- nProgress,
275
+ nProgress,
280
- history,
276
+ history,
281
- router,
277
+ router: null,
282
- rpcCache: {},
278
+ rpcCache: {},
283
- helmetContext,
279
+ helmetContext,
284
- PageComponent,
280
+ PageComponent,
285
- }), false && _jsx(_Fragment, {
281
+ }), _jsx(_Fragment, {
286
- children: _jsx("script", {
282
+ children: _jsx("script", {
287
- type: "module",
283
+ type: "module",
288
- defer: true,
284
+ defer: true,
289
- dangerouslySetInnerHTML: {
290
- __html: `
291
- import { hydrateApp } from "parotta-runtime";
285
+ src: `/js/${jsScript}.js?hydrate=true`,
292
- hydrateApp();
293
- `
294
- }
295
- })
286
+ })
296
- })]
287
+ })]
288
+ })
297
289
  })]
298
290
  }));
299
291
  // TODO:
@@ -307,21 +299,24 @@ export const renderPage = async (PageComponent, req) => {
307
299
  });
308
300
  }
309
301
 
310
- export const hydrateApp = () => {
302
+ export const hydrateApp = async (Page) => {
303
+ console.log("hydrating");
304
+ const { hydrateRoot } = await import("react-dom/client");
311
305
  const history = createBrowserHistory();
312
306
  const router = createRouter({
313
307
  strictTrailingSlash: true,
314
308
  routes: Object.keys(routemap).reduce((acc, r) => {
315
- acc[r] = React.lazy(() => import(`/static/js/${r}.js`));
309
+ acc[r] = React.lazy(() => import(routemap[r]));
316
310
  return acc;
317
311
  }, {}),
318
312
  });
313
+ const root = document.getElementById("root");
319
- hydrateRoot(document.body, React.createElement(App, {
314
+ hydrateRoot(root, React.createElement(App, {
320
315
  nProgress,
321
316
  history,
322
317
  router,
323
318
  rpcCache: {},
324
319
  helmetContext: {},
325
- PageComponent: null,
320
+ PageComponent: Page,
326
321
  }));
327
322
  }