crelte 0.4.8 → 0.5.0-alpha.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 (110) hide show
  1. package/dist/Crelte.d.ts +7 -6
  2. package/dist/Crelte.d.ts.map +1 -1
  3. package/dist/Crelte.js +5 -13
  4. package/dist/CrelteRequest.d.ts +9 -0
  5. package/dist/CrelteRequest.d.ts.map +1 -1
  6. package/dist/CrelteRequest.js +16 -2
  7. package/dist/blocks/Blocks.svelte +2 -2
  8. package/dist/blocks/Blocks.svelte.d.ts +3 -19
  9. package/dist/blocks/Blocks.svelte.d.ts.map +1 -1
  10. package/dist/cookies/ClientCookies.d.ts +0 -1
  11. package/dist/cookies/ClientCookies.d.ts.map +1 -1
  12. package/dist/cookies/ClientCookies.js +0 -1
  13. package/dist/cookies/ServerCookies.d.ts +1 -2
  14. package/dist/cookies/ServerCookies.d.ts.map +1 -1
  15. package/dist/cookies/ServerCookies.js +2 -6
  16. package/dist/cookies/index.d.ts +0 -2
  17. package/dist/cookies/index.d.ts.map +1 -1
  18. package/dist/graphql/GraphQl.d.ts +2 -2
  19. package/dist/graphql/GraphQl.d.ts.map +1 -1
  20. package/dist/index.d.ts +9 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +14 -6
  23. package/dist/init/InternalApp.d.ts +30 -0
  24. package/dist/init/InternalApp.d.ts.map +1 -0
  25. package/dist/init/InternalApp.js +71 -0
  26. package/dist/init/client.d.ts +0 -5
  27. package/dist/init/client.d.ts.map +1 -1
  28. package/dist/init/client.js +88 -75
  29. package/dist/init/crelte-vite-plugin.d.ts +5 -0
  30. package/dist/init/server.d.ts +0 -5
  31. package/dist/init/server.d.ts.map +1 -1
  32. package/dist/init/server.js +49 -20
  33. package/dist/init/shared.d.ts +7 -18
  34. package/dist/init/shared.d.ts.map +1 -1
  35. package/dist/init/shared.js +97 -154
  36. package/dist/init/svelteComponents.d.ts +3 -0
  37. package/dist/init/svelteComponents.d.ts.map +1 -0
  38. package/dist/init/svelteComponents.js +7 -0
  39. package/dist/loadData/Globals.d.ts +40 -33
  40. package/dist/loadData/Globals.d.ts.map +1 -1
  41. package/dist/loadData/Globals.js +99 -88
  42. package/dist/loadData/index.d.ts +3 -2
  43. package/dist/loadData/index.d.ts.map +1 -1
  44. package/dist/loadData/index.js +2 -0
  45. package/dist/plugins/Events.d.ts +11 -13
  46. package/dist/plugins/Events.d.ts.map +1 -1
  47. package/dist/plugins/Events.js +10 -3
  48. package/dist/routing/BaseRoute.d.ts +255 -0
  49. package/dist/routing/BaseRoute.d.ts.map +1 -0
  50. package/dist/routing/BaseRoute.js +349 -0
  51. package/dist/routing/BaseRouter.d.ts +210 -0
  52. package/dist/routing/BaseRouter.d.ts.map +1 -0
  53. package/dist/routing/BaseRouter.js +444 -0
  54. package/dist/routing/ClientRouter.d.ts +32 -0
  55. package/dist/routing/ClientRouter.d.ts.map +1 -0
  56. package/dist/routing/ClientRouter.js +259 -0
  57. package/dist/routing/LoadRunner.d.ts +39 -0
  58. package/dist/routing/LoadRunner.d.ts.map +1 -0
  59. package/dist/routing/{PageLoader.js → LoadRunner.js} +32 -20
  60. package/dist/routing/Request.d.ts +35 -3
  61. package/dist/routing/Request.d.ts.map +1 -1
  62. package/dist/routing/Request.js +64 -5
  63. package/dist/routing/Route.d.ts +24 -223
  64. package/dist/routing/Route.d.ts.map +1 -1
  65. package/dist/routing/Route.js +26 -315
  66. package/dist/routing/Router.d.ts +49 -73
  67. package/dist/routing/Router.d.ts.map +1 -1
  68. package/dist/routing/Router.js +85 -251
  69. package/dist/routing/ServerRouter.d.ts +23 -0
  70. package/dist/routing/ServerRouter.d.ts.map +1 -0
  71. package/dist/routing/ServerRouter.js +57 -0
  72. package/dist/routing/utils.d.ts +5 -0
  73. package/dist/routing/utils.d.ts.map +1 -1
  74. package/dist/routing/utils.js +39 -0
  75. package/dist/utils.d.ts +1 -0
  76. package/dist/utils.d.ts.map +1 -1
  77. package/dist/utils.js +3 -0
  78. package/package.json +7 -6
  79. package/src/Crelte.ts +12 -18
  80. package/src/CrelteRequest.ts +21 -2
  81. package/src/cookies/ClientCookies.ts +0 -2
  82. package/src/cookies/ServerCookies.ts +2 -7
  83. package/src/cookies/index.ts +0 -3
  84. package/src/graphql/GraphQl.ts +2 -1
  85. package/src/index.ts +17 -9
  86. package/src/init/InternalApp.ts +134 -0
  87. package/src/init/client.ts +104 -93
  88. package/src/init/crelte-vite-plugin.d.ts +5 -0
  89. package/src/init/server.ts +67 -35
  90. package/src/init/shared.ts +107 -227
  91. package/src/init/svelteComponents.ts +12 -0
  92. package/src/loadData/Globals.ts +121 -102
  93. package/src/loadData/index.ts +3 -2
  94. package/src/plugins/Events.ts +40 -42
  95. package/src/routing/BaseRoute.ts +422 -0
  96. package/src/routing/BaseRouter.ts +528 -0
  97. package/src/routing/ClientRouter.ts +329 -0
  98. package/src/routing/{PageLoader.ts → LoadRunner.ts} +43 -30
  99. package/src/routing/Request.ts +97 -12
  100. package/src/routing/Route.ts +56 -376
  101. package/src/routing/Router.ts +100 -359
  102. package/src/routing/ServerRouter.ts +78 -0
  103. package/src/routing/utils.ts +53 -0
  104. package/src/utils.ts +4 -0
  105. package/dist/routing/InnerRouter.d.ts +0 -113
  106. package/dist/routing/InnerRouter.d.ts.map +0 -1
  107. package/dist/routing/InnerRouter.js +0 -417
  108. package/dist/routing/PageLoader.d.ts +0 -36
  109. package/dist/routing/PageLoader.d.ts.map +0 -1
  110. package/src/routing/InnerRouter.ts +0 -498
