proteum 1.0.2 → 2.0.0-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 (185) hide show
  1. package/AGENTS.md +101 -0
  2. package/agents/codex/AGENTS.md +95 -0
  3. package/agents/codex/CODING_STYLE.md +71 -0
  4. package/agents/codex/agents.md.zip +0 -0
  5. package/agents/codex/client/AGENTS.md +102 -0
  6. package/agents/codex/client/pages/AGENTS.md +35 -0
  7. package/agents/codex/server/routes/AGENTS.md +12 -0
  8. package/agents/codex/server/services/AGENTS.md +137 -0
  9. package/agents/codex/tests/AGENTS.md +8 -0
  10. package/cli/app/config.ts +13 -11
  11. package/cli/app/index.ts +74 -82
  12. package/cli/bin.js +1 -1
  13. package/cli/commands/build.ts +51 -14
  14. package/cli/commands/check.ts +19 -0
  15. package/cli/commands/deploy/app.ts +4 -8
  16. package/cli/commands/deploy/web.ts +16 -20
  17. package/cli/commands/dev.ts +189 -64
  18. package/cli/commands/devEvents.ts +106 -0
  19. package/cli/commands/init.ts +63 -57
  20. package/cli/commands/lint.ts +21 -0
  21. package/cli/commands/refresh.ts +18 -0
  22. package/cli/commands/typecheck.ts +18 -0
  23. package/cli/compiler/client/identite.ts +80 -53
  24. package/cli/compiler/client/index.ts +139 -213
  25. package/cli/compiler/common/bundleAnalysis.ts +94 -0
  26. package/cli/compiler/common/clientManifest.ts +67 -0
  27. package/cli/compiler/common/controllers.ts +288 -0
  28. package/cli/compiler/common/files/autres.ts +7 -18
  29. package/cli/compiler/common/files/images.ts +40 -37
  30. package/cli/compiler/common/files/style.ts +11 -22
  31. package/cli/compiler/common/generatedRouteModules.ts +368 -0
  32. package/cli/compiler/common/index.ts +31 -65
  33. package/cli/compiler/common/loaders/forbid-ssr-import.js +13 -0
  34. package/cli/compiler/common/rspackAliases.ts +13 -0
  35. package/cli/compiler/common/scripts.ts +37 -0
  36. package/cli/compiler/index.ts +781 -230
  37. package/cli/compiler/server/index.ts +59 -75
  38. package/cli/compiler/writeIfChanged.ts +21 -0
  39. package/cli/index.ts +71 -72
  40. package/cli/paths.ts +51 -57
  41. package/cli/print.ts +17 -11
  42. package/cli/tsconfig.json +5 -4
  43. package/cli/utils/agents.ts +100 -0
  44. package/cli/utils/check.ts +71 -0
  45. package/cli/utils/index.ts +1 -3
  46. package/cli/utils/keyboard.ts +8 -25
  47. package/cli/utils/runProcess.ts +30 -0
  48. package/client/app/component.tsx +29 -29
  49. package/client/app/index.ts +36 -57
  50. package/client/app/service.ts +7 -12
  51. package/client/app.tsconfig.json +2 -2
  52. package/client/components/Dialog/Manager.ssr.tsx +40 -0
  53. package/client/components/Dialog/Manager.tsx +119 -150
  54. package/client/components/Dialog/status.tsx +3 -3
  55. package/client/components/index.ts +1 -1
  56. package/client/components/types.d.ts +1 -3
  57. package/client/dev/hmr.ts +65 -0
  58. package/client/global.d.ts +2 -2
  59. package/client/hooks.ts +6 -9
  60. package/client/index.ts +2 -1
  61. package/client/islands/index.ts +7 -0
  62. package/client/islands/useDeferredModule.ts +199 -0
  63. package/client/pages/_layout/index.tsx +4 -12
  64. package/client/pages/useHeader.tsx +14 -21
  65. package/client/router.ts +27 -0
  66. package/client/services/router/components/Link.tsx +34 -27
  67. package/client/services/router/components/Page.tsx +6 -14
  68. package/client/services/router/components/router.ssr.tsx +36 -0
  69. package/client/services/router/components/router.tsx +63 -83
  70. package/client/services/router/index.tsx +185 -220
  71. package/client/services/router/request/api.ts +97 -119
  72. package/client/services/router/request/history.ts +2 -2
  73. package/client/services/router/request/index.ts +13 -12
  74. package/client/services/router/request/multipart.ts +72 -62
  75. package/client/services/router/response/index.tsx +68 -61
  76. package/client/services/router/response/page.ts +28 -32
  77. package/client/utils/dom.ts +17 -33
  78. package/common/app/index.ts +3 -3
  79. package/common/data/chaines/index.ts +22 -23
  80. package/common/data/dates.ts +35 -70
  81. package/common/data/markdown.ts +42 -39
  82. package/common/dev/serverHotReload.ts +26 -0
  83. package/common/errors/index.tsx +110 -142
  84. package/common/router/contracts.ts +29 -0
  85. package/common/router/index.ts +89 -108
  86. package/common/router/layouts.ts +34 -47
  87. package/common/router/pageSetup.ts +50 -0
  88. package/common/router/register.ts +53 -24
  89. package/common/router/request/api.ts +30 -36
  90. package/common/router/request/index.ts +2 -8
  91. package/common/router/response/index.ts +8 -15
  92. package/common/router/response/page.ts +70 -58
  93. package/common/utils.ts +1 -1
  94. package/doc/TODO.md +1 -1
  95. package/eslint.js +62 -0
  96. package/package.json +14 -49
  97. package/prettier.config.cjs +9 -0
  98. package/scripts/cleanup-generated-controllers.ts +62 -0
  99. package/scripts/fix-reference-app-typing.ts +490 -0
  100. package/scripts/refactor-client-app-imports.ts +244 -0
  101. package/scripts/refactor-client-pages.ts +587 -0
  102. package/scripts/refactor-server-controllers.ts +470 -0
  103. package/scripts/refactor-server-runtime-aliases.ts +360 -0
  104. package/scripts/restore-client-app-import-files.ts +41 -0
  105. package/scripts/restore-files-from-git-head.ts +20 -0
  106. package/scripts/update-codex-agents.ts +35 -0
  107. package/server/app/commands.ts +35 -64
  108. package/server/app/container/config.ts +48 -59
  109. package/server/app/container/console/index.ts +202 -248
  110. package/server/app/container/index.ts +33 -71
  111. package/server/app/controller/index.ts +61 -0
  112. package/server/app/index.ts +39 -105
  113. package/server/app/service/container.ts +41 -42
  114. package/server/app/service/index.ts +120 -147
  115. package/server/context.ts +1 -1
  116. package/server/index.ts +25 -1
  117. package/server/services/auth/index.ts +75 -115
  118. package/server/services/auth/router/index.ts +31 -32
  119. package/server/services/auth/router/request.ts +14 -16
  120. package/server/services/cron/CronTask.ts +13 -26
  121. package/server/services/cron/index.ts +14 -36
  122. package/server/services/disks/driver.ts +40 -58
  123. package/server/services/disks/drivers/local/index.ts +79 -90
  124. package/server/services/disks/drivers/s3/index.ts +116 -163
  125. package/server/services/disks/index.ts +23 -38
  126. package/server/services/email/index.ts +45 -104
  127. package/server/services/email/utils.ts +14 -27
  128. package/server/services/fetch/index.ts +53 -85
  129. package/server/services/prisma/Facet.ts +39 -91
  130. package/server/services/prisma/index.ts +74 -110
  131. package/server/services/router/generatedRuntime.ts +29 -0
  132. package/server/services/router/http/index.ts +78 -73
  133. package/server/services/router/http/multipart.ts +19 -42
  134. package/server/services/router/index.ts +378 -365
  135. package/server/services/router/request/api.ts +26 -25
  136. package/server/services/router/request/index.ts +44 -51
  137. package/server/services/router/request/service.ts +7 -11
  138. package/server/services/router/request/validation/zod.ts +111 -148
  139. package/server/services/router/response/index.ts +110 -125
  140. package/server/services/router/response/mask/Filter.ts +31 -72
  141. package/server/services/router/response/mask/index.ts +8 -15
  142. package/server/services/router/response/mask/selecteurs.ts +11 -25
  143. package/server/services/router/response/page/clientManifest.ts +25 -0
  144. package/server/services/router/response/page/document.tsx +199 -127
  145. package/server/services/router/response/page/index.tsx +89 -94
  146. package/server/services/router/service.ts +13 -15
  147. package/server/services/schema/index.ts +17 -26
  148. package/server/services/schema/request.ts +19 -33
  149. package/server/services/schema/router/index.ts +8 -11
  150. package/server/services/security/encrypt/aes/index.ts +15 -35
  151. package/server/utils/slug.ts +29 -35
  152. package/skills/clean-project-code/SKILL.md +63 -0
  153. package/skills/clean-project-code/agents/openai.yaml +4 -0
  154. package/tsconfig.common.json +4 -3
  155. package/tsconfig.json +4 -1
  156. package/types/aliases.d.ts +17 -21
  157. package/types/controller-input.test.ts +48 -0
  158. package/types/express-extra.d.ts +6 -0
  159. package/types/global/constants.d.ts +13 -0
  160. package/types/global/express-extra.d.ts +6 -0
  161. package/types/global/modules.d.ts +13 -16
  162. package/types/global/utils.d.ts +17 -49
  163. package/types/global/vendors.d.ts +62 -0
  164. package/types/icons.d.ts +65 -1
  165. package/types/uuid.d.ts +3 -0
  166. package/types/vendors.d.ts +62 -0
  167. package/cli/compiler/common/babel/index.ts +0 -170
  168. package/cli/compiler/common/babel/plugins/index.ts +0 -0
  169. package/cli/compiler/common/babel/plugins/services.ts +0 -586
  170. package/cli/compiler/common/babel/routes/imports.ts +0 -127
  171. package/cli/compiler/common/babel/routes/routes.ts +0 -1130
  172. package/client/services/captcha/index.ts +0 -67
  173. package/client/services/socket/index.ts +0 -147
  174. package/common/data/rte/nodes.ts +0 -83
  175. package/common/data/stats.ts +0 -90
  176. package/common/utils/rte.ts +0 -183
  177. package/server/services/auth/old.ts +0 -277
  178. package/server/services/cache/commands.ts +0 -41
  179. package/server/services/cache/index.ts +0 -297
  180. package/server/services/cache/service.json +0 -6
  181. package/server/services/socket/index.ts +0 -162
  182. package/server/services/socket/scope.ts +0 -226
  183. package/server/services/socket/service.json +0 -6
  184. package/server/services_old/SocketClient.ts +0 -92
  185. package/server/services_old/Token.old.ts +0 -97
