~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


c44e7193 Peter John

2 years ago
add hydrate function
packages/cli/index.js CHANGED
@@ -62,14 +62,24 @@ const mapDeps = (dir) => {
62
62
 
63
63
  const staticDir = path.join(process.cwd(), "build", "static");
64
64
 
65
+ const ensureDir = (d) => {
66
+ if (!fs.existsSync(d)) {
67
+ fs.mkdirSync(d, { recursive: true });
68
+ }
69
+ }
70
+
65
71
  const createDirs = () => {
66
72
  const buildDir = path.join(process.cwd(), "build");
67
- // if (fs.existsSync(buildDir)) {
73
+ ensureDir(buildDir);
68
- // fs.rmSync(buildDir, { recursive: true });
69
- // }
70
- if (!fs.existsSync(staticDir)) {
74
+ ensureDir(staticDir);
71
- fs.mkdirSync(staticDir, { recursive: true });
72
- }
75
+ }
76
+
77
+ const recordSize = (buildStart, dest) => {
78
+ const outLength = fs.statSync(dest).size;
79
+ const builtTime = ms(Date.now() - buildStart);
80
+ console.log(
81
+ `${pc.green("✓ Bundled")} ${dest.replace(process.cwd() + "/", "")} ${pc.cyan(`(${bytes(outLength)})`)} ${pc.gray(`[${builtTime}]`)}`
82
+ );
73
83
  }
74
84
 
75
85
  const buildImportMap = async () => {
@@ -111,18 +121,12 @@ const buildRouteMap = () => {
111
121
 
112
122
  let generatedCss = ``;
113
123
  const cssCache = [];
114
- const buildServer = async (src, type) => {
124
+ const buildServer = async (options, src, dest, plg) => {
115
- const buildStart = Date.now();
116
- const shortName = src.replace(process.cwd(), "");
117
- const outName = type === "service"
118
- ? "/_rpc" + shortName.replace("/services", "").replace(".service.js", "")
119
- : shortName.replace("/pages", "").replace("/page.jsx", "") || "/index";
120
- const outfile = `${process.cwd()}/build/functions${outName}.js`;
121
125
  const result = await esbuild.build({
122
126
  bundle: true,
123
127
  target: ['es2022'],
124
128
  entryPoints: [src],
125
- outfile: outfile,
129
+ outfile: dest,
126
130
  format: 'esm',
127
131
  keepNames: true,
128
132
  external: ["node:*"],
@@ -132,6 +136,7 @@ const buildServer = async (src, type) => {
132
136
  // metafile: true,
133
137
  jsxDev: !isProd,
134
138
  jsx: 'automatic',
139
+ ...options,
135
140
  define: {
136
141
  'process.env.NODE_ENV': `"${process.env.NODE_ENV}"`,
137
142
  'process.env.PG_CONN_URL': "123",
@@ -141,45 +146,10 @@ const buildServer = async (src, type) => {
141
146
  "/static/routemap.json": `${staticDir}/routemap.json`,
142
147
  "/static/importmap.json": `${staticDir}/importmap.json`
143
148
  }),
144
- {
145
- name: "parotta-plugin",
146
- setup(build) {
147
- build.onLoad({ filter: /\\*.page.jsx/, namespace: undefined }, (args) => {
148
- const data = fs.readFileSync(args.path);
149
- const newSrc = `
150
- import { renderPage } from "parotta-runtime";
151
- ${data.toString()}
152
-
153
- export function onRequest(context) {
154
- return renderPage(Page, context.request);
155
- }
156
- `
157
- return {
158
- contents: newSrc,
159
- loader: "jsx",
160
- }
161
- });
149
+ plg,
162
- build.onLoad({ filter: /\\*.css/, namespace: undefined }, (args) => {
163
- if (!cssCache[args.path]) {
164
- const css = fs.readFileSync(args.path);
165
- generatedCss += css + "\n\n";
166
- cssCache[args.path] = true;
167
- }
168
- return {
169
- contents: "",
170
- loader: "file",
171
- }
172
- });
173
- }
174
- }
175
150
  ]
176
151
  });
177
- // console.log(await analyzeMetafile(result.metafile))
178
- const outLength = fs.statSync(outfile).size;
179
- const builtTime = ms(Date.now() - buildStart);
180
- console.log(
152
+ return result;
181
- `${pc.green("✓ Bundled")} ${outfile.replace(process.cwd() + "/", "")} ${pc.cyan(`(${bytes(outLength)})`)} ${pc.gray(`[${builtTime}]`)}`
182
- );
183
153
  }
184
154
 
185
155
  // const bundleBun = async (r, type) => {
@@ -219,10 +189,89 @@ const main = async () => {
219
189
  buildImportMap();
220
190
  buildRouteMap();
221
191
  for (const r of routes) {
192
+ const buildStart = Date.now();
193
+ const dest = r.replace(process.cwd(), "").replace("/pages", "").replace("/page.jsx", "") || "/index";
194
+ const outfile = `build/functions${dest}.js`;
222
- await buildServer(r, "page");
195
+ await buildServer({}, r, outfile, {
196
+ name: "parotta-page-plugin",
197
+ setup(build) {
198
+ build.onLoad({ filter: /\\*.page.jsx/, namespace: undefined }, (args) => {
199
+ const data = fs.readFileSync(args.path);
200
+ const newSrc = `
201
+ import { renderPage } from "parotta-runtime";
202
+ ${data.toString()}
203
+
204
+ export function onRequest(context) {
205
+ return renderPage(Page, context.request);
206
+ }
207
+ `
208
+ return {
209
+ contents: newSrc,
210
+ loader: "jsx",
211
+ }
212
+ });
213
+ build.onLoad({ filter: /\\*.css/, namespace: undefined }, (args) => {
214
+ if (!cssCache[args.path]) {
215
+ const css = fs.readFileSync(args.path);
216
+ generatedCss += css + "\n\n";
217
+ cssCache[args.path] = true;
218
+ }
219
+ return {
220
+ contents: "",
221
+ loader: "file",
222
+ }
223
+ });
224
+ }
225
+ });
226
+ recordSize(buildStart, outfile);
223
227
  }
224
228
  for (const s of services) {
229
+ const dest = s.replace(process.cwd(), "").replace("/services", "").replace(".service.js", "")
230
+ const pkg = await import(s);
231
+ for (const p of Object.keys(pkg)) {
225
- await buildServer(s, "service");
232
+ const buildStart = Date.now();
233
+ const result = await buildServer({ write: false }, s, `build/functions/_rpc${dest}.js`, {
234
+ name: "parotta-service-plugin",
235
+ setup(build) {
236
+ build.onLoad({ filter: /\\*.service.js/, namespace: undefined }, async (args) => {
237
+ const src = fs.readFileSync(args.path);
238
+ const newSrc = `
239
+ ${src.toString()}
240
+
241
+ export const renderApi = async (fn, req) => {
242
+ const url = new URL(req.url);
243
+ const params = req.method === "POST" ? await req.json() : Object.fromEntries(url.searchParams);
244
+ try {
245
+ const result = await fn(params);
246
+ return new Response(JSON.stringify(result), {
247
+ headers: { 'Content-Type': 'application/json' },
248
+ status: 200,
249
+ });
250
+ } catch (err) {
251
+ const message = err.format ? err.format() : err;
252
+ return new Response(JSON.stringify(message), {
253
+ headers: { 'Content-Type': 'application/json' },
254
+ status: 400,
255
+ });
256
+ }
257
+ }
258
+
259
+ export function onRequest(context) {
260
+ return renderApi(${p}, context.request);
261
+ }
262
+ `
263
+ return {
264
+ contents: newSrc,
265
+ loader: "js",
266
+ };
267
+ });
268
+ }
269
+ })
270
+ ensureDir(`build/functions/_rpc${dest}`)
271
+ const outfile = `build/functions/_rpc${dest}/${p}.js`;
272
+ fs.writeFileSync(outfile, result.outputFiles[0].contents);
273
+ recordSize(buildStart, outfile);
274
+ }
226
275
  }
227
276
  bundleCss();
228
277
  }
packages/example/components/Layout/Layout.jsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Link } from "parotta-runtime";
3
3
  import "modern-normalize";
4
- import "nprogress/nprogress.css"
4
+ import "nprogress";
5
5
  import "./Layout.css";
6
6
 
7
7
  const Layout = ({ children }) => {
packages/example/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "parotta-example",
3
3
  "type": "module",
4
4
  "scripts": {
5
- "dev": "wrangler pages dev static",
5
+ "dev": "wrangler pages dev static --local --live-reload",
6
6
  "build": "parotta build cloudflare",
7
7
  "run": "docker run -p 3000:3000 example",
8
8
  "test": "bun test",
packages/example/services/todos.service.js CHANGED
@@ -11,12 +11,12 @@ const todos = pgTable('todos', {
11
11
  updatedAt: date('updatedAt'),
12
12
  });
13
13
 
14
- export const createSchema = z.object({
14
+ const createSchema = z.object({
15
15
  text: z.string().nonempty("please enter some text"),
16
16
  completed: z.boolean(),
17
17
  });
18
18
 
19
- export const updateSchema = z.object({
19
+ const updateSchema = z.object({
20
20
  text: z.string().nonempty("please enter some text"),
21
21
  completed: z.boolean(),
22
22
  });
packages/runtime/index.js CHANGED
@@ -5,10 +5,12 @@ 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
7
  import { renderToReadableStream } from "react-dom/server";
8
+ import { hydrateRoot } from "react-dom/client";
8
9
  import { HelmetProvider } from 'react-helmet-async';
9
10
  import { ErrorBoundary } from "react-error-boundary";
10
- import { createMemoryHistory } from "history";
11
+ import { createMemoryHistory, createBrowserHistory } from "history";
11
12
  import { createRouter } from "radix3";
13
+ import nProgress from "nprogress";
12
14
  import importmap from '/static/importmap.json' assert {type: 'json'};
13
15
  import routemap from '/static/routemap.json' assert {type: 'json'};
14
16
 
@@ -126,7 +128,7 @@ export const RouterContext = createContext(undefined);
126
128
  const getMatch = (radixRouter, pathname) => {
127
129
  const matchedPage = radixRouter.lookup(pathname);
128
130
  if (!matchedPage) {
129
- return radixRouter.lookup("_404")
131
+ return radixRouter.lookup("/static/js/_404.js")
130
132
  }
131
133
  return matchedPage;
132
134
  }
@@ -246,7 +248,7 @@ export const NavLink = ({ children, className, activeClassName, ...props }) => {
246
248
  */
247
249
  export const renderPage = async (PageComponent, req) => {
248
250
  const url = new URL(req.url);
249
- const clientRouter = createRouter({
251
+ const router = createRouter({
250
252
  strictTrailingSlash: true,
251
253
  routes: Object.keys(routemap).reduce((acc, r) => {
252
254
  acc[r] = React.lazy(() => import(`/pages${r}/page.jsx`));
@@ -257,7 +259,6 @@ export const renderPage = async (PageComponent, req) => {
257
259
  initialEntries: [url.pathname + url.search],
258
260
  });
259
261
  const helmetContext = {}
260
- const nProgress = { start: () => { }, done: () => { } }
261
262
  const stream = await renderToReadableStream(
262
263
  _jsxs("html", {
263
264
  lang: "en",
@@ -277,7 +278,7 @@ export const renderPage = async (PageComponent, req) => {
277
278
  children: [_jsx(App, {
278
279
  nProgress,
279
280
  history,
280
- radixRouter: clientRouter,
281
+ router,
281
282
  rpcCache: {},
282
283
  helmetContext,
283
284
  PageComponent,
@@ -287,31 +288,9 @@ export const renderPage = async (PageComponent, req) => {
287
288
  defer: true,
288
289
  dangerouslySetInnerHTML: {
289
290
  __html: `
290
- import React from "react";
291
- import { hydrateRoot } from "react-dom/client";
292
- import { createBrowserHistory } from "history";
293
- import nProgress from "nprogress";
294
- import { createRouter } from "radix3";
295
- import { App } from "parotta/runtime";
291
+ import { hydrateApp } from "parotta-runtime";
296
- import routemap from '/static/routemap.json' assert {type: 'json'};
297
- // import sheet from './styles.css' assert { type: 'css' };
298
-
299
- const history = createBrowserHistory();
300
- const radixRouter = createRouter({
301
- strictTrailingSlash: true,
302
- routes: {
303
- ${Object.keys(routemap).map(r => `"${r}": React.lazy(() => import("/pages${r}/page.jsx"))`).join(',\n ')}
304
- },
305
- });
306
-
307
- hydrateRoot(document.body, React.createElement(App, {
308
- nProgress,
309
- history,
310
- radixRouter,
311
- rpcCache: {},
292
+ hydrateApp();
312
- helmetContext: {},
313
- PageComponent: null,
314
- }));`
293
+ `
315
294
  }
316
295
  })
317
296
  })]
@@ -328,22 +307,21 @@ export const renderPage = async (PageComponent, req) => {
328
307
  });
329
308
  }
330
309
 
331
- export const renderApi = async (key, filePath, req) => {
310
+ export const hydrateApp = () => {
311
+ const history = createBrowserHistory();
332
- const url = new URL(req.url);
312
+ const router = createRouter({
333
- const params = req.method === "POST" ? await req.json() : Object.fromEntries(url.searchParams);
334
- const funcName = url.pathname.replace(`${key}/`, "");
335
- const js = await import(path.join(process.cwd(), filePath));
336
- try {
337
- const result = await js[funcName](params);
313
+ strictTrailingSlash: true,
338
- return new Response(JSON.stringify(result), {
314
+ routes: Object.keys(routemap).reduce((acc, r) => {
339
- headers: { 'Content-Type': 'application/json' },
315
+ acc[r] = React.lazy(() => import(`/static/js/${r}.js`));
340
- status: 200,
316
+ return acc;
317
+ }, {}),
341
- });
318
+ });
319
+ hydrateRoot(document.body, React.createElement(App, {
320
+ nProgress,
321
+ history,
322
+ router,
342
- } catch (err) {
323
+ rpcCache: {},
343
- const message = err.format ? err.format() : err;
344
- return new Response(JSON.stringify(message), {
345
- headers: { 'Content-Type': 'application/json' },
346
- status: 400,
324
+ helmetContext: {},
325
+ PageComponent: null,
347
- });
326
+ }));
348
- }
349
327
  }
packages/runtime/package.json CHANGED
@@ -9,6 +9,7 @@
9
9
  "peerDependencies": {
10
10
  "react": "*",
11
11
  "react-dom": "*",
12
+ "nprogress": "*",
12
13
  "react-error-boundary": "*",
13
14
  "react-helmet-async": "*"
14
15
  }
pnpm-lock.yaml CHANGED
@@ -113,6 +113,9 @@ importers:
113
113
  history:
114
114
  specifier: ^5.3.0
115
115
  version: 5.3.0
116
+ nprogress:
117
+ specifier: '*'
118
+ version: 0.2.0
116
119
  radix3:
117
120
  specifier: ^1.0.0
118
121
  version: 1.0.0