@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/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-DjpZBEuB.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,27 +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.ErrorBoundary = useActive.ErrorBoundary;
21
- exports.Link = useActive.Link;
22
- exports.NestedView = useActive.NestedView;
23
- exports.ReactBrowserProvider = useActive.ReactBrowserProvider;
24
- exports.RedirectionError = useActive.RedirectionError;
25
- exports.RouterContext = useActive.RouterContext;
26
- exports.RouterHookApi = useActive.RouterHookApi;
27
- exports.RouterLayerContext = useActive.RouterLayerContext;
28
- exports.useActive = useActive.useActive;
29
- exports.useApi = useActive.useApi;
30
- exports.useClient = useActive.useClient;
31
- exports.useInject = useActive.useInject;
32
- exports.useQueryParams = useActive.useQueryParams;
33
- exports.useRouter = useActive.useRouter;
34
- exports.useRouterEvents = useActive.useRouterEvents;
35
- 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;
36
37
  exports.ReactModule = ReactModule;
37
- //# 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-BX41CqY8.js';
3
- export { E as ErrorBoundary, 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-BX41CqY8.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-DjpZBEuB.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,23 +181,25 @@ 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
194
  /**
165
195
  * For testing purposes, creates a render function that can be used.
166
196
  */
167
- createRenderFunction(name) {
197
+ createRenderFunction(name, withIndex = false) {
168
198
  return async (options = {}) => {
169
199
  const page = this.pageDescriptorProvider.page(name);
200
+ const url = new URL(this.pageDescriptorProvider.url(name, options));
170
201
  const context = {
171
- url: new URL("http://localhost"),
202
+ url,
172
203
  params: options.params ?? {},
173
204
  query: options.query ?? {},
174
205
  head: {},
@@ -178,7 +209,18 @@ class ReactServerProvider {
178
209
  page,
179
210
  context
180
211
  );
181
- return server$1.renderToString(this.pageDescriptorProvider.root(state, context));
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
+ };
182
224
  };
183
225
  }
184
226
  createHandler(page, templateLoader) {
@@ -198,11 +240,24 @@ class ReactServerProvider {
198
240
  };
199
241
  if (this.alepha.has(server.ServerLinksProvider)) {
200
242
  const srv = this.alepha.get(server.ServerLinksProvider);
201
- context.links = await srv.getLinks({
202
- user: serverRequest.user,
203
- authorization: serverRequest.headers.authorization
204
- });
205
- this.alepha.als.set("links", context.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;
206
261
  }
207
262
  await this.alepha.emit(
208
263
  "react:server:render",
@@ -214,49 +269,62 @@ class ReactServerProvider {
214
269
  log: false
215
270
  }
216
271
  );
272
+ this.serverTimingProvider.beginTiming("createLayers");
217
273
  const state = await this.pageDescriptorProvider.createLayers(
218
274
  page,
219
275
  context
220
276
  );
277
+ this.serverTimingProvider.endTiming("createLayers");
221
278
  if (state.redirect) {
222
279
  return reply.redirect(state.redirect);
223
280
  }
224
- const element = this.pageDescriptorProvider.root(state, context);
225
- const app = server$1.renderToString(element);
226
- const hydrationData = {
227
- links: context.links,
228
- layers: state.layers.map((it) => ({
229
- ...it,
230
- error: it.error ? {
231
- ...it.error,
232
- name: it.error.name,
233
- message: it.error.message,
234
- stack: it.error.stack
235
- // TODO: Hide stack in production ?
236
- } : void 0,
237
- index: void 0,
238
- path: void 0,
239
- element: void 0
240
- }))
241
- };
242
- const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
243
- const response = {
244
- html: template
245
- };
246
281
  reply.status = 200;
247
282
  reply.headers["content-type"] = "text/html";
248
283
  reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
249
284
  reply.headers.pragma = "no-cache";
250
285
  reply.headers.expires = "0";
251
- this.fillTemplate(response, app, script);
252
- if (context.head) {
253
- response.html = this.headProvider.renderHead(
254
- response.html,
255
- context.head
256
- );
286
+ if (page.cache && serverRequest.user) {
287
+ delete context.links;
257
288
  }
258
- return response.html;
289
+ return this.renderToHtml(template, state, context);
290
+ };
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
259
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;
260
328
  }
261
329
  fillTemplate(response, app, script) {
262
330
  if (this.ROOT_DIV_REGEX.test(response.html)) {
@@ -289,31 +357,31 @@ class ReactServerProvider {
289
357
  class ReactModule {
290
358
  alepha = core.$inject(core.Alepha);
291
359
  constructor() {
292
- 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);
293
361
  }
294
362
  }
295
- core.__bind(useActive.$page, ReactModule);
363
+ core.__bind(useRouterState.$page, ReactModule);
296
364
 
297
- exports.$page = useActive.$page;
298
- exports.ErrorBoundary = useActive.ErrorBoundary;
299
- exports.Link = useActive.Link;
300
- exports.NestedView = useActive.NestedView;
301
- exports.PageDescriptorProvider = useActive.PageDescriptorProvider;
302
- exports.ReactBrowserProvider = useActive.ReactBrowserProvider;
303
- exports.RedirectionError = useActive.RedirectionError;
304
- exports.RouterContext = useActive.RouterContext;
305
- exports.RouterHookApi = useActive.RouterHookApi;
306
- exports.RouterLayerContext = useActive.RouterLayerContext;
307
- exports.isPageRoute = useActive.isPageRoute;
308
- exports.useActive = useActive.useActive;
309
- exports.useApi = useActive.useApi;
310
- exports.useClient = useActive.useClient;
311
- exports.useInject = useActive.useInject;
312
- exports.useQueryParams = useActive.useQueryParams;
313
- exports.useRouter = useActive.useRouter;
314
- exports.useRouterEvents = useActive.useRouterEvents;
315
- 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;
316
385
  exports.ReactModule = ReactModule;
317
386
  exports.ReactServerProvider = ReactServerProvider;
318
387
  exports.envSchema = envSchema;
319
- //# sourceMappingURL=index.cjs.map