@@ -0,0 +1,65 @@
1
+ type TDevEvent = { type: 'reload'; reason: 'server' | 'manual' | 'client' };
2
+
3
+ const devEventsPath = '/__proteum_hmr';
4
+ let reloadPending = false;
5
+ let source: EventSource | null = null;
6
+
7
+ const delay = (timeout: number) => new Promise<void>((resolve) => window.setTimeout(resolve, timeout));
8
+
9
+ const buildDevEventsUrl = () => {
10
+ if (typeof window === 'undefined') return null;
11
+ if (PROTEUM_DEV_EVENT_PORT === null) return null;
12
+
13
+ const hostname = window.location.hostname || 'localhost';
14
+ return `${window.location.protocol}//${hostname}:${PROTEUM_DEV_EVENT_PORT}${devEventsPath}`;
15
+ };
16
+
17
+ const waitForServer = async () => {
18
+ for (let attempt = 0; attempt < 40; attempt += 1) {
19
+ try {
20
+ const response = await fetch(`/ping?ts=${Date.now()}`, { cache: 'no-store' });
21
+ if (response.ok) return;
22
+ } catch (error) {}
23
+
24
+ await delay(250);
25
+ }
26
+ };
27
+
28
+ const requestReload = async () => {
29
+ if (reloadPending) return;
30
+ reloadPending = true;
31
+ source?.close();
32
+ source = null;
33
+
34
+ try {
35
+ await waitForServer();
36
+ } finally {
37
+ window.location.reload();
38
+ }
39
+ };
40
+
41
+ const handleEvent = (_event: TDevEvent) => {
42
+ void requestReload();
43
+ };
44
+
45
+ const devEventsUrl = buildDevEventsUrl();
46
+
47
+ if (devEventsUrl && typeof EventSource !== 'undefined') {
48
+ source = new EventSource(devEventsUrl);
49
+
50
+ source.onmessage = (event) => {
51
+ try {
52
+ const payload = JSON.parse(event.data) as TDevEvent;
53
+ handleEvent(payload);
54
+ } catch (error) {
55
+ console.warn('[hmr] Failed to parse dev event payload.', error);
56
+ }
57
+ };
58
+
59
+ source.onerror = () => {
60
+ // EventSource reconnects automatically. Transient disconnects are expected
61
+ // during rebuilds and page reloads, so warning here would be mostly noise.
62
+ };
63
+ } else {
64
+ console.warn('[hmr] Dev event stream is unavailable in this environment.');
65
+ }
@@ -1,5 +1,5 @@
1
1
  declare global {
2
2
  interface Window {
3
- dev?: boolean
3
+ dev?: boolean;
4
4
  }
5
- }
5
+ }
package/client/hooks.ts CHANGED
@@ -9,14 +9,11 @@ export { default as useScript } from '@client/hooks/useScript';*/
9
9
 
