@alepha/react 0.10.6 → 0.11.0

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.
@@ -1,11 +1,11 @@
1
1
  import {
2
- AlephaError,
3
- type Async,
4
- createDescriptor,
5
- Descriptor,
6
- KIND,
7
- type Static,
8
- type TSchema,
2
+ AlephaError,
3
+ type Async,
4
+ createDescriptor,
5
+ Descriptor,
6
+ KIND,
7
+ type Static,
8
+ type TSchema,
9
9
  } from "@alepha/core";
10
10
  import type { ServerRequest } from "@alepha/server";
11
11
  import type { ServerRouteCache } from "@alepha/server-cache";
@@ -103,267 +103,269 @@ import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
103
103
  * ```
104
104
  */
105
105
  export const $page = <
106
- TConfig extends PageConfigSchema = PageConfigSchema,
107
- TProps extends object = TPropsDefault,
108
- TPropsParent extends object = TPropsParentDefault,
106
+ TConfig extends PageConfigSchema = PageConfigSchema,
107
+ TProps extends object = TPropsDefault,
108
+ TPropsParent extends object = TPropsParentDefault,
109
109
  >(
110
- options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
110
+ options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
111
111
  ): PageDescriptor<TConfig, TProps, TPropsParent> => {
112
- return createDescriptor(
113
- PageDescriptor<TConfig, TProps, TPropsParent>,
114
- options,
115
- );
112
+ return createDescriptor(
113
+ PageDescriptor<TConfig, TProps, TPropsParent>,
114
+ options,
115
+ );
116
116
  };
117
117
 
118
118
  // ---------------------------------------------------------------------------------------------------------------------
119
119
 
120
120
  export interface PageDescriptorOptions<
121
- TConfig extends PageConfigSchema = PageConfigSchema,
122
- TProps extends object = TPropsDefault,
123
- TPropsParent extends object = TPropsParentDefault,
121
+ TConfig extends PageConfigSchema = PageConfigSchema,
122
+ TProps extends object = TPropsDefault,
123
+ TPropsParent extends object = TPropsParentDefault,
124
124
  > {
125
- /**
126
- * Name your page.
127
- *
128
- * @default Descriptor key
129
- */
130
- name?: string;
131
-
132
- /**
133
- * Optional description of the page.
134
- */
135
- description?: string;
136
-
137
- /**
138
- * Add a pathname to the page.
139
- *
140
- * Pathname can contain parameters, like `/post/:slug`.
141
- *
142
- * @default ""
143
- */
144
- path?: string;
145
-
146
- /**
147
- * Add an input schema to define:
148
- * - `params`: parameters from the pathname.
149
- * - `query`: query parameters from the URL.
150
- */
151
- schema?: TConfig;
152
-
153
- /**
154
- * Load data before rendering the page.
155
- *
156
- * This function receives
157
- * - the request context and
158
- * - the parent props (if page has a parent)
159
- *
160
- * In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
161
- *
162
- * Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
163
- * It's common to throw a `NotFoundError` to display a 404 page.
164
- *
165
- * RedirectError can be thrown to redirect the user to another page.
166
- */
167
- resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
168
-
169
- /**
170
- * The component to render when the page is loaded.
171
- *
172
- * If `lazy` is defined, this will be ignored.
173
- * Prefer using `lazy` to improve the initial loading time.
174
- */
175
- component?: FC<TProps & TPropsParent>;
176
-
177
- /**
178
- * Lazy load the component when the page is loaded.
179
- *
180
- * It's recommended to use this for components to improve the initial loading time
181
- * and enable code-splitting.
182
- */
183
- lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
184
-
185
- /**
186
- * Set some children pages and make the page a parent page.
187
- *
188
- * /!\ Parent page can't be rendered directly. /!\
189
- *
190
- * If you still want to render at this pathname, add a child page with an empty path.
191
- */
192
- children?: Array<PageDescriptor> | (() => Array<PageDescriptor>);
193
-
194
- parent?: PageDescriptor<PageConfigSchema, TPropsParent>;
195
-
196
- can?: () => boolean;
197
-
198
- /**
199
- * Catch any error from the `resolve` function or during `rendering`.
200
- *
201
- * Expected to return one of the following:
202
- * - a ReactNode to render an error page
203
- * - a Redirection to redirect the user
204
- * - undefined to let the error propagate
205
- *
206
- * If not defined, the error will be thrown and handled by the server or client error handler.
207
- * If a leaf $page does not define an error handler, the error can be caught by parent pages.
208
- *
209
- * @example Catch a 404 from API and render a custom not found component:
210
- * ```ts
211
- * resolve: async ({ params, query }) => {
212
- * api.fetch("/api/resource", { params, query });
213
- * },
214
- * errorHandler: (error, context) => {
215
- * if (HttpError.is(error, 404)) {
216
- * return <ResourceNotFound />;
217
- * }
218
- * }
219
- * ```
220
- *
221
- * @example Catch an 401 error and redirect the user to the login page:
222
- * ```ts
223
- * resolve: async ({ params, query }) => {
224
- * // but the user is not authenticated
225
- * api.fetch("/api/resource", { params, query });
226
- * },
227
- * errorHandler: (error, context) => {
228
- * if (HttpError.is(error, 401)) {
229
- * // throwing a Redirection is also valid!
230
- * return new Redirection("/login");
231
- * }
232
- * }
233
- * ```
234
- */
235
- errorHandler?: ErrorHandler;
236
-
237
- /**
238
- * If true, the page will be considered as a static page, immutable and cacheable.
239
- * Replace boolean by an object to define static entries. (e.g. list of params/query)
240
- *
241
- * For now, it only works with `@alepha/vite` which can pre-render the page at build time.
242
- *
243
- * It will act as timeless cached page server-side. You can use `cache` to configure the cache behavior.
244
- */
245
- static?:
246
- | boolean
247
- | {
248
- entries?: Array<Partial<PageRequestConfig<TConfig>>>;
249
- };
250
-
251
- cache?: ServerRouteCache;
252
-
253
- /**
254
- * If true, force the page to be rendered only on the client-side.
255
- * It uses the `<ClientOnly/>` component to render the page.
256
- */
257
- client?: boolean | ClientOnlyProps;
258
-
259
- /**
260
- * Called before the server response is sent to the client.
261
- */
262
- onServerResponse?: (request: ServerRequest) => any;
263
-
264
- /**
265
- * Called when user leaves the page. (browser only)
266
- */
267
- onLeave?: () => void;
268
-
269
- /**
270
- * @experimental
271
- *
272
- * Add a css animation when the page is loaded or unloaded.
273
- * It uses CSS animations, so you need to define the keyframes in your CSS.
274
- *
275
- * @example Simple animation name
276
- * ```ts
277
- * animation: "fadeIn"
278
- * ```
279
- *
280
- * CSS example:
281
- * ```css
282
- * @keyframes fadeIn {
283
- * from { opacity: 0; }
284
- * to { opacity: 1; }
285
- * }
286
- * ```
287
- *
288
- * @example Detailed animation
289
- * ```ts
290
- * animation: {
291
- * enter: { name: "fadeIn", duration: 300 },
292
- * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
293
- * }
294
- * ```
295
- *
296
- * @example Only exit animation
297
- * ```ts
298
- * animation: {
299
- * exit: "fadeOut"
300
- * }
301
- * ```
302
- *
303
- * @example With custom timing function
304
- * ```ts
305
- * animation: {
306
- * enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
307
- * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
308
- * }
309
- * ```
310
- */
311
- animation?: PageAnimation;
125
+ /**
126
+ * Name your page.
127
+ *
128
+ * @default Descriptor key
129
+ */
130
+ name?: string;
131
+
132
+ /**
133
+ * Optional description of the page.
134
+ */
135
+ description?: string;
136
+
137
+ /**
138
+ * Add a pathname to the page.
139
+ *
140
+ * Pathname can contain parameters, like `/post/:slug`.
141
+ *
142
+ * @default ""
143
+ */
144
+ path?: string;
145
+
146
+ /**
147
+ * Add an input schema to define:
148
+ * - `params`: parameters from the pathname.
149
+ * - `query`: query parameters from the URL.
150
+ */
151
+ schema?: TConfig;
152
+
153
+ /**
154
+ * Load data before rendering the page.
155
+ *
156
+ * This function receives
157
+ * - the request context and
158
+ * - the parent props (if page has a parent)
159
+ *
160
+ * In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
161
+ *
162
+ * Resolve can be stopped by throwing an error, which will be handled by the `errorHandler` function.
163
+ * It's common to throw a `NotFoundError` to display a 404 page.
164
+ *
165
+ * RedirectError can be thrown to redirect the user to another page.
166
+ */
167
+ resolve?: (context: PageResolve<TConfig, TPropsParent>) => Async<TProps>;
168
+
169
+ /**
170
+ * The component to render when the page is loaded.
171
+ *
172
+ * If `lazy` is defined, this will be ignored.
173
+ * Prefer using `lazy` to improve the initial loading time.
174
+ */
175
+ component?: FC<TProps & TPropsParent>;
176
+
177
+ /**
178
+ * Lazy load the component when the page is loaded.
179
+ *
180
+ * It's recommended to use this for components to improve the initial loading time
181
+ * and enable code-splitting.
182
+ */
183
+ lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
184
+
185
+ /**
186
+ * Set some children pages and make the page a parent page.
187
+ *
188
+ * /!\ Parent page can't be rendered directly. /!\
189
+ *
190
+ * If you still want to render at this pathname, add a child page with an empty path.
191
+ */
192
+ children?: Array<PageDescriptor> | (() => Array<PageDescriptor>);
193
+
194
+ parent?: PageDescriptor<PageConfigSchema, TPropsParent>;
195
+
196
+ can?: () => boolean;
197
+
198
+ /**
199
+ * Catch any error from the `resolve` function or during `rendering`.
200
+ *
201
+ * Expected to return one of the following:
202
+ * - a ReactNode to render an error page
203
+ * - a Redirection to redirect the user
204
+ * - undefined to let the error propagate
205
+ *
206
+ * If not defined, the error will be thrown and handled by the server or client error handler.
207
+ * If a leaf $page does not define an error handler, the error can be caught by parent pages.
208
+ *
209
+ * @example Catch a 404 from API and render a custom not found component:
210
+ * ```ts
211
+ * resolve: async ({ params, query }) => {
212
+ * api.fetch("/api/resource", { params, query });
213
+ * },
214
+ * errorHandler: (error, context) => {
215
+ * if (HttpError.is(error, 404)) {
216
+ * return <ResourceNotFound />;
217
+ * }
218
+ * }
219
+ * ```
220
+ *
221
+ * @example Catch an 401 error and redirect the user to the login page:
222
+ * ```ts
223
+ * resolve: async ({ params, query }) => {
224
+ * // but the user is not authenticated
225
+ * api.fetch("/api/resource", { params, query });
226
+ * },
227
+ * errorHandler: (error, context) => {
228
+ * if (HttpError.is(error, 401)) {
229
+ * // throwing a Redirection is also valid!
230
+ * return new Redirection("/login");
231
+ * }
232
+ * }
233
+ * ```
234
+ */
235
+ errorHandler?: ErrorHandler;
236
+
237
+ /**
238
+ * If true, the page will be considered as a static page, immutable and cacheable.
239
+ * Replace boolean by an object to define static entries. (e.g. list of params/query)
240
+ *
241
+ * For now, it only works with `@alepha/vite` which can pre-render the page at build time.
242
+ *
243
+ * It will act as timeless cached page server-side. You can use `cache` to configure the cache behavior.
244
+ */
245
+ static?:
246
+ | boolean
247
+ | {
248
+ entries?: Array<Partial<PageRequestConfig<TConfig>>>;
249
+ };
250
+
251
+ cache?: ServerRouteCache;
252
+
253
+ /**
254
+ * If true, force the page to be rendered only on the client-side.
255
+ * It uses the `<ClientOnly/>` component to render the page.
256
+ */
257
+ client?: boolean | ClientOnlyProps;
258
+
259
+ /**
260
+ * Called before the server response is sent to the client.
261
+ */
262
+ onServerResponse?: (request: ServerRequest) => any;
263
+
264
+ /**
265
+ * Called when user leaves the page. (browser only)
266
+ */
267
+ onLeave?: () => void;
268
+
269
+ /**
270
+ * @experimental
271
+ *
272
+ * Add a css animation when the page is loaded or unloaded.
273
+ * It uses CSS animations, so you need to define the keyframes in your CSS.
274
+ *
275
+ * @example Simple animation name
276
+ * ```ts
277
+ * animation: "fadeIn"
278
+ * ```
279
+ *
280
+ * CSS example:
281
+ * ```css
282
+ * @keyframes fadeIn {
283
+ * from { opacity: 0; }
284
+ * to { opacity: 1; }
285
+ * }
286
+ * ```
287
+ *
288
+ * @example Detailed animation
289
+ * ```ts
290
+ * animation: {
291
+ * enter: { name: "fadeIn", duration: 300 },
292
+ * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
293
+ * }
294
+ * ```
295
+ *
296
+ * @example Only exit animation
297
+ * ```ts
298
+ * animation: {
299
+ * exit: "fadeOut"
300
+ * }
301
+ * ```
302
+ *
303
+ * @example With custom timing function
304
+ * ```ts
305
+ * animation: {
306
+ * enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
307
+ * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
308
+ * }
309
+ * ```
310
+ */
311
+ animation?: PageAnimation;
312
312
  }
313
313
 
314
314
  export type ErrorHandler = (
315
- error: Error,
316
- state: ReactRouterState,
315
+ error: Error,
316
+ state: ReactRouterState,
317
317
  ) => ReactNode | Redirection | undefined;
318
318
 
319
319
  export class PageDescriptor<
320
- TConfig extends PageConfigSchema = PageConfigSchema,
321
- TProps extends object = TPropsDefault,
322
- TPropsParent extends object = TPropsParentDefault,
320
+ TConfig extends PageConfigSchema = PageConfigSchema,
321
+ TProps extends object = TPropsDefault,
322
+ TPropsParent extends object = TPropsParentDefault,
323
323
  > extends Descriptor<PageDescriptorOptions<TConfig, TProps, TPropsParent>> {
324
- protected onInit() {
325
- if (this.options.static) {
326
- this.options.cache ??= {
327
- provider: "memory",
328
- ttl: [1, "week"],
329
- };
330
- }
331
- }
332
-
333
- public get name(): string {
334
- return this.options.name ?? this.config.propertyKey;
335
- }
336
-
337
- /**
338
- * For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
339
- * Only valid for server-side rendering, it will throw an error if called on the client-side.
340
- */
341
- public async render(
342
- options?: PageDescriptorRenderOptions,
343
- ): Promise<PageDescriptorRenderResult> {
344
- throw new AlephaError(
345
- "render() method is not implemented in this environment",
346
- );
347
- }
348
-
349
- public async fetch(options?: PageDescriptorRenderOptions): Promise<{
350
- html: string;
351
- response: Response;
352
- }> {
353
- throw new AlephaError(
354
- "fetch() method is not implemented in this environment",
355
- );
356
- }
357
-
358
- public match(url: string): boolean {
359
- // TODO: Implement a way to match the URL against the pathname
360
- return false;
361
- }
362
-
363
- public pathname(config: any) {
364
- // TODO: Implement a way to generate the pathname based on the config
365
- return this.options.path || "";
366
- }
324
+ protected onInit() {
325
+ if (this.options.static) {
326
+ this.options.cache ??= {
327
+ store: {
328
+ provider: "memory",
329
+ ttl: [1, "week"],
330
+ },
331
+ };
332
+ }
333
+ }
334
+
335
+ public get name(): string {
336
+ return this.options.name ?? this.config.propertyKey;
337
+ }
338
+
339
+ /**
340
+ * For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
341
+ * Only valid for server-side rendering, it will throw an error if called on the client-side.
342
+ */
343
+ public async render(
344
+ options?: PageDescriptorRenderOptions,
345
+ ): Promise<PageDescriptorRenderResult> {
346
+ throw new AlephaError(
347
+ "render() method is not implemented in this environment",
348
+ );
349
+ }
350
+
351
+ public async fetch(options?: PageDescriptorRenderOptions): Promise<{
352
+ html: string;
353
+ response: Response;
354
+ }> {
355
+ throw new AlephaError(
356
+ "fetch() method is not implemented in this environment",
357
+ );
358
+ }
359
+
360
+ public match(url: string): boolean {
361
+ // TODO: Implement a way to match the URL against the pathname
362
+ return false;
363
+ }
364
+
365
+ public pathname(config: any) {
366
+ // TODO: Implement a way to generate the pathname based on the config
367
+ return this.options.path || "";
368
+ }
367
369
  }
368
370
 
369
371
  $page[KIND] = PageDescriptor;
@@ -371,8 +373,8 @@ $page[KIND] = PageDescriptor;
371
373
  // ---------------------------------------------------------------------------------------------------------------------
372
374
 
373
375
  export interface PageConfigSchema {
374
- query?: TSchema;
375
- params?: TSchema;
376
+ query?: TSchema;
377
+ params?: TSchema;
376
378
  }
377
379
 
378
380
  export type TPropsDefault = any;
@@ -380,59 +382,59 @@ export type TPropsDefault = any;
380
382
  export type TPropsParentDefault = {};
381
383
 
382
384
  export interface PageDescriptorRenderOptions {
383
- params?: Record<string, string>;
384
- query?: Record<string, string>;
385
-
386
- /**
387
- * If true, the HTML layout will be included in the response.
388
- * If false, only the page content will be returned.
389
- *
390
- * @default true
391
- */
392
- html?: boolean;
393
- hydration?: boolean;
385
+ params?: Record<string, string>;
386
+ query?: Record<string, string>;
387
+
388
+ /**
389
+ * If true, the HTML layout will be included in the response.
390
+ * If false, only the page content will be returned.
391
+ *
392
+ * @default true
393
+ */
394
+ html?: boolean;
395
+ hydration?: boolean;
394
396
  }
395
397
 
396
398
  export interface PageDescriptorRenderResult {
397
- html: string;
398
- state: ReactRouterState;
399
- redirect?: string;
399
+ html: string;
400
+ state: ReactRouterState;
401
+ redirect?: string;
400
402
  }
401
403
 
402
404
  export interface PageRequestConfig<
403
- TConfig extends PageConfigSchema = PageConfigSchema,
405
+ TConfig extends PageConfigSchema = PageConfigSchema,
404
406
  > {
405
- params: TConfig["params"] extends TSchema
406
- ? Static<TConfig["params"]>
407
- : Record<string, string>;
407
+ params: TConfig["params"] extends TSchema
408
+ ? Static<TConfig["params"]>
409
+ : Record<string, string>;
408
410
 
409
- query: TConfig["query"] extends TSchema
410
- ? Static<TConfig["query"]>
411
- : Record<string, string>;
411
+ query: TConfig["query"] extends TSchema
412
+ ? Static<TConfig["query"]>
413
+ : Record<string, string>;
412
414
  }
413
415
 
414
416
  export type PageResolve<
415
- TConfig extends PageConfigSchema = PageConfigSchema,
416
- TPropsParent extends object = TPropsParentDefault,
417
+ TConfig extends PageConfigSchema = PageConfigSchema,
418
+ TPropsParent extends object = TPropsParentDefault,
417
419
  > = PageRequestConfig<TConfig> &
418
- TPropsParent &
419
- Omit<ReactRouterState, "layers" | "onError">;
420
+ TPropsParent &
421
+ Omit<ReactRouterState, "layers" | "onError">;
420
422
 
421
423
  export type PageAnimation =
422
- | PageAnimationObject
423
- | ((state: ReactRouterState) => PageAnimationObject | undefined);
424
+ | PageAnimationObject
425
+ | ((state: ReactRouterState) => PageAnimationObject | undefined);
424
426
 
425
427
  type PageAnimationObject =
426
- | CssAnimationName
427
- | {
428
- enter?: CssAnimation | CssAnimationName;
429
- exit?: CssAnimation | CssAnimationName;
430
- };
428
+ | CssAnimationName
429
+ | {
430
+ enter?: CssAnimation | CssAnimationName;
431
+ exit?: CssAnimation | CssAnimationName;
432
+ };
431
433
 
432
434
  type CssAnimationName = string;
433
435
 
434
436
  type CssAnimation = {
435
- name: string;
436
- duration?: number;
437
- timing?: string;
437
+ name: string;
438
+ duration?: number;
439
+ timing?: string;
438
440
  };
@@ -4,10 +4,10 @@
4
4
  * Depends on the context, it can be thrown or just returned.
5
5
  */
6
6
  export class Redirection extends Error {
7
- public readonly redirect: string;
7
+ public readonly redirect: string;
8
8
 
9
- constructor(redirect: string) {
10
- super("Redirection");
11
- this.redirect = redirect;
12
- }
9
+ constructor(redirect: string) {
10
+ super("Redirection");
11
+ this.redirect = redirect;
12
+ }
13
13
  }