crelte 0.3.0 → 0.3.2

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 (55) hide show
  1. package/dist/Crelte.d.ts +12 -0
  2. package/dist/Crelte.d.ts.map +1 -1
  3. package/dist/Crelte.js +12 -0
  4. package/dist/CrelteRequest.d.ts +0 -4
  5. package/dist/CrelteRequest.d.ts.map +1 -1
  6. package/dist/CrelteRequest.js +0 -4
  7. package/dist/entry/EntryRouter.d.ts +30 -0
  8. package/dist/entry/EntryRouter.d.ts.map +1 -0
  9. package/dist/entry/EntryRouter.js +45 -0
  10. package/dist/entry/index.d.ts +32 -0
  11. package/dist/entry/index.d.ts.map +1 -0
  12. package/dist/entry/index.js +31 -0
  13. package/dist/index.d.ts +2 -6
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +0 -6
  16. package/dist/init/client.d.ts +1 -1
  17. package/dist/init/client.d.ts.map +1 -1
  18. package/dist/init/client.js +4 -3
  19. package/dist/init/server.d.ts.map +1 -1
  20. package/dist/init/server.js +3 -2
  21. package/dist/init/shared.d.ts +3 -1
  22. package/dist/init/shared.d.ts.map +1 -1
  23. package/dist/init/shared.js +47 -26
  24. package/dist/routing/InnerRouter.d.ts +1 -10
  25. package/dist/routing/InnerRouter.d.ts.map +1 -1
  26. package/dist/routing/InnerRouter.js +20 -21
  27. package/dist/routing/Request.d.ts +2 -0
  28. package/dist/routing/Request.d.ts.map +1 -1
  29. package/dist/routing/Request.js +9 -0
  30. package/dist/routing/Route.d.ts +67 -1
  31. package/dist/routing/Route.d.ts.map +1 -1
  32. package/dist/routing/Route.js +98 -2
  33. package/dist/routing/Router.d.ts +49 -12
  34. package/dist/routing/Router.d.ts.map +1 -1
  35. package/dist/routing/Router.js +82 -28
  36. package/dist/routing/index.d.ts +2 -2
  37. package/dist/routing/index.d.ts.map +1 -1
  38. package/dist/utils.d.ts +2 -0
  39. package/dist/utils.d.ts.map +1 -0
  40. package/dist/utils.js +8 -0
  41. package/package.json +9 -4
  42. package/src/Crelte.ts +15 -0
  43. package/src/CrelteRequest.ts +0 -4
  44. package/src/entry/EntryRouter.ts +71 -0
  45. package/src/entry/index.ts +48 -0
  46. package/src/index.ts +2 -11
  47. package/src/init/client.ts +10 -3
  48. package/src/init/server.ts +9 -2
  49. package/src/init/shared.ts +78 -28
  50. package/src/routing/InnerRouter.ts +29 -30
  51. package/src/routing/Request.ts +11 -0
  52. package/src/routing/Route.ts +107 -2
  53. package/src/routing/Router.ts +110 -31
  54. package/src/routing/index.ts +2 -1
  55. package/src/utils.ts +10 -0
@@ -1,5 +1,6 @@
1
1
  import Crelte from '../Crelte.js';
2
2
  import CrelteRequest from '../CrelteRequest.js';
3
+ import EntryRouter, { EntryRoutes } from '../entry/EntryRouter.js';
3
4
  import { GraphQlQuery } from '../graphql/GraphQl.js';
4
5
  import { LoadData, callLoadData } from '../loadData/index.js';
5
6
  import { PluginCreator } from '../plugins/Plugins.js';
@@ -12,6 +13,8 @@ interface App<E, T> {
12
13
  loadEntryData?: LoadData<any>;
13
14
 
14
15
  templates?: Record<string, LazyTemplateModule<E, T>>;
16
+
17
+ entryRoutes?: EntryRoutes;
15
18
  }
16
19
 
17
20
  interface TemplateModule<E, T> {
@@ -58,14 +61,43 @@ export function getEntry(page: any): any {
58
61
  };
59
62
  }
60
63
 