10
10
  // Utils
11
11
  export const Switch = (val: string | number, options: { [cle: string]: ComponentChild }) => {
12
- return (val in options) ? options[val] : null;
13
- }
12
+ return val in options ? options[val] : null;
13
+ };
14
14
 
15
- export const useState = <TData extends TObjetDonnees>(initial: TData): [
16
- TData,
17
- (data: Partial<TData>) => void
18
- ] => {
15
+ export const useState = <TData extends TObjetDonnees>(initial: TData): [TData, (data: Partial<TData>) => void] => {
19
16
  const [state, setState] = React.useState<TData>(initial);
20
- const setPartialState = (data: Partial<TData>) => setState(current => ({ ...current, ...data }));
21
- return [state, setPartialState]
22
- }
17
+ const setPartialState = (data: Partial<TData>) => setState((current) => ({ ...current, ...data }));
18
+ return [state, setPartialState];
19
+ };
package/client/index.ts CHANGED
@@ -2,5 +2,6 @@ import Application from '@/client';
2
2
 
3
3
  const app = new Application();
4
4
  app.app = app;
5
+ window.app = app;
5
6
 
6
- app.start();
7
+ app.start();
@@ -0,0 +1,7 @@
1
+ export { default as useDeferredModule } from './useDeferredModule';
2
+ export type {
3
+ TDeferredModuleLoader,
4
+ TDeferredModuleOptions,
5
+ TDeferredModuleResult,
6
+ TDeferredModuleStrategy,
7
+ } from './useDeferredModule';
@@ -0,0 +1,199 @@
1
+ import React from 'react';
2
+
3
+ export type TDeferredModuleLoader<TModule> = () => Promise<TModule>;
4
+
5
+ export type TDeferredModuleStrategy = 'immediate' | 'idle' | 'visible';
6
+
7
+ export type TDeferredModuleOptions = {
8
+ enabled?: boolean;
9
+ when?: TDeferredModuleStrategy;
10
+ timeoutMs?: number;
11
+ rootMargin?: string;
12
+ cache?: boolean;
13
+ };
14
+
15
+ export type TDeferredModuleResult<TModule> = {
16
+ value?: TModule;
17
+ error?: unknown;
18
+ isLoaded: boolean;
19
+ isLoading: boolean;
20
+ load: () => Promise<TModule>;
21
+ ref: (node: HTMLElement | null) => void;
22
+ };
23
+
24
+ type TDeferredModuleEntry<TModule> = {
25
+ status: 'idle' | 'loading' | 'loaded' | 'error';
26
+ promise?: Promise<TModule>;
27
+ value?: TModule;
28
+ error?: unknown;
29
+ };
30
+
31
+ const moduleCache = new WeakMap<TDeferredModuleLoader<unknown>, TDeferredModuleEntry<unknown>>();
32
+
33
+ const getModuleEntry = <TModule>(
34
+ loader: TDeferredModuleLoader<TModule>,
35
+ cacheEnabled: boolean,
36
+ ): TDeferredModuleEntry<TModule> | undefined => {
37
+ if (!cacheEnabled) return undefined;
38
+
39
+ return moduleCache.get(loader) as TDeferredModuleEntry<TModule> | undefined;
40
+ };
41
+
42
+ const setModuleEntry = <TModule>(
43
+ loader: TDeferredModuleLoader<TModule>,
44
+ entry: TDeferredModuleEntry<TModule>,
45
+ cacheEnabled: boolean,
46
+ ) => {
47
+ if (!cacheEnabled) return;
48
+
49
+ moduleCache.set(loader as TDeferredModuleLoader<unknown>, entry as TDeferredModuleEntry<unknown>);
50
+ };
51
+
52
+ const getInitialState = <TModule>(loader: TDeferredModuleLoader<TModule>, cacheEnabled: boolean) => {
53
+ const entry = getModuleEntry(loader, cacheEnabled);
54
+
55
+ return {
56
+ value: entry?.value,
57
+ error: entry?.error,
58
+ isLoaded: entry?.status === 'loaded',
59
+ isLoading: entry?.status === 'loading',
60
+ };
61
+ };
62
+
63
+ export default function useDeferredModule<TModule>(
64
+ loader: TDeferredModuleLoader<TModule>,
65
+ {
66
+ enabled = true,
67
+ when = 'immediate',
68
+ timeoutMs = 1000,
69
+ rootMargin = '200px',
70
+ cache = true,
71
+ }: TDeferredModuleOptions = {},
72
+ ): TDeferredModuleResult<TModule> {
73
+ const mountedRef = React.useRef(true);
74
+ const [targetNode, setTargetNode] = React.useState<HTMLElement | null>(null);
75
+ const [state, setState] = React.useState(() => getInitialState(loader, cache));
76
+
77
+ React.useEffect(() => {
78
+ mountedRef.current = true;
79
+
80
+ return () => {
81
+ mountedRef.current = false;
82
+ };
83
+ }, []);
84
+
85
+ React.useEffect(() => {
86
+ setState(getInitialState(loader, cache));
87
+ }, [loader, cache]);
88
+
89
+ const ref = React.useCallback(
90
+ (node: HTMLElement | null) => {
91
+ if (when !== 'visible') return;
92
+ setTargetNode(node);
93
+ },
94
+ [when],
95
+ );
96
+
97
+ const load = React.useCallback(async () => {
98
+ const cachedEntry = getModuleEntry(loader, cache);
99
+
100
+ if (cachedEntry?.status === 'loaded') {
101
+ if (mountedRef.current)
102
+ setState({ value: cachedEntry.value, error: undefined, isLoaded: true, isLoading: false });
103
+
104
+ return cachedEntry.value as TModule;
105
+ }
106
+
107
+ if (cachedEntry?.status === 'loading' && cachedEntry.promise) {
108
+ if (mountedRef.current) setState((current) => ({ ...current, isLoading: true }));
109
+
110
+ return cachedEntry.promise;
111
+ }
112
+
113
+ const promise = loader();
114
+ const nextEntry: TDeferredModuleEntry<TModule> = { status: 'loading', promise };
115
+
116
+ setModuleEntry(loader, nextEntry, cache);
117
+
118
+ if (mountedRef.current) setState((current) => ({ ...current, error: undefined, isLoading: true }));
119
+
120
+ try {
121
+ const value = await promise;
122
+ setModuleEntry(loader, { status: 'loaded', value }, cache);
123
+
124
+ if (mountedRef.current) setState({ value, error: undefined, isLoaded: true, isLoading: false });
125
+
126
+ return value;
127
+ } catch (error) {
128
+ setModuleEntry(loader, { status: 'error', error }, cache);
129
+
130
+ if (mountedRef.current) setState({ value: undefined, error, isLoaded: false, isLoading: false });
131
+
132
+ throw error;
133
+ }
134
+ }, [cache, loader]);
135
+
136
+ React.useEffect(() => {
137
+ if (!enabled || state.isLoaded || state.isLoading) return;
138
+
139
+ if (when === 'immediate') {
140
+ void load();
141
+ return;
142
+ }
143
+
144
+ if (when === 'idle') {
145
+ if (typeof window === 'undefined') return;
146
+
147
+ let cancelled = false;
148
+
149
+ if ('requestIdleCallback' in window) {
150
+ const idleId = window.requestIdleCallback(
151
+ () => {
152
+ if (!cancelled) void load();
153
+ },
154
+ { timeout: timeoutMs },
155
+ );
156
+
157
+ return () => {
158
+ cancelled = true;
159
+ window.cancelIdleCallback(idleId);
160
+ };
161
+ }
162
+
163
+ const timeoutId = setTimeout(() => {
164
+ if (!cancelled) void load();
165
+ }, timeoutMs);
166
+
167
+ return () => {
168
+ cancelled = true;
169
+ clearTimeout(timeoutId);
170
+ };
171
+ }
172
+
173
+ if (when === 'visible') {
174
+ if (typeof window === 'undefined' || !targetNode) return;
175
+
176
+ if (typeof IntersectionObserver === 'undefined') {
177
+ void load();
178
+ return;
179
+ }
180
+
181
+ const observer = new IntersectionObserver(
182
+ (entries) => {
183
+ const isVisible = entries.some((entry) => entry.isIntersecting);
184
+ if (!isVisible) return;
185
+
186
+ observer.disconnect();
187
+ void load();
188
+ },
189
+ { rootMargin },
190
+ );
191
+
192
+ observer.observe(targetNode);
193
+
194
+ return () => observer.disconnect();
195
+ }
196
+ }, [enabled, load, rootMargin, state.isLoaded, state.isLoading, targetNode, timeoutMs, when]);
197
+
198
+ return { ...state, load, ref };
199
+ }
@@ -13,31 +13,23 @@ import { ClientContext } from '@/client/context';
13
13
  // Core components
