crelte 0.2.2 → 0.3.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 (55) hide show
  1. package/dist/Crelte.d.ts +61 -9
  2. package/dist/Crelte.d.ts.map +1 -1
  3. package/dist/Crelte.js +42 -10
  4. package/dist/CrelteRequest.d.ts +3 -11
  5. package/dist/CrelteRequest.d.ts.map +1 -1
  6. package/dist/CrelteRequest.js +9 -19
  7. package/dist/graphql/GraphQl.d.ts +7 -0
  8. package/dist/graphql/GraphQl.d.ts.map +1 -1
  9. package/dist/graphql/GraphQl.js +16 -3
  10. package/dist/index.d.ts +10 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -1
  13. package/dist/init/client.d.ts +0 -19
  14. package/dist/init/client.d.ts.map +1 -1
  15. package/dist/init/client.js +9 -12
  16. package/dist/init/server.d.ts +0 -4
  17. package/dist/init/server.d.ts.map +1 -1
  18. package/dist/init/server.js +2 -5
  19. package/dist/init/shared.d.ts.map +1 -1
  20. package/dist/init/shared.js +8 -8
  21. package/dist/loadData/Globals.d.ts +15 -31
  22. package/dist/loadData/Globals.d.ts.map +1 -1
  23. package/dist/loadData/Globals.js +65 -72
  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 +28 -23
  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 +56 -1
  31. package/dist/routing/Route.d.ts.map +1 -1
  32. package/dist/routing/Route.js +85 -2
  33. package/dist/routing/Router.d.ts +29 -4
  34. package/dist/routing/Router.d.ts.map +1 -1
  35. package/dist/routing/Router.js +39 -12
  36. package/dist/ssr/SsrCache.d.ts.map +1 -1
  37. package/dist/ssr/SsrCache.js +6 -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 +2 -2
  42. package/src/Crelte.ts +95 -13
  43. package/src/CrelteRequest.ts +14 -27
  44. package/src/graphql/GraphQl.ts +25 -6
  45. package/src/index.ts +19 -8
  46. package/src/init/client.ts +9 -40
  47. package/src/init/server.ts +2 -13
  48. package/src/init/shared.ts +8 -9
  49. package/src/loadData/Globals.ts +76 -93
  50. package/src/routing/InnerRouter.ts +38 -32
  51. package/src/routing/Request.ts +11 -0
  52. package/src/routing/Route.ts +93 -2
  53. package/src/routing/Router.ts +51 -20
  54. package/src/ssr/SsrCache.ts +6 -1
  55. package/src/utils.ts +10 -0
@@ -4,25 +4,30 @@ const emergency = getGlobal('emergency');
4
4
 
5
5
  // returns the data based on the current site (no store)
6
6
  cr.getGlobal('emergency')
7
+
7
8
  */
8
9
 
9
10
  import { Writable } from 'crelte-std/stores';
10
11
 
11
- export type GlobalWaiters = [(g: Global<any> | null) => void];
12
+ export type GlobalWaiters<T> = [(g: T | null) => void];
12
13
 
