@alepha/react 0.7.0 → 0.7.2

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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/dist/index.browser.cjs +59 -22
  3. package/dist/index.browser.js +42 -6
  4. package/dist/index.cjs +152 -85
  5. package/dist/index.d.ts +317 -206
  6. package/dist/index.js +130 -64
  7. package/dist/{useActive-DjpZBEuB.cjs → useRouterState-C2uo0jXu.cjs} +319 -140
  8. package/dist/{useActive-BX41CqY8.js → useRouterState-D5__ZcUV.js} +321 -143
  9. package/package.json +11 -10
  10. package/src/components/ClientOnly.tsx +35 -0
  11. package/src/components/ErrorBoundary.tsx +1 -1
  12. package/src/components/ErrorViewer.tsx +161 -0
  13. package/src/components/Link.tsx +9 -3
  14. package/src/components/NestedView.tsx +18 -3
  15. package/src/components/NotFound.tsx +30 -0
  16. package/src/descriptors/$page.ts +141 -30
  17. package/src/errors/RedirectionError.ts +4 -1
  18. package/src/hooks/RouterHookApi.ts +42 -24
  19. package/src/hooks/useAlepha.ts +12 -0
  20. package/src/hooks/useClient.ts +8 -6
  21. package/src/hooks/useInject.ts +2 -2
  22. package/src/hooks/useQueryParams.ts +1 -1
  23. package/src/hooks/useRouter.ts +6 -0
  24. package/src/index.browser.ts +4 -2
  25. package/src/index.shared.ts +11 -5
  26. package/src/index.ts +3 -4
  27. package/src/providers/BrowserRouterProvider.ts +4 -3
  28. package/src/providers/PageDescriptorProvider.ts +91 -20
  29. package/src/providers/ReactBrowserProvider.ts +6 -59
  30. package/src/providers/ReactBrowserRenderer.ts +72 -0
  31. package/src/providers/ReactServerProvider.ts +200 -81
  32. package/dist/index.browser.cjs.map +0 -1
  33. package/dist/index.browser.js.map +0 -1
  34. package/dist/index.cjs.map +0 -1
  35. package/dist/index.js.map +0 -1
  36. package/dist/useActive-BX41CqY8.js.map +0 -1
  37. package/dist/useActive-DjpZBEuB.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,15 +1,13 @@
1
1
  import { t, $logger, $inject, Alepha, $hook, OPTIONS, __bind } from '@alepha/core';
2
- import { ServerRouterProvider, ServerLinksProvider, ServerModule } from '@alepha/server';
3
- import { P as PageDescriptorProvider, $ as $page } from './useActive-BX41CqY8.js';
4
- export { E as ErrorBoundary, L as Link, N as NestedView, l as ReactBrowserProvider, j as RedirectionError, R as RouterContext, b as RouterHookApi, a as RouterLayerContext, k as isPageRoute, i as useActive, d as useApi, c as useClient, u as useInject, e as useQueryParams, f as useRouter, g as useRouterEvents, h as useRouterState } from './useActive-BX41CqY8.js';
2
+ import { ServerRouterProvider, ServerTimingProvider, ServerLinksProvider, apiLinksResponseSchema, ServerModule } from '@alepha/server';
3
+ import { P as PageDescriptorProvider, $ as $page } from './useRouterState-D5__ZcUV.js';
4
+ export { C as ClientOnly, E as ErrorBoundary, L as Link, N as NestedView, l as ReactBrowserProvider, R as RedirectionError, a as RouterContext, c as RouterHookApi, b as RouterLayerContext, k as isPageRoute, u as useActive, d as useAlepha, e as useClient, f as useInject, g as useQueryParams, h as useRouter, i as useRouterEvents, j as useRouterState } from './useRouterState-D5__ZcUV.js';
5
5
  import { existsSync } from 'node:fs';
6
- import { readFile } from 'node:fs/promises';
7
6
  import { join } from 'node:path';
8
7
  import { ServerStaticProvider } from '@alepha/server-static';
9
8
  import { renderToString } from 'react-dom/server';
10
9
  import 'react/jsx-runtime';
11
10
  import 'react';
12
- import 'react-dom/client';
13
11
  import '@alepha/router';
14
12
 