14
14
 
15
15
  // Resources
16
- import "./index.less";
16
+ import './index.less';
17
17
 
18
18
  /*----------------------------------
19
19
  - TYPES
20
20
  ----------------------------------*/
21
21
 
22
-
23
22
  /*----------------------------------
24
23
  - COMPOSANT
25
24
  ----------------------------------*/
26
- export default function App ({ context, menu }: {
27
- context: ClientContext,
28
- menu: ComponentChild
29
- }) {
30
-
25
+ export default function App({ context, menu }: { context: ClientContext; menu?: ComponentChild }) {
31
26
  const { Router, page, toast } = context;
32
27
 
33
28
  return (
34
29
  <div id="internaLlayout">
35
-
36
30
  <div class="center row al-fill">
37
-
38
31
  <RouterComponent service={Router} />
39
-
40
32
  </div>
41
33
  </div>
42
- )
43
- }
34
+ );
35
+ }
@@ -9,42 +9,35 @@ import useContext from '@/client/context';
9
9
  - TYPES
10
10
  ----------------------------------*/
11
11
  export type Props = {
12
- id?: string,
13
- focus?: boolean,
14
- jail?: boolean,
15
- error?: boolean,
12
+ id?: string;
13
+ focus?: boolean;
14
+ jail?: boolean;
15
+ error?: boolean;
16
16
 
17
- title: string,
18
- subtitle?: string,
19
- description?: string,
20
- }
17
+ title: string;
18
+ subtitle?: string;
19
+ description?: string;
20
+ };
21
21
 
