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