@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.
- package/README.md +1 -1
- package/dist/index.browser.cjs +21 -20
- package/dist/index.browser.js +2 -3
- package/dist/index.cjs +168 -82
- package/dist/index.d.ts +415 -232
- package/dist/index.js +146 -62
- package/dist/{useActive-4QlZKGbw.cjs → useRouterState-AdK-XeM2.cjs} +358 -170
- package/dist/{useActive-ClUsghB5.js → useRouterState-qoMq7Y9J.js} +358 -172
- package/package.json +11 -10
- package/src/components/ClientOnly.tsx +35 -0
- package/src/components/ErrorBoundary.tsx +72 -0
- package/src/components/ErrorViewer.tsx +161 -0
- package/src/components/Link.tsx +10 -4
- package/src/components/NestedView.tsx +28 -4
- package/src/descriptors/$page.ts +143 -38
- package/src/errors/RedirectionError.ts +4 -1
- package/src/hooks/RouterHookApi.ts +58 -35
- package/src/hooks/useAlepha.ts +12 -0
- package/src/hooks/useClient.ts +8 -6
- package/src/hooks/useInject.ts +3 -9
- package/src/hooks/useQueryParams.ts +4 -7
- package/src/hooks/useRouter.ts +6 -0
- package/src/index.browser.ts +1 -1
- package/src/index.shared.ts +11 -4
- package/src/index.ts +7 -4
- package/src/providers/BrowserRouterProvider.ts +27 -33
- package/src/providers/PageDescriptorProvider.ts +90 -40
- package/src/providers/ReactBrowserProvider.ts +21 -27
- package/src/providers/ReactServerProvider.ts +215 -77
- package/dist/index.browser.cjs.map +0 -1
- package/dist/index.browser.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/useActive-4QlZKGbw.cjs.map +0 -1
- 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 './
|
|
4
|
-
export { L as Link, N as NestedView, l as ReactBrowserProvider,
|
|
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: "
|
|
77
|
+
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
79
78
|
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
80
|
-
REACT_SSR_ENABLED: t.boolean(
|
|
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
|
-
|
|
95
|
+
onConfigure = $hook({
|
|
96
96
|
name: "configure",
|
|
97
97
|
handler: async () => {
|
|
98
98
|
const pages = this.alepha.getDescriptorValues($page);
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(),
|
|
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
|
-
|
|
183
|
+
async configureVite(ssrEnabled) {
|
|
184
|
+
if (!ssrEnabled) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
156
187
|
this.log.info("SSR (vite) OK");
|
|
157
|
-
|
|
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(
|
|
190
|
+
() => fetch(`${url}/index.html`).then((it) => it.text()).catch(() => void 0)
|
|
161
191
|
);
|
|
162
192
|
}
|
|
163
|
-
|
|
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
|
|
167
|
-
|
|
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
|
-
|
|
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
|
|
232
|
+
const context = {
|
|
183
233
|
url,
|
|
184
234
|
params,
|
|
185
235
|
query,
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
this.alepha.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
response.html = this.headProvider.renderHead(response.html, state.head);
|
|
285
|
+
if (page.cache && serverRequest.user) {
|
|
286
|
+
delete context.links;
|
|
239
287
|
}
|
|
240
|
-
return
|
|
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
|