22
22
  /*----------------------------------
23
23
  - HOOK
24
24
  ----------------------------------*/
25
25
  export default ({ id, title, subtitle, focus, jail, description }: Props) => {
26
-
27
26
  let { page } = useContext();
28
27
 
29
28
  // page est supposé ne pas être undefined
30
- if (!page)
31
- return;
29
+ if (!page) return;
32
30
 
33
31
  // SEO Title
34
32
  page.title = title;
35
- if (subtitle !== undefined)
36
- page.title += ' | ' + subtitle;
33
+ if (subtitle !== undefined) page.title += ' | ' + subtitle;
37
34
 
38
35
  // SEO Description
39
- if (description !== undefined)
40
- page.description = description;
36
+ if (description !== undefined) page.description = description;
41
37
 
42
38
  page.bodyId = page.bodyId || id || '';
43
39
 
44
- if (focus)
45
- page.bodyClass.add('focus');
40
+ if (focus) page.bodyClass.add('focus');
46
41
 
47
- if (jail)
48
- page.bodyClass.add('jail');
49
-
50
- }
42
+ if (jail) page.bodyClass.add('jail');
43
+ };
@@ -0,0 +1,27 @@
1
+ import type Router from '@client/services/router';
2
+
3
+ const getRouter = (): Router => {
4
+ if (typeof window === 'undefined') {
5
+ throw new Error(`Client router is not available on the server.`);
6
+ }
7
+
8
+ const router = (window.app as (Record<string, unknown> & { Router?: Router }) | undefined)?.Router;
9
+ if (!router) {
10
+ throw new Error(`Client router was accessed before the application booted.`);
11
+ }
12
+
13
+ return router;
14
+ };
15
+
16
+ const ClientRouter = new Proxy({} as Router, {
17
+ get(_target, property) {
18
+ const value = getRouter()[property as keyof Router];
19
+ return typeof value === 'function' ? value.bind(getRouter()) : value;
20
+ },
21
+ set(_target, property, value) {
22
+ ((getRouter() as unknown) as Record<PropertyKey, unknown>)[property] = value;
23
+ return true;
24
+ },
25
+ }) as Router;
26
+
27
+ export default ClientRouter;
@@ -7,40 +7,47 @@ import React from 'react';
7
7
  import type { ComponentChild } from 'preact';