13
14
  export default class Globals {
14
15
  // while the globals are not loaded if somebody calls
15
- // getOrWait then we need to store the waiters
16
- private waiters: Map<string, GlobalWaiters>;
17
- private entries: Map<string, Global<any>>;
18
- private loaded: boolean;
19
- private prevSiteId: number | null;
16
+ // getAsync then we need to store the waiters
17
+ private waiters: Map<number, Map<string, GlobalWaiters<any>>>;
18
+ private data: Map<number, Map<string, any>>;
19
+ private stores: Map<string, Global<any>>;
20
+ private currentSiteId: number | null;
20
21
 
21
22
  constructor() {
22
23
  this.waiters = new Map();
23
- this.entries = new Map();
24
- this.loaded = false;
25
- this.prevSiteId = null;
24
+ this.data = new Map();
25
+ this.stores = new Map();
26
+ this.currentSiteId = null;
27
+ }
28
+
29
+ get<T = any>(name: string, siteId: number): T | null {
30
+ return this.data.get(siteId)?.get(name) ?? null;
26
31
  }
27
32
 
28
33
  /**
@@ -31,10 +36,10 @@ export default class Globals {
31
36
  * ## Note
32
37
  * This only works in loadData, in loadGlobalData this will
33
38
  * always return null. In that context you should use
34
- * `.getGlobalAsync`
39
+ * `.getAsync`
35
40
  */
36
- get<T extends GlobalData>(name: string): Global<T> | null {
37
- return this.entries.get(name) ?? null;
41
+ getStore<T = any>(name: string): Global<T> | null {
42
+ return this.stores.get(name) ?? null;
38
43
  }
39
44
 
40
45
  /**
@@ -44,15 +49,20 @@ export default class Globals {
44
49
  * This is only useful in loadGlobalData in all other cases
45
50
  * you can use `.getGlobal` which does return a Promise
46
51
  */
47
- getAsync<T extends GlobalData>(
48
- name: string,
49
- ): Promise<Global<T> | null> | Global<T> | null {
50
- if (this.loaded) return this.get(name);
52
+ getAsync<T = any>(name: string, siteId: number): Promise<T | null> {
53
+ if (this._wasLoaded(siteId))
54
+ return Promise.resolve(this.get(name, siteId));
55
+
56
+ let listeners = this.waiters.get(siteId);
57
+ if (!listeners) {
58
+ listeners = new Map();
59
+ this.waiters.set(siteId, listeners);
60
+ }
51
61
 
52
- let waiter = this.waiters.get(name);
62
+ let waiter = listeners.get(name);
53
63
  if (!waiter) {
54
64
  waiter = [] as any;
55
- this.waiters.set(name, waiter!);
65
+ listeners.set(name, waiter!);
56
66
  }
57
67
 
58
68
  return new Promise(resolve => {
@@ -61,88 +71,73 @@ export default class Globals {
61
71
  }
62
72
 
63
73
  /** @hidden */
64
- _wasLoaded(): boolean {
65
- return this.loaded;
74
+ _wasLoaded(siteId: number): boolean {
75
+ return this.data.has(siteId);
66
76
  }
67
77
 
68
78
  // data is the data from the global graphql
69
79
  // so it contains some keys and data which should be parsed
70
80
  // and created a store for each key
81
+ // do not call this if _wasLoaded returns true with the same siteId
71
82
  /** @hidden */
72
83
  _setData(siteId: number, data: any) {
73
- const wasLoaded = this.loaded;
74
- this.loaded = true;
84
+ const map = new Map(Object.entries(data));
85
+ this.data.set(siteId, map);
75
86
 
76
- for (const [key, value] of Object.entries(data)) {
77
- this.entries.set(key, new Global(key, value as any, siteId));
78
- }
79
-
80
- if (!wasLoaded) {
81
- this.waiters.forEach((waiters, key) => {
82
- waiters.forEach(waiter => waiter(this.get(key)));
83
- });
84
- this.waiters.clear();
85
- }
87
+ this.waiters.get(siteId)?.forEach((waiters, key) => {
88
+ waiters.forEach(waiter => waiter(map.get(key)));
89
+ });
90
+ this.waiters.delete(siteId);
86
91
  }
87
92
 
88
93
  /** @hidden */
89
- _globalsBySite(siteId: number): Map<string, any> {
90
- const map = new Map();
94
+ _updateSiteId(siteId: number) {
95
+ if (this.currentSiteId === siteId) return;
91
96
 
92
- for (const [key, global] of this.entries) {
93
- map.set(key, global.bySiteId(siteId));
94
- }
97
+ const data = this.data.get(siteId) ?? new Map();
95
98
 
96
- return map;
97
- }
99
+ // we set all global data to null via setSilent
100
+ // then set them all with the new data
101
+ // and update all of them
98
102
 
99
- /** @hidden */
100
- _updateSiteId(siteId: number) {
101
- // todo we should only trigger
102
- if (this.prevSiteId === siteId) return;
103
+ this.stores.forEach(global => global._setSilent(null));
103
104
 
104
- this.entries.forEach(global => global._updateSiteId(siteId));
105
- }
106
- }
105
+ data.forEach((value, key) => {
106
+ let global = this.stores.get(key);
107
+ if (global) {
108
+ global._setSilent(value);
109
+ } else {
110
+ global = new Global(key, value);
111
+ this.stores.set(key, global);
112
+ }
113
+ });
107
114
 
108
- /**
109
- * A globalSet Data
110
- *
111
- * Each global query should contain the siteId
112
- */
113
- export interface GlobalData {
114
- siteId?: number;
115
- [key: string]: any;
115
+ this.stores.forEach(global => global._notify());
116
+ }
116
117
  }
117
118
 
118
119
  /**
119
120
  * A globalSet store
120
121
  */
121
- export class Global<T extends GlobalData> {
122
+ export class Global<T = any> {
123
+ /** @hidden */
122
124
  private inner: Writable<T>;
123
- /// if languages is null this means we always have the same data
124
- private languages: T[] | null;
125
-
126
- constructor(name: string, data: T[] | T, siteId: number) {
127
- this.languages = null;
128
-
129
- let inner: T;
130
- if (Array.isArray(data)) {
131
- // make sure the data contains an object with the property
132
- // siteId
133
- this.languages = data;
134
- inner = data.find(d => d.siteId === siteId)!;
135
-
136
- if (!inner?.siteId) {
137
- throw new Error(
138
- `The global query ${name} does not contain the required siteId property`,
139
- );
140
- }
141
- } else {
142
- inner = data;
125
+
126
+ constructor(name: string, data: T) {
127
+ // todo remove in v1.0
128
+ // In v0.2, we queried the global data for all sites.
129
+ // We now check if the siteId is present and notify the user to remove it.
130
+ if (
131
+ typeof (data as any).siteId === 'number' ||
132
+ (Array.isArray(data) && typeof data[0]?.siteId === 'number')
133
+ ) {
134
+ throw new Error(
135
+ `The global query ${name} should not include the siteId` +
136
+ ` property. Instead, use the siteId as a parameter.`,
137
+ );
143
138
  }
144
139
 
145
- this.inner = new Writable(inner);
140
+ this.inner = new Writable(data);
146
141
  }
147
142
 
148
143
  /**
@@ -162,25 +157,13 @@ export class Global<T extends GlobalData> {
162
157
  return this.inner.get();
163
158
  }
164
159
 
165
- /**
166
- * Get the value based on the siteId
167
- *
168
- * ## Note
169
- * If you pass a siteId which comes from craft
170
- * you will never receive null
171
- */
172
- bySiteId(siteId: number): T | null {
173
- if (this.languages)
174
- return this.languages.find(d => d.siteId === siteId) ?? null;
175
-
176
- return this.inner.get();
160
+ /** @hidden */
161
+ _setSilent(value: T) {
162
+ this.inner.setSilent(value);
177
163
  }
178
164
 
179
165
  /** @hidden */
180
- _updateSiteId(siteId: number) {
181
- if (!this.languages) return;
182
-
183
- const inner = this.languages.find(d => d.siteId === siteId);
184
- this.inner.set(inner!);
166
+ _notify() {
167
+ this.inner.notify();
185
168
  }
186
169
  }
@@ -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
  /**
@@ -133,6 +136,7 @@ export default class InnerRouter {
133
136
  return this.sites.find(s => s.id === id) ?? null;
134
137
  }
135
138
 
139
+ // keep this doc in sync with Router.targetToRequest
136
140
  /**
137
141
  * Resolve a url or Route and convert it to a Request
138
142
  *
@@ -150,6 +154,8 @@ export default class InnerRouter {
150
154
  // exists
151
155
  const site = this.route?.site ?? this.defaultSite();
152
156
  target = new URL(site.uri + target, site.url);
157
+ } else if (!target) {
158
+ throw new Error('the url is not allowed to be empty');
153
159
  } else {
154
160
  target = new URL(target);
155
161
  }
@@ -242,7 +248,11 @@ export default class InnerRouter {
242
248
  if (currentMouseOver && link === currentMouseOver) return;
243
249
  if (link && link.target.toLowerCase() === '_blank') return;
244
250
 
245
- if (link && !link.hasAttribute('data-no-preload')) {
251
+ if (
252
+ link &&
253
+ !link.hasAttribute('data-no-preload') &&
254
+ link.href
255
+ ) {
246
256
  this.preload(link.href);
247
257
  }
248
258
 
@@ -288,13 +298,16 @@ export default class InnerRouter {
288
298
  }
289
299
 
290
300
  window.addEventListener('popstate', async e => {
291
- if (!('route' in e.state)) return;
301
+ if (!e.state?.route) return;
292
302
 
293
303
  const req = this.targetToRequest(window.location.href);
294
304
  req._fillFromState(e.state);
295
305
  req.origin = 'pop';
296
306
 
297
- 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, () => {});
298
311
  });
299
312
  }
300
313
 
@@ -343,24 +356,15 @@ export default class InnerRouter {
343
356
 
344
357
  req.index = (current?.index ?? 0) + 1;
345
358
  this.onRoute(req, () => {
346
- 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();
347
365
  });
348
366
  }
349
367
 
350
- /**
351
- * Sets a route
352
- *
353
- * Will trigger an onRoute event but will not store any scroll progress
354
- * or modify the history
355
- *
356
- * @param req
357
- */
358
- setRoute(req: Request, preventOnRoute = false) {
359
- this.route = req.toRoute();
360
-
361
- if (!preventOnRoute) this.onRoute(req, () => {});
362
- }
363
-
364
368
  /**
365
369
  * This pushes a new route to the history
366
370
  *
@@ -369,7 +373,7 @@ export default class InnerRouter {
369
373
  * ## Important
370
374
  * Make sure the route has the correct origin
371
375
  */
372
- push(req: Request, preventOnRoute = false) {
376
+ push(req: Request) {
373
377
  const url = req.url;
374
378
  // todo a push should also store the previous scrollY
375
379
 
@@ -383,12 +387,13 @@ export default class InnerRouter {
383
387
  nReq.scrollY = this.history.scrollY();
384
388
  }
385
389
 
386
- this.history.pushState(
387
- nReq._toState(),
388
- url.pathname + url.search + url.hash,
389
- );
390
-
391
- 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
+ });
392
397
  }
393
398
 
394
399
  /**
@@ -413,12 +418,13 @@ export default class InnerRouter {
413
418
  nReq.scrollY = this.history.scrollY();
414
419
  }
415
420
 
416
- this.history.replaceState(
417
- nReq._toState(),
418
- url.pathname + url.search + url.hash,
419
- );
420
-
421
- 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
+ });
422
428
  }
423
429
 
424
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
  /**
@@ -184,7 +208,8 @@ export default class Route {
184
208
  }
185
209
 
186
210
  /**
187
- * Sets the search param or removes it if the value is null or undefined
211
+ * Sets the search param or removes it if the value is null, undefined or an
212
+ * empty string
188
213
  *
189
214
  * ## Example
190
215
  * ```
@@ -197,13 +222,69 @@ export default class Route {
197
222
  * ```
198
223
  */
199
224
  setSearchParam(key: string, value?: string | number | null) {
200
- if (typeof value !== 'undefined' && value !== null) {
225
+ const deleteValue =
226
+ typeof value === 'undefined' ||
227
+ value === null ||
228
+ (typeof value === 'string' && value === '');
229
+
230
+ if (!deleteValue) {
201
231
  this.search.set(key, value as string);
202
232
  } else {
203
233
  this.search.delete(key);
204
234
  }
205
235
  }
206
236
 
237
+ /**
238
+ * Returns a state value if it exists.
239
+ */
240
+ getState<T = any>(key: string): T | null {
241
+ return this._state[key] ?? null;
242
+ }
243
+
244
+ /**
245
+ * Sets a state value.
246
+ * If the value is null or undefined, the key will be removed.
247
+ *
248
+ * ## When to use state
249
+ * State is used to store additional information that persists across route changes.
250
+ * The State is only available in the client code since it is stored using window.history.
251
+ *
252
+ * Consider using setSearchParam instead to enable server side rendering.
253
+ */
254
+ setState<T>(key: string, value: T | null | undefined) {
255
+ if (typeof value === 'undefined' || value === null) {
256
+ delete this._state[key];
257
+ } else {
258
+ this._state[key] = value;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Returns a context value if it exists.
264
+ */
265
+ getContext<T = any>(key: string): T | null {
266
+ return this._context[key] ?? null;
267
+ }
268
+
269
+ /**
270
+ * Sets a context value.
271
+ * If the value is null or undefined, the key will be removed.
272
+ *
273
+ * ## When to use context
274
+ * Context is used to pass data to onRoute and onRequest handlers or exchange data between loadData calls.
275
+ * This context is not persistent and should be considered valid only for the current request/route.
276
+ * The context is not cloned in the clone call and will be the same object.
277
+ */
278
+ setContext<T>(key: string, value: T | null | undefined) {
279
+ if (typeof value === 'undefined' || value === null) {
280
+ delete this._context[key];
281
+ } else {
282
+ this._context[key] = value;
283
+ }
284
+ }
285
+ /**
286
+ * Returns true if the route is in live preview mode
287
+ */
207
288
  inLivePreview(): boolean {
208
289
  return !!this.search.get('x-craft-live-preview');
209
290
  }
@@ -229,6 +310,9 @@ export default class Route {
229
310
  *
230
311
  * This checks all properties of the url but search params do not have to be
231
312
  * in the same order
313
+ *
314
+ * ## Note
315
+ * This does not check the state or context
232
316
  */
233
317
  eq(route: Route | null) {
234
318
  return (
@@ -288,6 +372,8 @@ export default class Route {
288
372
  scrollY: this.scrollY ?? undefined,
289
373
  index: this.index,
290
374
  origin: this.origin,
375
+ state: objClone(this._state),
376
+ context: this._context,
291
377
  });
292
378
  }
293
379
 
@@ -298,6 +384,10 @@ export default class Route {
298
384
 
299
385
  if (typeof state?.route?.index === 'number')
300
386
  this.index = state.route.index;
387
+
388
+ if (typeof state?.state === 'object' && state.state !== null) {
389
+ this._state = state.state;
390
+ }
301
391
  }
302
392
 
303
393
  /** @hidden */
@@ -307,6 +397,7 @@ export default class Route {
307
397
  scrollY: this.scrollY,
308
398
  index: this.index,
309
399
  },
400
+ state: this._state,
310
401
  };
311
402
  }
312
403
  }