@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.
- package/README.md +1 -1
- package/dist/index.browser.cjs +21 -21
- package/dist/index.browser.js +2 -3
- package/dist/index.cjs +151 -83
- package/dist/index.d.ts +360 -205
- package/dist/index.js +129 -62
- package/dist/{useActive-DjpZBEuB.cjs → useRouterState-AdK-XeM2.cjs} +270 -81
- package/dist/{useActive-BX41CqY8.js → useRouterState-qoMq7Y9J.js} +272 -84
- package/package.json +11 -10
- package/src/components/ClientOnly.tsx +35 -0
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorViewer.tsx +161 -0
- package/src/components/Link.tsx +9 -3
- package/src/components/NestedView.tsx +18 -3
- package/src/descriptors/$page.ts +139 -30
- package/src/errors/RedirectionError.ts +4 -1
- package/src/hooks/RouterHookApi.ts +42 -5
- package/src/hooks/useAlepha.ts +12 -0
- package/src/hooks/useClient.ts +8 -6
- package/src/hooks/useInject.ts +2 -2
- package/src/hooks/useQueryParams.ts +1 -1
- package/src/hooks/useRouter.ts +6 -0
- package/src/index.browser.ts +1 -1
- package/src/index.shared.ts +11 -5
- package/src/index.ts +3 -4
- package/src/providers/BrowserRouterProvider.ts +1 -1
- package/src/providers/PageDescriptorProvider.ts +72 -21
- package/src/providers/ReactBrowserProvider.ts +5 -8
- package/src/providers/ReactServerProvider.ts +197 -80
- 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-BX41CqY8.js.map +0 -1
- 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 './
|
|
4
|
-
export { E as ErrorBoundary, 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,23 +180,25 @@ 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
|
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
|
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
|