8
8
  import { history } from '../request/history';
9
9
 
10
- export const shouldOpenNewTab = (url: string, target?: string) => url && (
11
- target !== undefined
12
- ||
13
- !['/', '#'].includes(url[0])
14
- ||
15
- url.startsWith('//')
16
- )
10
+ export const shouldOpenNewTab = (url: string, target?: string) =>
11
+ url && (target !== undefined || !['/', '#'].includes(url[0]) || url.startsWith('//'));
17
12
 
18
13
  /*----------------------------------
19
14
  - COMPONENT
20
15
  ----------------------------------*/
21
16
  // Simple link
22
- export const Link = ({ to, ...props }: {
23
- to: string,
24
- children?: ComponentChild,
25
- class?: string,
26
- className?: string
27
- } & React.HTMLProps<HTMLAnchorElement>) => {
17
+ export const Link = ({
18
+ to,
19
+ children,
20
+ class: classNameAttr,
21
+ className,
22
+ onClick,
23
+ target,
24
+ ...props
25
+ }: {
26
+ to: string;
27
+ children?: ComponentChild;
28
+ class?: string;
29
+ className?: string;
30
+ } & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
31
+ const openNewTab = shouldOpenNewTab(to, typeof target === 'string' ? target : undefined);
32
+ const resolvedTarget = openNewTab ? '_blank' : target;
28
33
 
29
- const openNewTab = shouldOpenNewTab(to, props.target);
30
-
31
- // External = open in new tab by default
32
- if (openNewTab)
33
- props.target = '_blank';
34
- // Otherwise, propagate to the router
35
- else
36
- props.onClick = (e) => {
34
+ const handleClick: React.MouseEventHandler<HTMLAnchorElement> | undefined = openNewTab
35
+ ? onClick
36
+ : (e) => {
37
37
  history?.push(to);
38
38
  e.preventDefault();
39
- return false
40
- }
39
+ return false;
40
+ };
41
41
 
42
42
  return (
43
- <a {...props} href={to} />
44
- )
45
-
46
- }
43
+ <a
44
+ {...props}
45
+ href={to}
46
+ target={resolvedTarget}
47
+ onClick={handleClick}
48
+ class={classNameAttr ?? className}
49
+ >
50
+ {children}
51
+ </a>
52
+ );
53
+ };
@@ -15,19 +15,15 @@ import type Page from '../response/page';
15
15
  ----------------------------------*/