@@ -0,0 +1,528 @@
1
+ /**
2
+ * This is the Router which we internally extend from
3
+ * it does not directly get exposed
4
+ */
5
+
6
+ import { Writable } from 'crelte-std/stores';
7
+ import Request, { isRequest, RequestOptions } from './Request.js';
8
+ import Route from './Route.js';
9
+ import Site, { SiteFromGraphQl, siteFromUrl } from './Site.js';
10
+ import { matchAcceptLang } from './utils.js';
11
+ import LoadRunner, { LoadRunnerOptions } from './LoadRunner.js';
12
+ import { Entry, type CrelteRequest } from '../index.js';
13
+ import { Listeners } from 'crelte-std/sync';
14
+ import { isPromise, objClone } from '../utils.js';
15
+
16
+ export type BaseRouterOptions = {} & LoadRunnerOptions;
17
+
18
+ const INF_LOOP_CHECK = '__REQ_FROM_REQ_START__';
19
+
20
+ export default class BaseRouter {
21
+ sites: Site[];
22
+ private sitesByLanguage: Map<string, Site>;
23
+
24
+ /**
25
+ * The current route
26
+ *
27
+ * ## Note
28
+ * Will always contain a route except in the first loadData call
29
+ */
30
+ route: Writable<Route | null>;
31
+
32
+ // todo should probably be a derived
33
+ /**
34
+ * The current site
35
+ *
36
+ * ## Note
37
+ * Will always contain a site except in the first loadData call
38
+ */
39
+ site: Writable<Site | null>;
40
+
41
+ // todo should probably be a derived
42
+ /**
43
+ * The current entry
44
+ *
45
+ * ## Note
46
+ * Will always contain an entry except in the first loadData call
47
+ */
48
+ entry: Writable<Entry | null>;
49
+
50
+ // the next request if it is not only a preload request
51
+ request: Request | null;
52
+
53
+ /**
54
+ * The loading flag, specifies if a page is currently
55
+ * getting loaded
56
+ */
57
+ loading: Writable<boolean>;
58
+
59
+ /**
60
+ * The loading progress, the value is between 0 and 1
61
+ */
62
+ loadingProgress: Writable<number>;
63
+
64
+ onNewCrelteRequest: (req: Request) => CrelteRequest;
65
+
66
+ /**
67
+ * Not sure the naming here is great but
68
+ */
69
+ onBeforeRequest: (cr: CrelteRequest) => Promise<void> | void;
70
+
71
+ onRequestListeners: Listeners<[CrelteRequest]>;
72
+
73
+ loadRunner: LoadRunner;
74
+
75
+ onRouteListeners: Listeners<[Route]>;
76
+
77
+ /// should return once the render is done
78
+ onRender: (
79
+ cr: CrelteRequest,
80
+ /**
81
+ * ## Throws
82
+ * if the route is missing entry, template or loadedData
83
+ */
84
+ readyForRoute: () => Route,
85
+ domUpdated: (cr: CrelteRequest, route: Route) => void,
86
+ ) => Promise<Route> | Route;
87
+
88
+ constructor(sites: SiteFromGraphQl[], opts: BaseRouterOptions) {
89
+ this.sites = sites.map(s => new Site(s));
90
+ this.sitesByLanguage = new Map(this.sites.map(s => [s.language, s]));
91
+ this.route = new Writable(null);
92
+ this.site = new Writable(null);
93
+ this.entry = new Writable(null);
94
+ this.request = null;
95
+ this.loading = new Writable(false);
96
+ this.loadingProgress = new Writable(0);
97
+ this.onNewCrelteRequest = () => null!;
98
+ this.onBeforeRequest = () => {};
99
+ this.onRequestListeners = new Listeners();
100
+ this.loadRunner = new LoadRunner(opts);
101
+ this.onRouteListeners = new Listeners();
102
+ this.onRender = async () => null!;
103
+
104
+ // todo move this to the client?
105
+ this.loadRunner.onProgress = (loading, progress) => {
106
+ if (this.loading.get() !== loading) this.loading.set(loading);
107
+
108
+ if (typeof progress === 'number')
109
+ this.loadingProgress.set(progress);
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Get the default site
115
+ */
116
+ primarySite(): Site {
117
+ return this.sites.find(s => s.primary) ?? this.sites[0];
118
+ }
119
+
120
+ /**
121
+ * Should be override by environment specific router
122
+ *
123
+ * This should be based on the language
124
+ *
125
+ * todo check that the router uses the correct sites for each function
126
+ */
127
+ defaultSite(): Site {
128
+ return this.primarySite();
129
+ }
130
+
131
+ /**
132
+ * Get a site and if possible use the accept lang header.
133
+ *
134
+ * @param acceptLang Accept Language header.
135
+ */
136
+ siteByAcceptLang(acceptLang: string | null = null): Site {
137
+ if (!acceptLang) return this.primarySite();
138
+
139
+ const lang = matchAcceptLang(
140
+ acceptLang,
141
+ Array.from(this.sitesByLanguage.keys()),
142
+ );
143
+
144
+ return lang ? this.sitesByLanguage.get(lang)! : this.primarySite();
145
+ }
146
+
147
+ /**
148
+ * Tries to get a site by it's id
149
+ */
150
+ siteById(id: number): Site | null {
151
+ return this.sites.find(s => s.id === id) ?? null;
152
+ }
153
+
154
+ // keep this doc in sync with Router.targetToRequest
155
+ /**
156
+ * Resolve a url or Route and convert it to a Request
157
+ *
158
+ * @param target
159
+ * @param opts, any option present will override the value in target
160
+ * @return Returns null if the url does not match our host (the protocol get's ignored)
161
+ */
162
+ targetToRequest(
163
+ target: string | URL | Route | Request,
164
+ opts: RequestOptions = {},
165
+ ): Request {
166
+ if (typeof target === 'string') {
167
+ if (target.startsWith('/')) {
168
+ // todo should we use the language matching or throw if the route does not
169
+ // exists
170
+ const site = this.route.get()?.site ?? this.primarySite();
171
+ target = new URL(site.uri + target, site.url);
172
+ } else if (!target) {
173
+ throw new Error('the url is not allowed to be empty');
174
+ } else {
175
+ target = new URL(target);
176
+ }
177
+ }
178
+
179
+ if (target instanceof URL) {
180
+ target = this.requestFromUrl(target);
181
+ }
182
+
183
+ if (!isRequest(target)) {
184
+ return Request.fromRoute(target, opts);
185
+ }
186
+
187
+ target._updateOpts(opts);
188
+ return target;
189
+ }
190
+
191
+ /**
192
+ * Resolve a url and convert it to a Request
193
+ *
194
+ * @param url
195
+ * @return Returns null if the url does not match our host (the protocol get's ignored)
196
+ */
197
+ requestFromUrl(fullUrl: URL): Request {
198
+ // strip stuff we dont need from url
199
+ const req = new Request(fullUrl, null!);
200
+
201
+ const site = siteFromUrl(req.url, this.sites);
202
+
203
+ // todo should we throw if we can't find a site
204
+ // or use the site which matches the language
205
+ req.site = site ?? this.primarySite();
206
+
207
+ return req;
208
+ }
209
+
210
+ /**
211
+ * Open a new route
212
+ *
213
+ * @param target the target to open can be an url, a route or a request
214
+ * the url needs to start with http or with a / which will be considered as
215
+ * the site baseUrl
216
+ *
217
+ * ## Note
218
+ * The origin will always be set to 'manual'
219
+ *
220
+ * ## Example
221
+ * ```
222
+ * import { getRouter } from 'crelte';
223
+ *
224
+ * const router = getRouter();
225
+ * console.log(router.site.get().url.href); // 'https://example.com/de';
226
+ *
227
+ * router.open('/foo/bar');
228
+ * // the following page will be opened https://example.com/de/foo/bar
229
+ * ```
230
+ */
231
+ async open(
232
+ target: string | URL | Route | Request,
233
+ opts: RequestOptions = {},
234
+ ): Promise<Route | void> {
235
+ const req = this.targetToRequest(target, {
236
+ ...opts,
237
+ origin: 'manual',
238
+ });
239
+ if (!req) return;
240
+
241
+ if (req === this.request) {
242
+ throw new Error(
243
+ 'Cannot open the same request object twice. Either clone the request ' +
244
+ 'or just pass in the url.',
245
+ );
246
+ }
247
+
248
+ try {
249
+ return await this.openRequest(req);
250
+ } catch (e) {
251
+ console.warn('opening route failed', e);
252
+ throw e;
253
+ }
254
+ }
255
+
256
+ async openRequest(_req: Request): Promise<Route | void> {
257
+ throw new Error('environment specific');
258
+ }
259
+
260
+ /**
261
+ * This pushes the new route without triggering a new pageload
262
+ *
263
+ * You can use this when using pagination for example change the route object
264
+ * (search argument) and then call push
265
+ *
266
+ * ## Note
267
+ * This will always set the origin to 'push'
268
+ * And will clear the scrollY value if you not provide a new one via the `opts`
269
+ * This will disableLoadData by default if you not provide an override via the `opts`
270
+ *
271
+ * ## Example using the update function
272
+ * ```
273
+ * import { getRouter } from 'crelte';
274
+ *
275
+ * const router = getRouter();
276
+ *
277
+ * const page = 1;
278
+ * router.push(req => req.setSearchParam('page', page || null));
279
+ * ```
280
+ *
281
+ * ## Example using the route object
282
+ * ```
283
+ * import { getRouter } from 'crelte';
284
+ *
285
+ * const router = getRouter();
286
+ *
287
+ * const page = 1;
288
+ * const route = router.route.get();
289
+ * route.setSearchParam('page', page > 0 ? page : null);
290
+ * router.push(route);
291
+ * ```
292
+ */
293
+ async push(route: Route | Request, opts: RequestOptions = {}) {
294
+ // theoretically string and URL also work but we might
295
+ // change that in the future
296
+ const req = this.targetToRequest(route, {
297
+ ...opts,
298
+ origin: 'push',
299
+ scrollY: opts.scrollY ?? undefined,
300
+ disableLoadData: opts.disableLoadData ?? true,
301
+ });
302
+
303
+ if (!req) return;
304
+
305
+ try {
306
+ return await this.pushRequest(req, opts);
307
+ } catch (e) {
308
+ console.warn('pushing route failed', e);
309
+ throw e;
310
+ }
311
+ }
312
+
313
+ pushRequest(
314
+ _req: Request,
315
+ _opts: RequestOptions = {},
316
+ ): Promise<Route | void> {
317
+ throw new Error('environment specific');
318
+ }
319
+
320
+ /**
321
+ * This replaces the state of the route without triggering a new pageload
322
+ *
323
+ * You can use this when using some filters for example a search filter
324
+ *
325
+ * ## Note
326
+ * This will always set the origin to 'replace'
327
+ * And will clear the scrollY value if you don't provide a new one via the `opts`
328
+ * This will disableLoadData by default if you don't provide an override via the `opts`
329
+ *
330
+ * ## Example using the update function
331
+ * ```
332
+ * import { getRouter } from 'crelte';
333
+ *
334
+ * const router = getRouter();
335
+ *
336
+ * const search = 'foo';
337
+ * router.replace(req => req.setSearchParam('search', search));
338
+ * ```
339
+ *
340
+ * ## Example using the route object
341
+ * ```
342
+ * import { getRouter } from 'crelte';
343
+ *
344
+ * const router = getRouter();
345
+ *
346
+ * const search = 'foo';
347
+ * const route = router.route.get();
348
+ * route.setSearchParam('search', search);
349
+ * router.replace(route);
350
+ * ```
351
+ */
352
+ async replace(route: Route | Request, opts: RequestOptions = {}) {
353
+ // theoretically string and URL also work but we might
354
+ // change that in the future
355
+ const req = this.targetToRequest(route, {
356
+ ...opts,
357
+ origin: 'replace',
358
+ scrollY: opts.scrollY ?? undefined,
359
+ disableLoadData: opts.disableLoadData ?? true,
360
+ });
361
+ if (!req) return;
362
+
363
+ try {
364
+ return await this.replaceRequest(req, opts);
365
+ } catch (e) {
366
+ console.warn('replacing route failed', e);
367
+ throw e;
368
+ }
369
+ }
370
+
371
+ async replaceRequest(
372
+ _req: Request,
373
+ _opts: RequestOptions = {},
374
+ ): Promise<Route | void> {
375
+ throw new Error('environment specific');
376
+ }
377
+
378
+ /**
379
+ * Checks if there are previous routes which would allow it to go back
380
+ */
381
+ canGoBack(): boolean {
382
+ throw new Error('environment specific');
383
+ // return this.route.get()?.canGoBack() ?? false;
384
+ }
385
+
386
+ /**
387
+ * Go back in the history
388
+ */
389
+ back() {
390
+ throw new Error('environment specific');
391
+ // this.inner.history.back();
392
+ }
393
+
394
+ async preload(target: string | URL | Route | Request) {
395
+ const req = this.targetToRequest(target);
396
+ const current = this.route.get();
397
+
398
+ // if the origin matches, the route will be able to be load
399
+ // so let's preload it
400
+ if (current && current.url.origin === req.url.origin) {
401
+ // todo i don't wan't to send a CrelteRequest?
402
+ this.loadRunner.preload(this.onNewCrelteRequest(req));
403
+ }
404
+ }
405
+
406
+ cancelRequest() {
407
+ // destroy the old request
408
+ if (this.request) {
409
+ this.request._cancel();
410
+ this.request = null;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * ## Throws
416
+ * If the request completed but had an error
417
+ */
418
+ async handleRequest(
419
+ req: Request,
420
+ updateHistory: (route: Route) => void,
421
+ ): Promise<Route | void> {
422
+ // this isCancelled check is not if a user cancelled the request
423
+ // but if the router cancelled the request, because for example
424
+ // the user clicked on a new link or an event decided to call open
425
+ const isCancelled = () => req.cancelled;
426
+
427
+ // cancel the previous request
428
+ this.cancelRequest();
429
+
430
+ const barrier = req._renderBarrier;
431
+ if (barrier.isOpen()) throw new Error('the request was already used');
432
+
433
+ // not sure this really helps
434
+ // it should be in open maybe?
435
+ if (req.getContext(INF_LOOP_CHECK))
436
+ throw new Error('infinite loop detected');
437
+
438
+ this.request = req;
439
+ const cr = this.onNewCrelteRequest(req);
440
+
441
+ // trigger event onRequestStart
442
+ const onBeforeReqProm = this.onBeforeRequest(cr);
443
+ if (isPromise(onBeforeReqProm)) await onBeforeReqProm;
444
+ if (isCancelled()) return;
445
+
446
+ // if the request does not have a matching site, redirect
447
+ if (!req.siteMatches()) {
448
+ const site = this.defaultSite();
449
+ const req = new Request(site.url, site);
450
+ req.setContext(INF_LOOP_CHECK, true);
451
+ return await this.openRequest(req);
452
+ }
453
+
454
+ // trigger onRequest listeners (this is not an event and more intended
455
+ // to be used by the actual site and not a plugin)
456
+ this.onRequestListeners.trigger(cr);
457
+ if (isCancelled()) return;
458
+
459
+ //!! this block might throw if something did not work as expected
460
+ // todo do we wan't this?
461
+ if (!req.disableLoadData) {
462
+ const completed = await this.loadRunner.load(cr);
463
+ // the request was succeeded by some other request
464
+ if (!completed) return;
465
+ } else {
466
+ // just discard the old one since nobody will wan't it
467
+ this.loadRunner.discard();
468
+ this.copyEntryToRequest(req);
469
+ }
470
+
471
+ // check if the render was cancelled
472
+ // else wait until the renderBarrier gets opened
473
+ const readyProm = req._renderBarrier.ready();
474
+ const wasCancelled = isPromise(readyProm) ? await readyProm : readyProm;
475
+ if (wasCancelled || isCancelled()) return;
476
+
477
+ // the onRender should decide by itself if it wants to render or not
478
+ // for example in most cases if !entryChanged it does not make sense to render
479
+ return await this.onRender(
480
+ cr,
481
+ // readyForRoute
482
+ () => {
483
+ // throws if the route is missing entry, template or loadedData
484
+ // or the request was cancelled
485
+ const route = req.toRoute();
486
+
487
+ updateHistory(route);
488
+
489
+ // update route, site and onRoute
490
+ this.triggerRoute(route);
491
+
492
+ return route;
493
+ },
494
+ // domUpdated
495
+ // we use a callback to maybe decrease latency?
496
+ (cr, route) => {
497
+ this.updateScroll(cr, route);
498
+ },
499
+ );
500
+ }
501
+
502
+ copyEntryToRequest(req: Request) {
503
+ const route = this.route.get();
504
+ if (!route)
505
+ throw new Error(
506
+ 'the first request is not allowed to disableLoadData',
507
+ );
508
+
509
+ req.entry = objClone(route.entry);
510
+ req.template = route.template;
511
+ req.loadedData = objClone(route.loadedData);
512
+ }
513
+
514
+ triggerRoute(route: Route) {
515
+ this.route.setSilent(route);
516
+ const siteChanged = this.site.get()?.id !== route.site.id;
517
+ this.site.setSilent(route.site);
518
+ this.entry.setSilent(route.entry);
519
+
520
+ // trigger an update
521
+ this.route.notify();
522
+ if (siteChanged) this.site.notify();
523
+ if (route.entryChanged) this.entry.notify();
524
+ this.onRouteListeners.trigger(route);
525
+ }
526
+
527
+ updateScroll(_cr: CrelteRequest, _route: Route) {}
528
+ }