~repos /edge-city
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
- packages/cli/index.js +61 -25
- 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
|
|
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
|
-
"/
|
|
142
|
+
"/routemap.json": `${staticDir}/routemap.json`,
|
|
147
|
-
"/
|
|
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
|
|
174
|
+
const bundlePages = async () => {
|
|
179
|
-
const result = await postcss([
|
|
180
|
-
autoprefixer(),
|
|
181
|
-
postcssCustomMedia(),
|
|
182
|
-
postcssNesting,
|
|
183
|
-
|
|
175
|
+
const routes = walkdir.sync(path.join(process.cwd(), "pages"))
|
|
184
|
-
|
|
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
|
|
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
|
|
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 '/
|
|
12
|
+
import importmap from '/importmap.json' assert {type: 'json'};
|
|
15
|
-
import routemap from '/
|
|
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 = (
|
|
126
|
+
const getMatch = (router, pathname) => {
|
|
129
|
-
const matchedPage =
|
|
127
|
+
const matchedPage = router.lookup(pathname);
|
|
130
128
|
if (!matchedPage) {
|
|
131
|
-
return
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
}),
|
|
271
|
+
}), _jsx("body", {
|
|
272
|
+
children: _jsxs("div", {
|
|
273
|
+
id: "root",
|
|
278
|
-
|
|
274
|
+
children: [_jsx(App, {
|
|
279
|
-
|
|
275
|
+
nProgress,
|
|
280
|
-
|
|
276
|
+
history,
|
|
281
|
-
|
|
277
|
+
router: null,
|
|
282
|
-
|
|
278
|
+
rpcCache: {},
|
|
283
|
-
|
|
279
|
+
helmetContext,
|
|
284
|
-
|
|
280
|
+
PageComponent,
|
|
285
|
-
|
|
281
|
+
}), _jsx(_Fragment, {
|
|
286
|
-
|
|
282
|
+
children: _jsx("script", {
|
|
287
|
-
|
|
283
|
+
type: "module",
|
|
288
|
-
|
|
284
|
+
defer: true,
|
|
289
|
-
dangerouslySetInnerHTML: {
|
|
290
|
-
__html: `
|
|
291
|
-
|
|
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(
|
|
309
|
+
acc[r] = React.lazy(() => import(routemap[r]));
|
|
316
310
|
return acc;
|
|
317
311
|
}, {}),
|
|
318
312
|
});
|
|
313
|
+
const root = document.getElementById("root");
|
|
319
|
-
hydrateRoot(
|
|
314
|
+
hydrateRoot(root, React.createElement(App, {
|
|
320
315
|
nProgress,
|
|
321
316
|
history,
|
|
322
317
|
router,
|
|
323
318
|
rpcCache: {},
|
|
324
319
|
helmetContext: {},
|
|
325
|
-
PageComponent:
|
|
320
|
+
PageComponent: Page,
|
|
326
321
|
}));
|
|
327
322
|
}
|