61
- export async function loadFn<D, E, T>(
64
+ // todo it would be nice to call this only once per server start
65
+ export async function prepareLoadFn<E, T>(
66
+ crelte: Crelte,
67
+ app: App<E, T>,
68
+ entryQuery: GraphQlQuery,
69
+ globalQuery?: GraphQlQuery,
70
+ ): Promise<(cr: CrelteRequest, loadOpts?: LoadOptions) => Promise<any>> {
71
+ const templateModules = prepareTemplates(app.templates ?? {});
72
+ let entryRouter: EntryRouter | null = null;
73
+ if (app.entryRoutes) {
74
+ entryRouter = new EntryRouter(crelte);
75
+ await app.entryRoutes(entryRouter);
76
+ }
77
+
78
+ return async (cr, loadOpts) => {
79
+ return await loadFn(
80
+ cr,
81
+ app,
82
+ templateModules,
83
+ entryRouter,
84
+ entryQuery,
85
+ globalQuery,
86
+ loadOpts,
87
+ );
88
+ };
89
+ }
90
+
91
+ async function loadFn<E, T>(
62
92
  cr: CrelteRequest,
63
93
  app: App<E, T>,
94
+ templateModules: Map<string, LazyTemplateModule<E, T>>,
95
+ entryRouter: EntryRouter | null,
64
96
  entryQuery: GraphQlQuery,
65
97
  globalQuery?: GraphQlQuery,
66
98
  loadOpts?: LoadOptions,
67
99
  ): Promise<any> {
68
- let dataProm: Promise<D> | null = null;
100
+ let dataProm: Promise<any> | null = null;
69
101
  // @ts-ignore
70
102
  if (app.loadData) {
71
103
  throw new Error(
@@ -93,43 +125,29 @@ export async function loadFn<D, E, T>(
93
125
  })();
94
126
  }
95
127
 
96
- let pageProm = null;
97
- if (cr.req.siteMatches()) {
98
- let uri = decodeURI(cr.req.uri);
99
- if (uri.startsWith('/')) uri = uri.substring(1);
100
- if (uri === '' || uri === '/') uri = '__home__';
101
-
102
- pageProm = cr.query(entryQuery, {
103
- uri,
104
- siteId: cr.site.id,
105
- });
106
- }
128
+ const entryProm = queryEntry(cr, app, entryRouter, entryQuery);
107
129
 
108
130
  const pluginsLoadGlobalData = cr.events.trigger('loadGlobalData', cr);
109
131
 
110
132
  // loading progress is at 20%
111
133
  loadOpts?.setProgress(0.2);
112
134
 
113
- const [data, global, page] = await Promise.all([
135
+ const [data, global, entry] = await Promise.all([
114
136
  dataProm,
115
137
  globalProm,
116
- pageProm,
138
+ entryProm,
117
139
  ...pluginsLoadGlobalData,
118
140
  ]);
119
141
 
120
- if (global) {
121
- cr.globals._setData(cr.site.id, global);
122
- } else if (!cr.globals._wasLoaded(cr.site.id)) {
123
- // we need to set the global data to an empty object
124
- // so any waiters get's triggered
125
- cr.globals._setData(cr.site.id, {});
142
+ // global is only set if !wasLoaded but we need to store something
143
+ // even if no globalQuery exists
144
+ if (global || !cr.globals._wasLoaded(cr.site.id)) {
145
+ cr.globals._setData(cr.site.id, global ?? {});
126
146
  }
127
147
 
128
- const entry = getEntry(page);
129
-
130
148
  let template;
131
149
  if (app.templates) {
132
- template = await loadTemplate(app.templates, entry);
150
+ template = await loadTemplate(templateModules, entry);
133
151
  } else {
134
152
  throw new Error('App must have templates or loadTemplate method');
135
153
  }
@@ -180,12 +198,39 @@ function parseFilename(path: string): [string, string] {
180
198
  return [name, ext];
181
199
  }
182
200
 
183
- async function loadTemplate<E, T>(
201
+ async function queryEntry<E, T>(
202
+ cr: CrelteRequest,
203
+ app: App<E, T>,
204
+ entryRouter: EntryRouter | null,
205
+ entryQuery: GraphQlQuery,
206
+ ): Promise<any | null> {
207
+ // check
208
+ if (entryRouter) {
209
+ const entry = await entryRouter._handle(cr);
210
+ if (entry) return entry;
211
+ }
212
+
213
+ if (cr.req.siteMatches()) {
214
+ let uri = decodeURI(cr.req.uri);
215
+ if (uri.startsWith('/')) uri = uri.substring(1);
216
+ if (uri === '' || uri === '/') uri = '__home__';
217
+
218
+ const page = await cr.query(entryQuery, {
219
+ uri,
220
+ siteId: cr.site.id,
221
+ });
222
+
223
+ return getEntry(page);
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ function prepareTemplates<E, T>(
184
230
  rawModules: Record<string, LazyTemplateModule<E, T>>,
185
- entry: E,
186
- ): Promise<TemplateModule<E, T>> {
231
+ ): Map<string, LazyTemplateModule<E, T>> {
187
232
  // parse modules
188
- const modules = new Map(
233
+ return new Map(
189
234
  Object.entries(rawModules)
190
235
  .map(([path, mod]) => {
191
236
  const [name, _ext] = parseFilename(path);
@@ -193,7 +238,12 @@ async function loadTemplate<E, T>(
193
238
  })
194
239
  .filter(([name, _mod]) => !!name),
195
240
  );
241
+ }
196
242
 
243
+ async function loadTemplate<E, T>(
244
+ modules: Map<string, LazyTemplateModule<E, T>>,
245
+ entry: E,
246
+ ): Promise<TemplateModule<E, T>> {
197
247
  const entr = entry as any;
198
248
  const handle = `${entr.sectionHandle}-${entr.typeHandle}`;
199
249
 
@@ -68,7 +68,10 @@ export default class InnerRouter {
68
68
  req.origin = 'init';
69
69
  window.history.scrollRestoration = 'manual';
70
70
 
71
- this.setRoute(req);
71
+ // we set it now instead of waiting for the onRoute call
72
+ // because the window.history is already set
73
+ this.route = req.toRoute();
74
+ this.onRoute(req, () => {});
72
75
  }
73
76
 
74
77
  /**
@@ -301,7 +304,10 @@ export default class InnerRouter {
301
304
  req._fillFromState(e.state);
302
305
  req.origin = 'pop';
303
306
 
304
- this.setRoute(req);
307
+ // we set it now instead of waiting for the onRoute call
308
+ // because the window.history was already modified
309
+ this.route = req.toRoute();
310
+ this.onRoute(req, () => {});
305
311
  });
306
312
  }
307
313
 
@@ -350,24 +356,15 @@ export default class InnerRouter {
350
356
 
351
357
  req.index = (current?.index ?? 0) + 1;
352
358
  this.onRoute(req, () => {
353
- this.push(req, true);
359
+ const url = req.url;
360
+ this.history.pushState(
361
+ req._toState(),
362
+ url.pathname + url.search + url.hash,
363
+ );
364
+ this.route = req.toRoute();
354
365
  });
355
366
  }
356
367
 
357
- /**
358
- * Sets a route
359
- *
360
- * Will trigger an onRoute event but will not store any scroll progress
361
- * or modify the history
362
- *
363
- * @param req
364
- */
365
- setRoute(req: Request, preventOnRoute = false) {
366
- this.route = req.toRoute();
367
-
368
- if (!preventOnRoute) this.onRoute(req, () => {});
369
- }
370
-
371
368
  /**
372
369
  * This pushes a new route to the history
373
370
  *
@@ -376,7 +373,7 @@ export default class InnerRouter {
376
373
  * ## Important
377
374
  * Make sure the route has the correct origin
378
375
  */
379
- push(req: Request, preventOnRoute = false) {
376
+ push(req: Request) {
380
377
  const url = req.url;
381
378
  // todo a push should also store the previous scrollY
382
379
 
@@ -390,12 +387,13 @@ export default class InnerRouter {
390
387
  nReq.scrollY = this.history.scrollY();
391
388
  }
392
389
 
393
- this.history.pushState(
394
- nReq._toState(),
395
- url.pathname + url.search + url.hash,
396
- );
397
-
398
- this.setRoute(req, preventOnRoute);
390
+ this.onRoute(req, () => {
391
+ this.history.pushState(
392
+ req._toState(),
393
+ url.pathname + url.search + url.hash,
394
+ );
395
+ this.route = req.toRoute();
396
+ });
399
397
  }
400
398
 
401
399
  /**
@@ -420,12 +418,13 @@ export default class InnerRouter {
420
418
  nReq.scrollY = this.history.scrollY();
421
419
  }
422
420
 
423
- this.history.replaceState(
424
- nReq._toState(),
425
- url.pathname + url.search + url.hash,
426
- );
427
-
428
- this.setRoute(req);
421
+ this.onRoute(req, () => {
422
+ this.history.replaceState(
423
+ req._toState(),
424
+ url.pathname + url.search + url.hash,
425
+ );
426
+ this.route = req.toRoute();
427
+ });
429
428
  }
430
429
 
431
430
  /**
@@ -1,6 +1,7 @@
1
1
  import { Barrier } from 'crelte-std/sync';
2
2
  import Route, { RouteOrigin } from './Route.js';
3
3
  import Site from './Site.js';
4
+ import { objClone } from '../utils.js';
4
5
 
5
6
  /**
6
7
  * Options to create a Request
@@ -9,6 +10,8 @@ export type RequestOptions = {
9
10
  scrollY?: number;
10
11
  index?: number;
11
12
  origin?: RouteOrigin;
13
+ state?: Record<string, any>;
14
+ context?: Record<string, any>;
12
15
  disableScroll?: boolean;
13
16
  disableLoadData?: boolean;
14
17
  statusCode?: number;
@@ -60,6 +63,8 @@ export default class Request extends Route {
60
63
  scrollY: route.scrollY ?? undefined,
61
64
  index: route.index,
62
65
  origin: route.origin,
66
+ state: route._state,
67
+ context: route._context,
63
68
  ...opts,
64
69
  });
65
70
  }
@@ -104,6 +109,8 @@ export default class Request extends Route {
104
109
  scrollY: this.scrollY ?? undefined,
105
110
  index: this.index,
106
111
  origin: this.origin,
112
+ state: objClone(this._state),
113
+ context: this._context,
107
114
  disableScroll: this.disableScroll,
108
115
  statusCode: this.statusCode ?? undefined,
109
116
  });
@@ -117,6 +124,8 @@ export default class Request extends Route {
117
124
  scrollY: this.scrollY ?? undefined,
118
125
  index: this.index,
119
126
  origin: this.origin,
127
+ state: objClone(this._state),
128
+ context: this._context,
120
129
  });
121
130
  }
122
131
 
@@ -125,6 +134,8 @@ export default class Request extends Route {
125
134
  this.scrollY = opts.scrollY ?? this.scrollY;
126
135
  this.index = opts.index ?? this.index;
127
136
  this.origin = opts.origin ?? this.origin;
137
+ this._state = opts.state ?? this._state;
138
+ this._context = opts.context ?? this._context;
128
139
  this.disableScroll = opts.disableScroll ?? this.disableScroll;
129
140
  this.statusCode = opts.statusCode ?? this.statusCode;
130
141
  }
@@ -1,3 +1,4 @@
1
+ import { objClone } from '../utils.js';
1
2
  import Site from './Site.js';
2
3
  import { trimSlashEnd } from './utils.js';
3
4
 
@@ -5,6 +6,8 @@ export type RouteOptions = {
5
6
  scrollY?: number;
6
7
  index?: number;
7
8
  origin?: RouteOrigin;
9
+ state?: Record<string, any>;
10
+ context?: Record<string, any>;
8
11
  };
9
12
 
10
13
  /**
@@ -74,6 +77,25 @@ export default class Route {
74
77
  */
75
78
  origin: RouteOrigin;
76
79
 
80
+ /**
81
+ * @hidden
82
+ * State data that can be used to store additional information
83
+ */
84
+ _state: Record<string, any>;
85
+
86
+ /**
87
+ * @hidden
88
+ * Any data that should be passed to onRoute and onRequest handlers
89
+ * or exchanged between loadData's
90
+ * This context is not persistant and should be considered "valid"
91
+ * only for the current request / route
92
+ *
93
+ * ## Note
94
+ * Consider using state instead. This will not be cloned in the clone
95
+ * call so will always be the same object
96
+ */
97
+ _context: Record<string, any>;
98
+
77
99
  /**
78
100
  * Creates a new Route
79
101
  */
@@ -84,6 +106,8 @@ export default class Route {
84
106
  this.scrollY = opts.scrollY ?? null;
85
107
  this.index = opts.index ?? 0;
86
108
  this.origin = opts.origin ?? 'manual';
109
+ this._state = opts.state ?? {};
110
+ this._context = opts.context ?? {};
87
111
  }
88
112
 
89
113
  /**
@@ -163,6 +187,20 @@ export default class Route {
163
187
  return this.url.hash;
164
188
  }
165
189
 
190
+ /**
191
+ * Set the hash of the route
192
+ *
193
+ * ## Example
194
+ * ```
195
+ * const route = new Route('https://example.com/foo/bar/', null);
196
+ * route.hash = '#hash';
197
+ * console.log(route.url.href); // 'https://example.com/foo/bar/#hash'
198
+ * ```
199
+ */
200
+ set hash(hash: string) {
201
+ this.url.hash = hash;
202
+ }
203
+
166
204
  /**
167
205
  * Checks if there are previous routes which would allow it to go back
168
206
  */
@@ -184,7 +222,8 @@ export default class Route {
184
222
  }
185
223
 
186
224
  /**
187
- * Sets the search param or removes it if the value is null or undefined
225
+ * Sets the search param or removes it if the value is null, undefined or an
226
+ * empty string
188
227
  *
189
228
  * ## Example
190
229
  * ```
@@ -197,13 +236,69 @@ export default class Route {
197
236
  * ```
198
237
  */
199
238
  setSearchParam(key: string, value?: string | number | null) {
200
- if (typeof value !== 'undefined' && value !== null) {
239
+ const deleteValue =
240
+ typeof value === 'undefined' ||
241
+ value === null ||
242
+ (typeof value === 'string' && value === '');
243
+
244
+ if (!deleteValue) {
201
245
  this.search.set(key, value as string);
202
246
  } else {
203
247
  this.search.delete(key);
204
248
  }
205
249
  }
206
250
 
251
+ /**
252
+ * Returns a state value if it exists.
253
+ */
254
+ getState<T = any>(key: string): T | null {
255
+ return this._state[key] ?? null;
256
+ }
257
+
258
+ /**
259
+ * Sets a state value.
260
+ * If the value is null or undefined, the key will be removed.
261
+ *
262
+ * ## When to use state
263
+ * State is used to store additional information that persists across route changes.
264
+ * The State is only available in the client code since it is stored using window.history.
265
+ *
266
+ * Consider using setSearchParam instead to enable server side rendering.
267
+ */
268
+ setState<T>(key: string, value: T | null | undefined) {
269
+ if (typeof value === 'undefined' || value === null) {
270
+ delete this._state[key];
271
+ } else {
272
+ this._state[key] = value;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Returns a context value if it exists.
278
+ */
279
+ getContext<T = any>(key: string): T | null {
280
+ return this._context[key] ?? null;
281
+ }
282
+
283
+ /**
284
+ * Sets a context value.
285
+ * If the value is null or undefined, the key will be removed.
286
+ *
287
+ * ## When to use context
288
+ * Context is used to pass data to onRoute and onRequest handlers or exchange data between loadData calls.
289
+ * This context is not persistent and should be considered valid only for the current request/route.
290
+ * The context is not cloned in the clone call and will be the same object.
291
+ */
292
+ setContext<T>(key: string, value: T | null | undefined) {
293
+ if (typeof value === 'undefined' || value === null) {
294
+ delete this._context[key];
295
+ } else {
296
+ this._context[key] = value;
297
+ }
298
+ }
299
+ /**
300
+ * Returns true if the route is in live preview mode
301
+ */
207
302
  inLivePreview(): boolean {
208
303
  return !!this.search.get('x-craft-live-preview');
209
304
  }
@@ -229,6 +324,9 @@ export default class Route {
229
324
  *
230
325
  * This checks all properties of the url but search params do not have to be
231
326
  * in the same order
327
+ *
328
+ * ## Note
329
+ * This does not check the state or context
232
330
  */
233
331
  eq(route: Route | null) {
234
332
  return (
@@ -288,6 +386,8 @@ export default class Route {
288
386
  scrollY: this.scrollY ?? undefined,
289
387
  index: this.index,
290
388
  origin: this.origin,
389
+ state: objClone(this._state),
390
+ context: this._context,
291
391
  });
292
392
  }
293
393
 
@@ -298,6 +398,10 @@ export default class Route {
298
398
 
299
399
  if (typeof state?.route?.index === 'number')
300
400
  this.index = state.route.index;
401
+
402
+ if (typeof state?.state === 'object' && state.state !== null) {
403
+ this._state = state.state;
404
+ }
301
405
  }
302
406
 
303
407
  /** @hidden */
@@ -307,6 +411,7 @@ export default class Route {
307
411
  scrollY: this.scrollY,
308
412
  index: this.index,
309
413
  },
414
+ state: this._state,
310
415
  };
311
416
  }
312
417
  }