16
16
 
17
17
  export default ({ page }: { page: Page }) => {
18
-
19
18
  /*----------------------------------
20
19
  - CONTEXT
21
20
  ----------------------------------*/
22
21
  const context = useContext();
23
22
 
24
23
  // Bind data
25
- const [apiData, setApiData] = React.useState<{[k: string]: any} | null>( page.data || {});
24
+ const [apiData, setApiData] = React.useState<{ [k: string]: any } | null>(page.data || {});
26
25
  page.setAllData = setApiData;
27
- const fullData = {
28
- ...context.data,
29
- ...apiData
30
- }
26
+ const fullData = { ...context.data, ...apiData };
31
27
 
32
28
  // Temporary fix: context.page may not be updated at this stage
33
29
  // Seems to be the case when we change page, but still same page component with different data
@@ -38,18 +34,14 @@ export default ({ page }: { page: Page }) => {
38
34
 
39
35
  // Page component has not changed, but data were updated (ex: url parameters change)
40
36
  React.useEffect(() => {
41
-
42
37
  setApiData(page.data);
43
-
44
38
  }, [page.data]);
45
39
 
40
+ const rendererProps = { ...context, ...fullData } as Parameters<NonNullable<typeof page.renderer>>[0];
41
+
46
42
  /*----------------------------------
47
43
  - RENDER
48
44
  ----------------------------------*/
49
45
  // Make request parameters and api data accessible from the page component
50
- return page.renderer ? (
51
-
52
- <page.renderer {...context} />
53
-
54
- ) : <>Renderer missing</>
55
- }
46
+ return page.renderer ? <page.renderer {...rendererProps} /> : <>Renderer missing</>;
47
+ };
@@ -0,0 +1,36 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import React from 'react';
7
+
8
+ // Core
9
+ import useContext from '@/client/context';
10
+
11
+ // Specific
12
+ import type ClientRouter from '..';
13
+ import PageComponent from './Page';
14
+
15
+ /*----------------------------------
16
+ - TYPES
17
+ ----------------------------------*/
18
+
19
+ export type TProps = { service?: ClientRouter; loaderComponent?: React.ComponentType<{ isLoading: boolean }> };
20
+
21
+ /*----------------------------------
22
+ - COMPONENT
23
+ ----------------------------------*/
24
+ export default function RouterComponent({ service: _clientRouter, loaderComponent: _loaderComponent }: TProps) {
25
+ const context = useContext();
26
+ const currentPage = context.page;
27
+
28
+ if (!currentPage) return null;
29
+
30
+ return (
31
+ <PageComponent
32
+ page={currentPage as Parameters<typeof PageComponent>[0]['page']}
33
+ key={currentPage.chunkId === undefined ? undefined : 'page_' + currentPage.chunkId}
34
+ />
35
+ );
36
+ }