5htp-core 0.4.4 → 0.4.5
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/package.json +1 -1
- package/src/client/assets/css/components/button.less +32 -24
- package/src/client/assets/css/text/text.less +0 -5
- package/src/client/components/inputv3/index.tsx +1 -1
- package/src/client/services/router/components/router.tsx +59 -37
- package/src/client/services/router/index.tsx +20 -16
- package/src/client/services/router/request/api.ts +3 -2
- package/src/client/services/router/request/index.ts +0 -1
- package/src/common/errors/index.tsx +48 -29
- package/src/common/router/index.ts +20 -0
- package/src/common/router/request/index.ts +1 -0
- package/src/common/router/response/page.ts +0 -1
- package/src/server/services/router/index.ts +17 -20
- package/src/server/services/router/request/api.ts +1 -2
- package/src/server/services/router/request/index.ts +3 -2
- package/src/server/services/router/response/page/document.tsx +9 -19
- package/src/server/services/router/response/page/index.tsx +2 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5htp-core",
|
|
3
3
|
"description": "Convenient TypeScript framework designed for Performance and Productivity.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
// Hover
|
|
36
36
|
//transition: all .5s linear;
|
|
37
37
|
&:hover,
|
|
38
|
-
&.active,
|
|
39
38
|
li:hover > & {
|
|
40
39
|
|
|
41
40
|
color: var(--cTxtImportant);
|
|
@@ -46,6 +45,38 @@
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
48
|
+
&.active {
|
|
49
|
+
&::after {
|
|
50
|
+
content: ' ';
|
|
51
|
+
display: block;
|
|
52
|
+
position: absolute;
|
|
53
|
+
|
|
54
|
+
background: @c1;
|
|
55
|
+
height: @sizeActiveIndicator;
|
|
56
|
+
width: @sizeActiveIndicator;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
|
|
59
|
+
// Default: bottom
|
|
60
|
+
left: 50%;
|
|
61
|
+
margin-left: -@sizeActiveIndicator / 2;
|
|
62
|
+
bottom: -@sizeActiveIndicator / 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.col > &::after,
|
|
66
|
+
.col > li > &::after {
|
|
67
|
+
|
|
68
|
+
// Reset potition
|
|
69
|
+
left: auto;
|
|
70
|
+
margin-left: auto;
|
|
71
|
+
bottom: auto;
|
|
72
|
+
|
|
73
|
+
// Position right
|
|
74
|
+
top: 50%;
|
|
75
|
+
margin-top: -@sizeActiveIndicator / 2;
|
|
76
|
+
right: -@sizeActiveIndicator / 2;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
49
80
|
// Click
|
|
50
81
|
&.pressed {
|
|
51
82
|
transform: scale(0.9);
|
|
@@ -129,29 +160,6 @@
|
|
|
129
160
|
&.col {
|
|
130
161
|
box-shadow: 0 0 0 0.2em @c2;
|
|
131
162
|
}
|
|
132
|
-
|
|
133
|
-
.menu &::after {
|
|
134
|
-
content: ' ';
|
|
135
|
-
display: block;
|
|
136
|
-
position: absolute;
|
|
137
|
-
|
|
138
|
-
background: @c1;
|
|
139
|
-
height: @sizeActiveIndicator;
|
|
140
|
-
width: @sizeActiveIndicator;
|
|
141
|
-
border-radius: 50%;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.menu.row &::after {
|
|
145
|
-
left: 50%;
|
|
146
|
-
margin-left: -@sizeActiveIndicator / 2;
|
|
147
|
-
bottom: -@sizeActiveIndicator / 2;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.menu.col &::after {
|
|
151
|
-
top: 50%;
|
|
152
|
-
margin-top: -@sizeActiveIndicator / 2;
|
|
153
|
-
right: -@sizeActiveIndicator / 2;
|
|
154
|
-
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
&[disabled] {
|
|
@@ -23,25 +23,34 @@ export type PropsPage<TParams extends { [cle: string]: unknown }> = TParams & {
|
|
|
23
23
|
data: {[cle: string]: unknown}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type TProps = {
|
|
27
|
+
service?: ClientRouter,
|
|
28
|
+
loaderComponent?: React.ComponentType<{ isLoading: boolean }>,
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/*----------------------------------
|
|
27
32
|
- PAGE STATE
|
|
28
33
|
----------------------------------*/
|
|
29
34
|
|
|
30
35
|
const LogPrefix = `[router][component]`
|
|
31
36
|
|
|
32
|
-
const PageLoading = ({ clientRouter
|
|
37
|
+
const PageLoading = ({ clientRouter, loaderComponent: LoaderComponent }: {
|
|
38
|
+
clientRouter?: ClientRouter,
|
|
39
|
+
loaderComponent?: React.ComponentType<{ isLoading: boolean }>,
|
|
40
|
+
}) => {
|
|
33
41
|
|
|
34
42
|
const [isLoading, setLoading] = React.useState(false);
|
|
35
43
|
|
|
36
44
|
if (clientRouter)
|
|
37
45
|
clientRouter.setLoading = setLoading;
|
|
38
46
|
|
|
39
|
-
return
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
return LoaderComponent
|
|
48
|
+
? <LoaderComponent isLoading={isLoading} />
|
|
49
|
+
: (
|
|
50
|
+
<div id="loading" class={isLoading ? 'display' : ''}>
|
|
51
|
+
<i src="spin" />
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
const scrollToElement = (selector: string) => document.querySelector( selector )
|
|
@@ -54,7 +63,7 @@ const scrollToElement = (selector: string) => document.querySelector( selector )
|
|
|
54
63
|
/*----------------------------------
|
|
55
64
|
- COMPONENT
|
|
56
65
|
----------------------------------*/
|
|
57
|
-
export default ({ service: clientRouter }:
|
|
66
|
+
export default ({ service: clientRouter, loaderComponent }: TProps) => {
|
|
58
67
|
|
|
59
68
|
/*----------------------------------
|
|
60
69
|
- INIT
|
|
@@ -62,20 +71,18 @@ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
|
|
|
62
71
|
|
|
63
72
|
const context = useContext();
|
|
64
73
|
|
|
74
|
+
const [currentPage, setCurrentPage] = React.useState<undefined | Page>(context.page);
|
|
75
|
+
|
|
65
76
|
// Bind context object to client router
|
|
66
|
-
if (clientRouter !== undefined)
|
|
77
|
+
if (clientRouter !== undefined) {
|
|
67
78
|
clientRouter.context = context;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
current: undefined | Page
|
|
71
|
-
}>({
|
|
72
|
-
current: context.page
|
|
73
|
-
});
|
|
79
|
+
clientRouter.navigate = changePage;
|
|
80
|
+
}
|
|
74
81
|
|
|
75
82
|
/*----------------------------------
|
|
76
83
|
- ACTIONS
|
|
77
84
|
----------------------------------*/
|
|
78
|
-
const resolvePage = async (request: ClientRequest,
|
|
85
|
+
const resolvePage = async (request: ClientRequest, data: {} = {}) => {
|
|
79
86
|
|
|
80
87
|
if (!clientRouter) return;
|
|
81
88
|
|
|
@@ -85,42 +92,57 @@ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
|
|
|
85
92
|
// WARNING: Don"t try to play with pages here, since the object will not be updated
|
|
86
93
|
// If needed to play with pages, do it in the setPages callback below
|
|
87
94
|
// Unchanged path
|
|
88
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
request.path === currentRequest.path
|
|
97
|
+
&&
|
|
98
|
+
request.hash !== currentRequest.hash
|
|
99
|
+
&&
|
|
100
|
+
request.hash !== undefined
|
|
101
|
+
) {
|
|
89
102
|
scrollToElement(request.hash);
|
|
90
103
|
return;
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
// Set loading state
|
|
107
|
+
clientRouter.runHook('page.change', request);
|
|
108
|
+
window.scrollTo({
|
|
109
|
+
top: 0,
|
|
110
|
+
behavior: 'smooth'
|
|
111
|
+
});
|
|
94
112
|
clientRouter.setLoading(true);
|
|
95
113
|
const newpage = context.page = await clientRouter.resolve(request);
|
|
96
114
|
|
|
97
|
-
// Page not found: Directly load with the browser
|
|
98
|
-
if (newpage === undefined) {
|
|
99
|
-
window.location.replace(request.url);
|
|
100
|
-
console.error("not found");
|
|
101
|
-
return;
|
|
102
115
|
// Unable to load (no connection, server error, ....)
|
|
103
|
-
|
|
116
|
+
if (newpage === null) {
|
|
104
117
|
return;
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
return await changePage(newpage, data, request);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function changePage(newpage: Page, data?: {}, request?: ClientRequest) {
|
|
124
|
+
|
|
107
125
|
// Fetch API data to hydrate the page
|
|
108
126
|
try {
|
|
109
127
|
await newpage.preRender();
|
|
110
128
|
} catch (error) {
|
|
111
129
|
console.error(LogPrefix, "Unable to fetch data:", error);
|
|
112
|
-
clientRouter
|
|
130
|
+
clientRouter?.setLoading(false);
|
|
113
131
|
return;
|
|
114
132
|
}
|
|
115
133
|
|
|
134
|
+
// Add additional data
|
|
135
|
+
if (data)
|
|
136
|
+
newpage.data = { ...newpage.data, ...data };
|
|
137
|
+
|
|
116
138
|
// Add page container
|
|
117
|
-
|
|
139
|
+
setCurrentPage( page => {
|
|
118
140
|
|
|
119
141
|
// WARN: Don't cancel navigation if same page as before, as we already instanciated the new page and bound the context with it
|
|
120
142
|
// Otherwise it would cause reference issues (ex: page.setAllData makes ref to the new context)
|
|
121
143
|
|
|
122
144
|
// If if the layout changed
|
|
123
|
-
const curLayout =
|
|
145
|
+
const curLayout = currentPage?.layout;
|
|
124
146
|
const newLayout = newpage?.layout;
|
|
125
147
|
if (newLayout && curLayout && newLayout.path !== curLayout.path) {
|
|
126
148
|
|
|
@@ -129,13 +151,13 @@ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
|
|
|
129
151
|
// But when we call setLayout, the style of the previous layout are still oaded and applied
|
|
130
152
|
// Find a way to unload the previous layout / page resources before to load the new one
|
|
131
153
|
console.log(LogPrefix, `Changing layout. Before:`, curLayout, 'New layout:', newLayout);
|
|
132
|
-
window.location.replace(request.url);
|
|
133
|
-
return { ...
|
|
154
|
+
window.location.replace( request ? request.url : location.href );
|
|
155
|
+
return { ...page }
|
|
134
156
|
|
|
135
157
|
context.app.setLayout(newLayout);
|
|
136
158
|
}
|
|
137
159
|
|
|
138
|
-
return
|
|
160
|
+
return newpage;
|
|
139
161
|
});
|
|
140
162
|
}
|
|
141
163
|
|
|
@@ -169,28 +191,28 @@ export default ({ service: clientRouter }: { service?: ClientRouter }) => {
|
|
|
169
191
|
// Reset scroll
|
|
170
192
|
window.scrollTo(0, 0);
|
|
171
193
|
// Should be called AFTER rendering the page (so after the state change)
|
|
172
|
-
|
|
194
|
+
currentPage?.updateClient();
|
|
173
195
|
// Scroll to the selected content via url hash
|
|
174
|
-
restoreScroll(
|
|
196
|
+
restoreScroll(currentPage);
|
|
175
197
|
|
|
176
198
|
// Hooks
|
|
177
|
-
clientRouter.runHook('page.changed',
|
|
199
|
+
clientRouter.runHook('page.changed', currentPage)
|
|
178
200
|
|
|
179
|
-
}, [
|
|
201
|
+
}, [currentPage]);
|
|
180
202
|
|
|
181
203
|
/*----------------------------------
|
|
182
204
|
- RENDER
|
|
183
205
|
----------------------------------*/
|
|
184
206
|
// Render the page component
|
|
185
207
|
return <>
|
|
186
|
-
{
|
|
187
|
-
<PageComponent page={
|
|
208
|
+
{currentPage && (
|
|
209
|
+
<PageComponent page={currentPage}
|
|
188
210
|
/* Create a new instance of the Page component every time the page change
|
|
189
211
|
Otherwise the page will memorise the data of the previous page */
|
|
190
|
-
key={
|
|
212
|
+
key={currentPage.chunkId === undefined ? undefined : 'page_' + currentPage.chunkId}
|
|
191
213
|
/>
|
|
192
214
|
)}
|
|
193
215
|
|
|
194
|
-
<PageLoading clientRouter={clientRouter} />
|
|
216
|
+
<PageLoading clientRouter={clientRouter} loaderComponent={loaderComponent} />
|
|
195
217
|
</>
|
|
196
218
|
}
|
|
@@ -17,7 +17,7 @@ import type { TBasicSSrData } from '@server/services/router/response';
|
|
|
17
17
|
import BaseRouter, {
|
|
18
18
|
defaultOptions, TRoute, TErrorRoute,
|
|
19
19
|
TClientOrServerContext, TRouteModule,
|
|
20
|
-
buildUrl, TDomainsList
|
|
20
|
+
matchRoute, buildUrl, TDomainsList
|
|
21
21
|
} from '@common/router'
|
|
22
22
|
import { getLayout } from '@common/router/layouts';
|
|
23
23
|
import { getRegisterPageArgs, buildRegex } from '@common/router/register';
|
|
@@ -123,7 +123,7 @@ export type TRoutesLoaders = {
|
|
|
123
123
|
|
|
124
124
|
export type THookCallback<TRouter extends ClientRouter> = (request: ClientRequest<TRouter>) => void;
|
|
125
125
|
|
|
126
|
-
type THookName = '
|
|
126
|
+
type THookName = 'page.change' | 'page.changed'
|
|
127
127
|
|
|
128
128
|
type Config<TAdditionnalContext extends {} = {}> = {
|
|
129
129
|
preload: string[], // List of globs
|
|
@@ -145,6 +145,7 @@ export default class ClientRouter<
|
|
|
145
145
|
public context!: ClientContext;
|
|
146
146
|
|
|
147
147
|
public setLoading!: React.Dispatch< React.SetStateAction<boolean> >;
|
|
148
|
+
public navigate!: (page: ClientPage, data?: {}) => void;
|
|
148
149
|
|
|
149
150
|
public constructor(app: TApplication, config: Config<TAdditionnalContext>) {
|
|
150
151
|
|
|
@@ -161,10 +162,18 @@ export default class ClientRouter<
|
|
|
161
162
|
public url = (path: string, params: {} = {}, absolute: boolean = true) =>
|
|
162
163
|
buildUrl(path, params, this.domains, absolute);
|
|
163
164
|
|
|
164
|
-
public go( url: string, data: {} = {}, opt: {
|
|
165
|
+
public go( url: string | number, data: {} = {}, opt: {
|
|
165
166
|
newTab?: boolean
|
|
166
167
|
} = {}) {
|
|
167
168
|
|
|
169
|
+
// Error code
|
|
170
|
+
if (typeof url === 'number') {
|
|
171
|
+
this.createResponse( this.errors[url], this.context.request ).then(( page ) => {
|
|
172
|
+
this.navigate(page, data);
|
|
173
|
+
})
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
168
177
|
url = this.url(url, data, false);
|
|
169
178
|
|
|
170
179
|
if (opt.newTab)
|
|
@@ -304,10 +313,9 @@ export default class ClientRouter<
|
|
|
304
313
|
/*----------------------------------
|
|
305
314
|
- RESOLUTION
|
|
306
315
|
----------------------------------*/
|
|
307
|
-
public async resolve(request: ClientRequest<this>): Promise<ClientPage
|
|
316
|
+
public async resolve(request: ClientRequest<this>): Promise<ClientPage> {
|
|
308
317
|
|
|
309
318
|
debug && console.log(LogPrefix, 'Resolving request', request.path, Object.keys(request.data));
|
|
310
|
-
this.runHook('location.change', request);
|
|
311
319
|
|
|
312
320
|
for (let iRoute = 0; iRoute < this.routes.length; iRoute++) {
|
|
313
321
|
|
|
@@ -315,17 +323,10 @@ export default class ClientRouter<
|
|
|
315
323
|
if (!('regex' in route))
|
|
316
324
|
continue;
|
|
317
325
|
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
326
|
+
const isMatching = matchRoute(route, request);
|
|
327
|
+
if (!isMatching)
|
|
320
328
|
continue;
|
|
321
329
|
|
|
322
|
-
// URL data
|
|
323
|
-
for (let iKey = 0; iKey < route.keys.length; iKey++) {
|
|
324
|
-
const nomParam = route.keys[iKey];
|
|
325
|
-
if (typeof nomParam === 'string') // number = sans nom
|
|
326
|
-
request.data[nomParam] = match[iKey + 1]
|
|
327
|
-
}
|
|
328
|
-
|
|
329
330
|
// Create response
|
|
330
331
|
debug && console.log(LogPrefix, 'Resolved request', request.path, '| Route:', route);
|
|
331
332
|
const page = await this.createResponse(route, request);
|
|
@@ -334,7 +335,10 @@ export default class ClientRouter<
|
|
|
334
335
|
|
|
335
336
|
};
|
|
336
337
|
|
|
337
|
-
|
|
338
|
+
console.log("404 error page not found.", this.errors, this.routes);
|
|
339
|
+
|
|
340
|
+
const notFoundRoute = this.errors[404];
|
|
341
|
+
return await this.createResponse(notFoundRoute, request);
|
|
338
342
|
}
|
|
339
343
|
|
|
340
344
|
private async load(route: TUnresolvedNormalRoute): Promise<TRoute>;
|
|
@@ -415,7 +419,7 @@ export default class ClientRouter<
|
|
|
415
419
|
}
|
|
416
420
|
|
|
417
421
|
private async createResponse(
|
|
418
|
-
route: TUnresolvedRoute | TRoute,
|
|
422
|
+
route: TUnresolvedRoute | TErrorRoute | TRoute,
|
|
419
423
|
request: ClientRequest<this>,
|
|
420
424
|
pageData: {} = {}
|
|
421
425
|
): Promise<ClientPage> {
|
|
@@ -9,7 +9,7 @@ import ApiClientService, {
|
|
|
9
9
|
TApiFetchOptions, TFetcherList, TFetcherArgs, TFetcher,
|
|
10
10
|
TDataReturnedByFetchers
|
|
11
11
|
} from '@common/router/request/api';
|
|
12
|
-
import {
|
|
12
|
+
import { fromJson as errorFromJson, NetworkError } from '@common/errors';
|
|
13
13
|
import type ClientApplication from '@client/app';
|
|
14
14
|
|
|
15
15
|
import { toMultipart } from './multipart';
|
|
@@ -232,9 +232,10 @@ export default class ApiClient implements ApiClientService {
|
|
|
232
232
|
return fetch(url, config)
|
|
233
233
|
.then(async (response) => {
|
|
234
234
|
if (!response.ok) {
|
|
235
|
+
|
|
235
236
|
const errorData = await response.json();
|
|
236
237
|
console.warn(`[api] Failure:`, response.status, errorData);
|
|
237
|
-
const error =
|
|
238
|
+
const error = errorFromJson(errorData);
|
|
238
239
|
throw error;
|
|
239
240
|
}
|
|
240
241
|
debug && console.log(`[api] Success:`, response);
|
|
@@ -7,16 +7,17 @@ import type { ComponentChild } from 'preact';
|
|
|
7
7
|
|
|
8
8
|
export type TListeErreursSaisie<TClesDonnees extends string = string> = {[champ in TClesDonnees]: string[]}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
type TJsonError = {
|
|
11
11
|
code: number,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
origin?: string,
|
|
13
|
+
message: string,
|
|
14
|
+
// Form fields
|
|
15
|
+
errors?: TListeErreursSaisie
|
|
16
|
+
} & TDetailsErreur
|
|
15
17
|
|
|
16
18
|
type TDetailsErreur = {
|
|
17
19
|
stack?: string,
|
|
18
|
-
|
|
19
|
-
urlRequete?: string,
|
|
20
|
+
origin?: string,
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
/*----------------------------------
|
|
@@ -49,9 +50,7 @@ export abstract class CoreError extends Error {
|
|
|
49
50
|
public abstract http: number;
|
|
50
51
|
public title: string = "Uh Oh ...";
|
|
51
52
|
public message: string;
|
|
52
|
-
|
|
53
|
-
public urlRequete?: string;
|
|
54
|
-
public idRapport?: string;
|
|
53
|
+
public details: TDetailsErreur = {};
|
|
55
54
|
|
|
56
55
|
// Note: On ne le redéfini pas ici, car déjà présent dans Error
|
|
57
56
|
// La redéfinition reset la valeur du stacktrace
|
|
@@ -62,25 +61,20 @@ export abstract class CoreError extends Error {
|
|
|
62
61
|
super(message);
|
|
63
62
|
|
|
64
63
|
this.message = message || (this.constructor as typeof CoreError).msgDefaut;
|
|
64
|
+
this.details = details || {};
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Inject stack
|
|
67
|
+
if (details !== undefined)
|
|
68
68
|
this.stack = details.stack;
|
|
69
|
-
this.urlRequete = details.urlRequete;
|
|
70
|
-
|
|
71
|
-
if (this.urlRequete !== undefined)
|
|
72
|
-
this.message + '(' + this.urlRequete + ') ' + this.message;
|
|
73
|
-
}
|
|
74
69
|
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
public json():
|
|
72
|
+
public json(): TJsonError {
|
|
78
73
|
|
|
79
74
|
return {
|
|
80
75
|
code: this.http,
|
|
81
76
|
message: this.message,
|
|
82
|
-
|
|
83
|
-
urlRequete: this.urlRequete,
|
|
77
|
+
...this.details
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
|
|
@@ -116,7 +110,7 @@ export class InputErrorSchema extends CoreError {
|
|
|
116
110
|
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
public json():
|
|
113
|
+
public json(): TJsonError {
|
|
120
114
|
return {
|
|
121
115
|
...super.json(),
|
|
122
116
|
errors: this.errors,
|
|
@@ -182,21 +176,46 @@ export class NetworkError extends Error {
|
|
|
182
176
|
|
|
183
177
|
export const viaHttpCode = (
|
|
184
178
|
code: number,
|
|
185
|
-
message
|
|
179
|
+
message: string,
|
|
186
180
|
details?: TDetailsErreur
|
|
187
181
|
): CoreError => {
|
|
182
|
+
return fromJson({
|
|
183
|
+
code,
|
|
184
|
+
message,
|
|
185
|
+
...details
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const toJson = (e: Error | CoreError): TJsonError => {
|
|
190
|
+
|
|
191
|
+
if (('json' in e) && typeof e.json === 'function')
|
|
192
|
+
return e.json();
|
|
193
|
+
|
|
194
|
+
const details = ('details' in e)
|
|
195
|
+
? e.details
|
|
196
|
+
: { stack: e.stack };
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
return { code: 500, message: e.message, ...details }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export const fromJson = ({ code, message, ...details }: TJsonError) => {
|
|
192
202
|
|
|
193
203
|
switch (code) {
|
|
194
|
-
case 400:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
case 400:
|
|
205
|
+
if (details.errors)
|
|
206
|
+
return new InputErrorSchema( details.errors, details );
|
|
207
|
+
else
|
|
208
|
+
return new InputError( message, details );
|
|
209
|
+
|
|
210
|
+
case 401: return new AuthRequired( message, details );
|
|
211
|
+
|
|
212
|
+
case 403: return new Forbidden( message, details );
|
|
213
|
+
|
|
214
|
+
case 404: return new NotFound( message, details );
|
|
215
|
+
|
|
216
|
+
default: return new Anomaly( message, details );
|
|
199
217
|
}
|
|
218
|
+
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
export default CoreError;
|
|
@@ -15,6 +15,8 @@ import type {
|
|
|
15
15
|
TRouteHttpMethod
|
|
16
16
|
} from '@server/services/router';
|
|
17
17
|
|
|
18
|
+
import type RouterRequest from './request';
|
|
19
|
+
|
|
18
20
|
import type { TUserRole } from '@server/services/auth';
|
|
19
21
|
|
|
20
22
|
import type { TAppArrowFunction } from '@common/app';
|
|
@@ -173,6 +175,24 @@ export const buildUrl = (
|
|
|
173
175
|
return prefix + path + (searchParams.toString() ? '?' + searchParams.toString() : '');
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
export const matchRoute = (route: TRoute, request: RouterRequest) => {
|
|
179
|
+
|
|
180
|
+
// Match Path
|
|
181
|
+
const match = route.regex.exec(request.path);
|
|
182
|
+
if (!match)
|
|
183
|
+
return false;
|
|
184
|
+
|
|
185
|
+
// Extract URL params
|
|
186
|
+
for (let iKey = 0; iKey < route.keys.length; iKey++) {
|
|
187
|
+
const key = route.keys[iKey];
|
|
188
|
+
const value = match[iKey + 1];
|
|
189
|
+
if (typeof key === 'string' && value) // number = sans nom
|
|
190
|
+
request.data[key] = decodeURIComponent( value.replaceAll('+', '%20') );
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
176
196
|
/*----------------------------------
|
|
177
197
|
- BASE ROUTER
|
|
178
198
|
----------------------------------*/
|
|
@@ -21,11 +21,11 @@ import Service, { AnyService } from '@server/app/service';
|
|
|
21
21
|
import type { TRegisteredServicesIndex } from '@server/app/service/container';
|
|
22
22
|
import context from '@server/context';
|
|
23
23
|
import type DisksManager from '@server/services/disks';
|
|
24
|
-
import { CoreError, NotFound } from '@common/errors';
|
|
24
|
+
import { CoreError, NotFound, toJson as errorToJson } from '@common/errors';
|
|
25
25
|
import BaseRouter, {
|
|
26
26
|
TRoute, TErrorRoute, TRouteModule,
|
|
27
27
|
TRouteOptions, defaultOptions,
|
|
28
|
-
buildUrl, TDomainsList
|
|
28
|
+
matchRoute, buildUrl, TDomainsList
|
|
29
29
|
} from '@common/router';
|
|
30
30
|
import { buildRegex, getRegisterPageArgs } from '@common/router/register';
|
|
31
31
|
import { layoutsList, getLayout } from '@common/router/layouts';
|
|
@@ -523,19 +523,10 @@ declare type Routes = {
|
|
|
523
523
|
if (!request.accepts(route.options.accept))
|
|
524
524
|
continue;
|
|
525
525
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (!match)
|
|
526
|
+
const isMatching = matchRoute(route, request);
|
|
527
|
+
if (!isMatching)
|
|
529
528
|
continue;
|
|
530
529
|
|
|
531
|
-
// Extract URL params
|
|
532
|
-
for (let iKey = 0; iKey < route.keys.length; iKey++) {
|
|
533
|
-
const key = route.keys[iKey];
|
|
534
|
-
const value = match[iKey + 1];
|
|
535
|
-
if (typeof key === 'string' && value) // number = sans nom
|
|
536
|
-
request.data[key] = decodeURIComponent(value);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
530
|
// Run on resolution hooks. Ex: authentication check
|
|
540
531
|
await this.runHook('resolved', route);
|
|
541
532
|
|
|
@@ -552,7 +543,11 @@ declare type Routes = {
|
|
|
552
543
|
|
|
553
544
|
if (this.app.env.profile === 'dev') {
|
|
554
545
|
console.log('API batch error:', request.method, request.path, error);
|
|
555
|
-
|
|
546
|
+
const errOrigin = request.method + ' ' + request.path;
|
|
547
|
+
if (error.details === undefined)
|
|
548
|
+
error.details = { origin: errOrigin }
|
|
549
|
+
else
|
|
550
|
+
error.details.origin = errOrigin;
|
|
556
551
|
}
|
|
557
552
|
|
|
558
553
|
throw error;
|
|
@@ -609,14 +604,16 @@ declare type Routes = {
|
|
|
609
604
|
} else if (code !== 404 && this.app.env.profile === "dev")
|
|
610
605
|
console.warn(e);
|
|
611
606
|
|
|
612
|
-
|
|
607
|
+
// Return error based on the request format
|
|
608
|
+
if (request.accepts("html")) {
|
|
609
|
+
const jsonError = errorToJson(e);
|
|
613
610
|
await response.runController(route, {
|
|
614
|
-
|
|
615
|
-
type: e.constructor.name
|
|
611
|
+
error: jsonError
|
|
616
612
|
});
|
|
617
|
-
else if (request.accepts("json"))
|
|
618
|
-
|
|
619
|
-
|
|
613
|
+
} else if (request.accepts("json")) {
|
|
614
|
+
const jsonError = errorToJson(e);
|
|
615
|
+
await response.json(jsonError);
|
|
616
|
+
} else
|
|
620
617
|
await response.text(e.message);
|
|
621
618
|
|
|
622
619
|
return response;
|
|
@@ -89,8 +89,7 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
89
89
|
continue;
|
|
90
90
|
|
|
91
91
|
// Create a children request to resolve the api data
|
|
92
|
-
const
|
|
93
|
-
const request = this.request.children(method, path, data, { ...internalHeaders/*, ...headers*/ });
|
|
92
|
+
const request = this.request.children(method, path, data);
|
|
94
93
|
fetchedData[id] = await request.router.resolve(request).then(res => res.data);
|
|
95
94
|
}
|
|
96
95
|
|
|
@@ -87,6 +87,7 @@ export default class ServerRequest<
|
|
|
87
87
|
this.router = router;
|
|
88
88
|
this.api = new ApiClient(this);
|
|
89
89
|
|
|
90
|
+
this.url = this.req.url;
|
|
90
91
|
this.host = this.req.get('host') as string;
|
|
91
92
|
this.method = method;
|
|
92
93
|
this.headers = headers || {};
|
|
@@ -99,9 +100,9 @@ export default class ServerRequest<
|
|
|
99
100
|
this.data = data || {};
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
public children(method: HttpMethod, path: string, data: TObjetDonnees | undefined
|
|
103
|
+
public children(method: HttpMethod, path: string, data: TObjetDonnees | undefined) {
|
|
103
104
|
const children = new ServerRequest(
|
|
104
|
-
this.id, method, path, data, { ...
|
|
105
|
+
this.id, method, path, data, { ...this.headers, accept: 'application/json' },
|
|
105
106
|
this.res, this.router, true
|
|
106
107
|
);
|
|
107
108
|
children.user = this.user;
|
|
@@ -57,31 +57,26 @@ export default class DocumentRenderer<TRouter extends Router> {
|
|
|
57
57
|
|
|
58
58
|
public async page( html: string, page: Page, response: ServerResponse<TRouter> ) {
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// TODO: can be customized via page / route config
|
|
61
|
+
const canonicalUrl = response.request.req.url;
|
|
61
62
|
|
|
62
63
|
let attrsBody = {
|
|
63
64
|
className: [...page.bodyClass].join(' '),
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
return '<!doctype html>' + renderToString(
|
|
67
|
-
<html lang="en"
|
|
68
|
+
<html lang="en">
|
|
68
69
|
<head>
|
|
69
70
|
{/* Format */}
|
|
70
71
|
<meta charSet="utf-8" />
|
|
71
|
-
{page.amp && ( // As a best practice, you should include the script as early as possible in the <head>.
|
|
72
|
-
<script async={true} src="https://cdn.ampproject.org/v0.js"></script>
|
|
73
|
-
)}
|
|
74
72
|
<meta content="IE=edge" httpEquiv="X-UA-Compatible" />
|
|
75
73
|
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1" />
|
|
76
|
-
{!page.amp && page.amp && (
|
|
77
|
-
<link rel="amphtml" href={fullUrl + '/amp'} />
|
|
78
|
-
)}
|
|
79
74
|
|
|
80
75
|
{/* Basique*/}
|
|
81
76
|
<meta content={this.app.identity.web.title} name="apple-mobile-web-app-title" />
|
|
82
77
|
<title>{page.title}</title>
|
|
83
78
|
<meta content={page.description} name="description" />
|
|
84
|
-
<link rel="canonical" href={
|
|
79
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
85
80
|
|
|
86
81
|
{this.metas( page )}
|
|
87
82
|
|
|
@@ -147,9 +142,6 @@ export default class DocumentRenderer<TRouter extends Router> {
|
|
|
147
142
|
</> : <>
|
|
148
143
|
<style id={style.id} dangerouslySetInnerHTML={{ __html: style.inline }} />
|
|
149
144
|
</>)}
|
|
150
|
-
|
|
151
|
-
{/* Sera remplacé par la chaine exacte après renderToStaticMarkup */}
|
|
152
|
-
{page.amp && (<style amp-boilerplate=""></style>)}
|
|
153
145
|
</>
|
|
154
146
|
}
|
|
155
147
|
|
|
@@ -161,13 +153,11 @@ export default class DocumentRenderer<TRouter extends Router> {
|
|
|
161
153
|
|
|
162
154
|
return <>
|
|
163
155
|
{/* JS */}
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}} />
|
|
170
|
-
)}
|
|
156
|
+
<script type="text/javascript" dangerouslySetInnerHTML={{
|
|
157
|
+
__html: `window.ssr=${context}; window.routes=${routesForClient};` + (
|
|
158
|
+
this.app.env.profile === 'dev' ? 'window.dev = true;' : ''
|
|
159
|
+
)
|
|
160
|
+
}} />
|
|
171
161
|
|
|
172
162
|
<link rel="preload" href={"/public/client.js?v=" + BUILD_ID} as="script" />
|
|
173
163
|
<script defer type="text/javascript" src={"/public/client.js?v=" + BUILD_ID} />
|
|
@@ -83,11 +83,7 @@ export default class Page<TRouter extends Router = Router> extends PageResponse<
|
|
|
83
83
|
attrsBody.className += ' ' + page.classeBody.join(' ');
|
|
84
84
|
|
|
85
85
|
if (page.theme)
|
|
86
|
-
attrsBody.className += ' ' + page.theme
|
|
87
|
-
|
|
88
|
-
// L'url canonique doit pointer vers la version html
|
|
89
|
-
if (page.amp && fullUrl.endsWith('/amp'))
|
|
90
|
-
fullUrl = fullUrl.substring(0, fullUrl.length - 4);*/
|
|
86
|
+
attrsBody.className += ' ' + page.theme;*/
|
|
91
87
|
|
|
92
88
|
return this.router.render.page(html, this, this.context.response);
|
|
93
89
|
}
|
|
@@ -113,9 +109,7 @@ export default class Page<TRouter extends Router = Router> extends PageResponse<
|
|
|
113
109
|
id: chunk,
|
|
114
110
|
url: '/public/' + asset
|
|
115
111
|
})
|
|
116
|
-
|
|
117
|
-
// Sauf si mode dev, car le hot reload est quand même bien pratique ...
|
|
118
|
-
else if (!this.amp)
|
|
112
|
+
else
|
|
119
113
|
this.scripts.push({
|
|
120
114
|
id: chunk,
|
|
121
115
|
url: '/public/' + asset
|