15
13
  class ServerHeadProvider {
@@ -75,9 +73,9 @@ class ServerHeadProvider {
75
73
  }
76
74
 
77
75
  const envSchema = t.object({
78
- REACT_SERVER_DIST: t.string({ default: "client" }),
76
+ REACT_SERVER_DIST: t.string({ default: "public" }),
79
77
  REACT_SERVER_PREFIX: t.string({ default: "" }),
80
- REACT_SSR_ENABLED: t.boolean({ default: false }),
78
+ REACT_SSR_ENABLED: t.optional(t.boolean()),
81
79
  REACT_ROOT_ID: t.string({ default: "root" })
82
80
  });
83
81
  class ReactServerProvider {
@@ -87,46 +85,75 @@ class ReactServerProvider {
87
85
  serverStaticProvider = $inject(ServerStaticProvider);
88
86
  serverRouterProvider = $inject(ServerRouterProvider);
89
87
  headProvider = $inject(ServerHeadProvider);
88
+ serverTimingProvider = $inject(ServerTimingProvider);
90
89
  env = $inject(envSchema);
91
90
  ROOT_DIV_REGEX = new RegExp(
92
91
  `<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
93
92
  "is"
94
93
  );
95
- configure = $hook({
94
+ onConfigure = $hook({
96
95
  name: "configure",
97
96
  handler: async () => {
98
97
  const pages = this.alepha.getDescriptorValues($page);
99
- if (pages.length === 0) {
100
- return;
101
- }
98
+ const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
99
+ this.alepha.state("ReactServerProvider.ssr", ssrEnabled);
102
100
  for (const { key, instance, value } of pages) {
103
101
  const name = value[OPTIONS].name ?? key;
102
+ instance[key].prerender = this.createRenderFunction(name, true);
104
103
  if (this.alepha.isTest()) {
105
104
  instance[key].render = this.createRenderFunction(name);
106
105
  }
107
106
  }
108
107
  if (this.alepha.isServerless() === "vite") {
109
- await this.configureVite();
108
+ await this.configureVite(ssrEnabled);
110
109
  return;
111
110
  }
112
111
  let root = "";
113
112
  if (!this.alepha.isServerless()) {
114
113
  root = this.getPublicDirectory();
115
114
  if (!root) {
116
- this.log.warn("Missing static files, SSR will be disabled");
117
- return;
115
+ this.log.warn(
116
+ "Missing static files, static file server will be disabled"
117
+ );
118
+ } else {
119
+ this.log.debug(`Using static files from: ${root}`);
120
+ await this.configureStaticServer(root);
118
121
  }
119
- await this.configureStaticServer(root);
120
122
  }
121
- const template = this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8");
122
- await this.registerPages(async () => template);
123
- this.alepha.state("ReactServerProvider.ssr", true);
123
+ if (ssrEnabled) {
124
+ await this.registerPages(async () => this.template);
125
+ this.log.info("SSR OK");
126
+ return;
127
+ }
128
+ this.log.info("SSR is disabled, use History API fallback");
129
+ await this.serverRouterProvider.route({
130
+ path: "*",
131
+ handler: async ({ url, reply }) => {
132
+ if (url.pathname.includes(".")) {
133
+ reply.headers["content-type"] = "text/plain";
134
+ reply.body = "Not Found";
135
+ reply.status = 404;
136
+ return;
137
+ }
138
+ reply.headers["content-type"] = "text/html";
139
+ return this.template;
140
+ }
141
+ });
124
142
  }
125
143
  });
144
+ get template() {
145
+ return this.alepha.state("ReactServerProvider.template");
146
+ }
126
147
  async registerPages(templateLoader) {
127
148
  for (const page of this.pageDescriptorProvider.getPages()) {
149
+ if (page.children?.length) {
150
+ continue;
151
+ }
128
152
  this.log.debug(`+ ${page.match} -> ${page.name}`);
129
153
  await this.serverRouterProvider.route({
154
+ ...page,
155
+ schema: void 0,
156
+ // schema is handled by the page descriptor provider for now (shared by browser and server)
130
157
  method: "GET",
131
158
  path: page.match,
132
159
  handler: this.createHandler(page, templateLoader)
@@ -135,8 +162,8 @@ class ReactServerProvider {
135
162
  }
136
163
  getPublicDirectory() {
137
164
  const maybe = [
138
- join(process.cwd(), this.env.REACT_SERVER_DIST),
139
- join(process.cwd(), "..", this.env.REACT_SERVER_DIST)
165
+ join(process.cwd(), `dist/${this.env.REACT_SERVER_DIST}`),
166
+ join(process.cwd(), this.env.REACT_SERVER_DIST)
140
167
  ];
141
168
  for (const it of maybe) {
142
169
  if (existsSync(it)) {
@@ -151,23 +178,25 @@ class ReactServerProvider {
151
178
  path: this.env.REACT_SERVER_PREFIX
152
179
  });
153
180
  }
154
- async configureVite() {
155
- const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
181
+ async configureVite(ssrEnabled) {
182
+ if (!ssrEnabled) {
183
+ return;
184
+ }
156
185
  this.log.info("SSR (vite) OK");
157
- this.alepha.state("ReactServerProvider.ssr", true);
158
- const templateUrl = `${url}/index.html`;
186
+ const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
159
187
  await this.registerPages(
160
- () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0)
188
+ () => fetch(`${url}/index.html`).then((it) => it.text()).catch(() => void 0)
161
189
  );
162
190
  }
163
191
  /**
164
192
  * For testing purposes, creates a render function that can be used.
165
193
  */
166
- createRenderFunction(name) {
194
+ createRenderFunction(name, withIndex = false) {
167
195
  return async (options = {}) => {
168
196
  const page = this.pageDescriptorProvider.page(name);
197
+ const url = new URL(this.pageDescriptorProvider.url(name, options));
169
198
  const context = {
170
- url: new URL("http://localhost"),
199
+ url,
171
200
  params: options.params ?? {},
172
201
  query: options.query ?? {},
173
202
  head: {},
@@ -177,7 +206,18 @@ class ReactServerProvider {
177
206
  page,
178
207
  context
179
208
  );
180
- return renderToString(this.pageDescriptorProvider.root(state, context));
209
+ if (!withIndex) {
210
+ return {
211
+ context,
212
+ html: renderToString(
213
+ this.pageDescriptorProvider.root(state, context)
214
+ )
215
+ };
216
+ }
217
+ return {
218
+ context,
219
+ html: this.renderToHtml(this.template ?? "", state, context)
220
+ };
181
221
  };
182
222
  }
183
223
  createHandler(page, templateLoader) {
@@ -197,11 +237,24 @@ class ReactServerProvider {
197
237
  };
198
238
  if (this.alepha.has(ServerLinksProvider)) {
199
239
  const srv = this.alepha.get(ServerLinksProvider);
200
- context.links = await srv.getLinks({
201
- user: serverRequest.user,
202
- authorization: serverRequest.headers.authorization
203
- });
204
- this.alepha.als.set("links", context.links);
240
+ const schema = apiLinksResponseSchema;
241
+ context.links = this.alepha.parse(
242
+ schema,
243
+ await srv.getLinks({
244
+ user: serverRequest.user,
245
+ authorization: serverRequest.headers.authorization
246
+ })
247
+ );
248
+ this.alepha.context.set("links", context.links);
249
+ }
250
+ let target = page;
251
+ while (target) {
252
+ if (page.can && !page.can()) {
253
+ reply.status = 403;
254
+ reply.headers["content-type"] = "text/plain";
255
+ return "Forbidden";
256
+ }
257
+ target = target.parent;
205
258
  }
206
259
  await this.alepha.emit(
207
260
  "react:server:render",
@@ -213,49 +266,63 @@ class ReactServerProvider {
213
266
  log: false
214
267
  }
215
268
  );
269
+ this.serverTimingProvider.beginTiming("createLayers");
216
270
  const state = await this.pageDescriptorProvider.createLayers(
217
271
  page,
218
272
  context
219
273
  );
274
+ this.serverTimingProvider.endTiming("createLayers");
220
275
  if (state.redirect) {
221
276
  return reply.redirect(state.redirect);
222
277
  }
223
- const element = this.pageDescriptorProvider.root(state, context);
224
- const app = renderToString(element);
225
- const hydrationData = {
226
- links: context.links,
227
- layers: state.layers.map((it) => ({
228
- ...it,
229
- error: it.error ? {
230
- ...it.error,
231
- name: it.error.name,
232
- message: it.error.message,
233
- stack: it.error.stack
234
- // TODO: Hide stack in production ?
235
- } : void 0,
236
- index: void 0,
237
- path: void 0,
238
- element: void 0
239
- }))
240
- };
241
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
242
- const response = {
243
- html: template
244
- };
245
- reply.status = 200;
246
278
  reply.headers["content-type"] = "text/html";
247
279
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
248
280
  reply.headers.pragma = "no-cache";
249
281
  reply.headers.expires = "0";
250
- this.fillTemplate(response, app, script);
251
- if (context.head) {
252
- response.html = this.headProvider.renderHead(
253
- response.html,
254
- context.head
255
- );
282
+ if (page.cache && serverRequest.user) {
283
+ delete context.links;
256
284
  }
257
- return response.html;
285
+ const html = this.renderToHtml(template, state, context);
286
+ page.afterHandler?.(serverRequest);
287
+ return html;
288
+ };
289
+ }
290
+ renderToHtml(template, state, context) {
291
+ const element = this.pageDescriptorProvider.root(state, context);
292
+ this.serverTimingProvider.beginTiming("renderToString");
293
+ let app = "";
294
+ try {
295
+ app = renderToString(element);
296
+ } catch (error) {
297
+ this.log.error("Error during SSR", error);
298
+ app = renderToString(context.onError(error));
299
+ }
300
+ this.serverTimingProvider.endTiming("renderToString");
301
+ const hydrationData = {
302
+ links: context.links,
303
+ layers: state.layers.map((it) => ({
304
+ ...it,
305
+ error: it.error ? {
306
+ ...it.error,
307
+ name: it.error.name,
308
+ message: it.error.message,
309
+ stack: it.error.stack
310
+ // TODO: Hide stack in production ?
311
+ } : void 0,
312
+ index: void 0,
313
+ path: void 0,
314
+ element: void 0
315
+ }))
316
+ };
317
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
318
+ const response = {
319
+ html: template
258
320
  };
321
+ this.fillTemplate(response, app, script);
322
+ if (context.head) {
323
+ response.html = this.headProvider.renderHead(response.html, context.head);
324
+ }
325
+ return response.html;
259
326
  }
260
327
  fillTemplate(response, app, script) {
261
328
  if (this.ROOT_DIV_REGEX.test(response.html)) {
@@ -294,4 +361,3 @@ class ReactModule {
294
361
  __bind($page, ReactModule);
295
362
 
296
363
  export { $page, PageDescriptorProvider, ReactModule, ReactServerProvider, envSchema };
297
- //# sourceMappingURL=index.js.map