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