@alepha/react 0.7.0 → 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 -21
  3. package/dist/index.browser.js +2 -3
  4. package/dist/index.cjs +151 -83
  5. package/dist/index.d.ts +360 -205
  6. package/dist/index.js +129 -62
  7. package/dist/{useActive-DjpZBEuB.cjs → useRouterState-AdK-XeM2.cjs} +270 -81
  8. package/dist/{useActive-BX41CqY8.js → useRouterState-qoMq7Y9J.js} +272 -84
  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/descriptors/$page.ts +139 -30
  16. package/src/errors/RedirectionError.ts +4 -1
  17. package/src/hooks/RouterHookApi.ts +42 -5
  18. package/src/hooks/useAlepha.ts +12 -0
  19. package/src/hooks/useClient.ts +8 -6
  20. package/src/hooks/useInject.ts +2 -2
  21. package/src/hooks/useQueryParams.ts +1 -1
  22. package/src/hooks/useRouter.ts +6 -0
  23. package/src/index.browser.ts +1 -1
  24. package/src/index.shared.ts +11 -5
  25. package/src/index.ts +3 -4
  26. package/src/providers/BrowserRouterProvider.ts +1 -1
  27. package/src/providers/PageDescriptorProvider.ts +72 -21
  28. package/src/providers/ReactBrowserProvider.ts +5 -8
  29. package/src/providers/ReactServerProvider.ts +197 -80
  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-BX41CqY8.js.map +0 -1
  35. package/dist/useActive-DjpZBEuB.cjs.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-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-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,23 +180,25 @@ 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
193
  /**
164
194
  * For testing purposes, creates a render function that can be used.
165
195
  */
166
- createRenderFunction(name) {
196
+ createRenderFunction(name, withIndex = false) {
167
197
  return async (options = {}) => {
168
198
  const page = this.pageDescriptorProvider.page(name);
199
+ const url = new URL(this.pageDescriptorProvider.url(name, options));
169
200
  const context = {
170
- url: new URL("http://localhost"),
201
+ url,
171
202
  params: options.params ?? {},
172
203
  query: options.query ?? {},
173
204
  head: {},
@@ -177,7 +208,18 @@ class ReactServerProvider {
177
208
  page,
178
209
  context
179
210
  );
180
- return renderToString(this.pageDescriptorProvider.root(state, context));
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
+ };
181
223
  };
182
224
  }
183
225
  createHandler(page, templateLoader) {
@@ -197,11 +239,24 @@ class ReactServerProvider {
197
239
  };
198
240
  if (this.alepha.has(ServerLinksProvider)) {
199
241
  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);
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;
205
260
  }
206
261
  await this.alepha.emit(
207
262
  "react:server:render",
@@ -213,49 +268,62 @@ class ReactServerProvider {
213
268
  log: false
214
269
  }
215
270
  );
271
+ this.serverTimingProvider.beginTiming("createLayers");
216
272
  const state = await this.pageDescriptorProvider.createLayers(
217
273
  page,
218
274
  context
219
275
  );
276
+ this.serverTimingProvider.endTiming("createLayers");
220
277
  if (state.redirect) {
221
278
  return reply.redirect(state.redirect);
222
279
  }
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
280
  reply.status = 200;
246
281
  reply.headers["content-type"] = "text/html";
247
282
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
248
283
  reply.headers.pragma = "no-cache";
249
284
  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
- );
285
+ if (page.cache && serverRequest.user) {
286
+ delete context.links;
256
287
  }
257
- return response.html;
288
+ return this.renderToHtml(template, state, context);
289
+ };
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
258
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;
259
327
  }
260
328
  fillTemplate(response, app, script) {
261
329
  if (this.ROOT_DIV_REGEX.test(response.html)) {
@@ -294,4 +362,3 @@ class ReactModule {
294
362
  __bind($page, ReactModule);
295
363
 
296
364
  export { $page, PageDescriptorProvider, ReactModule, ReactServerProvider, envSchema };
297
- //# sourceMappingURL=index.js.map