crelte 0.1.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.
Files changed (130) hide show
  1. package/LICENSE.md +41 -0
  2. package/dist/Crelte.d.ts +55 -0
  3. package/dist/Crelte.d.ts.map +1 -0
  4. package/dist/Crelte.js +106 -0
  5. package/dist/CrelteBase.d.ts +16 -0
  6. package/dist/CrelteBase.d.ts.map +1 -0
  7. package/dist/CrelteBase.js +1 -0
  8. package/dist/CrelteRouted.d.ts +50 -0
  9. package/dist/CrelteRouted.d.ts.map +1 -0
  10. package/dist/CrelteRouted.js +88 -0
  11. package/dist/blocks/Blocks.d.ts +35 -0
  12. package/dist/blocks/Blocks.d.ts.map +1 -0
  13. package/dist/blocks/Blocks.js +100 -0
  14. package/dist/blocks/Blocks.svelte +21 -0
  15. package/dist/blocks/Blocks.svelte.d.ts +24 -0
  16. package/dist/blocks/Blocks.svelte.d.ts.map +1 -0
  17. package/dist/blocks/index.d.ts +5 -0
  18. package/dist/blocks/index.d.ts.map +1 -0
  19. package/dist/blocks/index.js +3 -0
  20. package/dist/cookies/ClientCookies.d.ts +9 -0
  21. package/dist/cookies/ClientCookies.d.ts.map +1 -0
  22. package/dist/cookies/ClientCookies.js +22 -0
  23. package/dist/cookies/ServerCookies.d.ts +13 -0
  24. package/dist/cookies/ServerCookies.d.ts.map +1 -0
  25. package/dist/cookies/ServerCookies.js +31 -0
  26. package/dist/cookies/index.d.ts +20 -0
  27. package/dist/cookies/index.d.ts.map +1 -0
  28. package/dist/cookies/index.js +1 -0
  29. package/dist/cookies/utils.d.ts +12 -0
  30. package/dist/cookies/utils.d.ts.map +1 -0
  31. package/dist/cookies/utils.js +32 -0
  32. package/dist/graphql/GraphQl.d.ts +60 -0
  33. package/dist/graphql/GraphQl.d.ts.map +1 -0
  34. package/dist/graphql/GraphQl.js +197 -0
  35. package/dist/graphql/gql.test.d.ts +2 -0
  36. package/dist/graphql/gql.test.d.ts.map +1 -0
  37. package/dist/graphql/gql.test.js +80 -0
  38. package/dist/graphql/index.d.ts +3 -0
  39. package/dist/graphql/index.d.ts.map +1 -0
  40. package/dist/graphql/index.js +2 -0
  41. package/dist/index.d.ts +67 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +83 -0
  44. package/dist/init/client.d.ts +13 -0
  45. package/dist/init/client.d.ts.map +1 -0
  46. package/dist/init/client.js +129 -0
  47. package/dist/init/server.d.ts +38 -0
  48. package/dist/init/server.d.ts.map +1 -0
  49. package/dist/init/server.js +95 -0
  50. package/dist/init/shared.d.ts +29 -0
  51. package/dist/init/shared.d.ts.map +1 -0
  52. package/dist/init/shared.js +154 -0
  53. package/dist/loadData/Globals.d.ts +33 -0
  54. package/dist/loadData/Globals.d.ts.map +1 -0
  55. package/dist/loadData/Globals.js +119 -0
  56. package/dist/loadData/index.d.ts +25 -0
  57. package/dist/loadData/index.d.ts.map +1 -0
  58. package/dist/loadData/index.js +39 -0
  59. package/dist/plugins/Events.d.ts +11 -0
  60. package/dist/plugins/Events.d.ts.map +1 -0
  61. package/dist/plugins/Events.js +29 -0
  62. package/dist/plugins/Plugins.d.ts +12 -0
  63. package/dist/plugins/Plugins.d.ts.map +1 -0
  64. package/dist/plugins/Plugins.js +12 -0
  65. package/dist/plugins/index.d.ts +5 -0
  66. package/dist/plugins/index.d.ts.map +1 -0
  67. package/dist/plugins/index.js +2 -0
  68. package/dist/routing/History.d.ts +22 -0
  69. package/dist/routing/History.d.ts.map +1 -0
  70. package/dist/routing/History.js +36 -0
  71. package/dist/routing/InnerRouter.d.ts +111 -0
  72. package/dist/routing/InnerRouter.d.ts.map +1 -0
  73. package/dist/routing/InnerRouter.js +397 -0
  74. package/dist/routing/PageLoader.d.ts +37 -0
  75. package/dist/routing/PageLoader.d.ts.map +1 -0
  76. package/dist/routing/PageLoader.js +72 -0
  77. package/dist/routing/Route.d.ts +82 -0
  78. package/dist/routing/Route.d.ts.map +1 -0
  79. package/dist/routing/Route.js +134 -0
  80. package/dist/routing/Router.d.ts +162 -0
  81. package/dist/routing/Router.d.ts.map +1 -0
  82. package/dist/routing/Router.js +333 -0
  83. package/dist/routing/Site.d.ts +47 -0
  84. package/dist/routing/Site.d.ts.map +1 -0
  85. package/dist/routing/Site.js +48 -0
  86. package/dist/routing/index.d.ts +5 -0
  87. package/dist/routing/index.d.ts.map +1 -0
  88. package/dist/routing/index.js +4 -0
  89. package/dist/ssr/SsrCache.d.ts +12 -0
  90. package/dist/ssr/SsrCache.d.ts.map +1 -0
  91. package/dist/ssr/SsrCache.js +50 -0
  92. package/dist/ssr/SsrComponents.d.ts +7 -0
  93. package/dist/ssr/SsrComponents.d.ts.map +1 -0
  94. package/dist/ssr/SsrComponents.js +30 -0
  95. package/dist/ssr/index.d.ts +4 -0
  96. package/dist/ssr/index.d.ts.map +1 -0
  97. package/dist/ssr/index.js +3 -0
  98. package/package.json +79 -0
  99. package/src/Crelte.ts +135 -0
  100. package/src/CrelteBase.ts +24 -0
  101. package/src/CrelteRouted.ts +128 -0
  102. package/src/blocks/Blocks.svelte +68 -0
  103. package/src/blocks/Blocks.ts +155 -0
  104. package/src/blocks/index.ts +14 -0
  105. package/src/cookies/ClientCookies.ts +30 -0
  106. package/src/cookies/ServerCookies.ts +42 -0
  107. package/src/cookies/index.ts +24 -0
  108. package/src/cookies/utils.ts +53 -0
  109. package/src/graphql/GraphQl.ts +281 -0
  110. package/src/graphql/gql.test.ts +123 -0
  111. package/src/graphql/index.ts +8 -0
  112. package/src/index.ts +109 -0
  113. package/src/init/client.ts +190 -0
  114. package/src/init/server.ts +177 -0
  115. package/src/init/shared.ts +221 -0
  116. package/src/loadData/Globals.ts +150 -0
  117. package/src/loadData/index.ts +67 -0
  118. package/src/plugins/Events.ts +50 -0
  119. package/src/plugins/Plugins.ts +23 -0
  120. package/src/plugins/index.ts +5 -0
  121. package/src/routing/History.ts +52 -0
  122. package/src/routing/InnerRouter.ts +469 -0
  123. package/src/routing/PageLoader.ts +112 -0
  124. package/src/routing/Route.ts +184 -0
  125. package/src/routing/Router.ts +476 -0
  126. package/src/routing/Site.ts +65 -0
  127. package/src/routing/index.ts +5 -0
  128. package/src/ssr/SsrCache.ts +61 -0
  129. package/src/ssr/SsrComponents.ts +34 -0
  130. package/src/ssr/index.ts +4 -0
@@ -0,0 +1,177 @@
1
+ import { CrelteBuilder } from '../Crelte.js';
2
+ import { SiteFromGraphQl } from '../routing/Site.js';
3
+ import { loadFn, pluginsBeforeRender, setupPlugins } from './shared.js';
4
+ import SsrComponents from '../ssr/SsrComponents.js';
5
+ import SsrCache from '../ssr/SsrCache.js';
6
+ import ServerCookies from '../cookies/ServerCookies.js';
7
+
8
+ export type ServerData = {
9
+ url: string;
10
+ htmlTemplate: string;
11
+ ssrManifest: Record<string, string[]>;
12
+ acceptLang?: string;
13
+ endpoint: string;
14
+ craftWeb: string;
15
+ viteEnv: Map<string, string>;
16
+ cookies?: string;
17
+ };
18
+
19
+ export type MainData = {
20
+ /// svelte app component
21
+ app: any;
22
+ entryQuery: any;
23
+ globalQuery?: any;
24
+
25
+ serverData: ServerData;
26
+
27
+ // debug
28
+ graphQlDebug?: boolean;
29
+ debugTiming?: boolean;
30
+ };
31
+
32
+ export async function main(data: MainData): Promise<{
33
+ status: number;
34
+ location?: string;
35
+ html?: string;
36
+ setCookies?: string[];
37
+ }> {
38
+ const builder = new CrelteBuilder();
39
+
40
+ // setup viteEnv
41
+ data.serverData.viteEnv.forEach((v, k) => {
42
+ builder.ssrCache.set(k, v);
43
+ });
44
+
45
+ const endpoint = data.serverData.endpoint;
46
+ builder.ssrCache.set('ENDPOINT_URL', endpoint);
47
+ builder.ssrCache.set('CRAFT_WEB_URL', data.serverData.craftWeb);
48
+ builder.setupGraphQl(endpoint, {
49
+ debug: data.graphQlDebug,
50
+ debugTiming: data.debugTiming,
51
+ });
52
+
53
+ const cookies = data.serverData.cookies ?? '';
54
+ builder.setupCookies(cookies);
55
+
56
+ const csites = await loadSites(builder);
57
+ builder.ssrCache.set('crelteSites', csites);
58
+ builder.setupRouter(csites);
59
+
60
+ const crelte = builder.build();
61
+
62
+ // setup plugins
63
+ setupPlugins(crelte, data.app.plugins ?? []);
64
+
65
+ // setup load Data
66
+
67
+ crelte.router._internal.onLoad = (route, site) => {
68
+ const cr = crelte.toRouted(route, site);
69
+ return loadFn(cr, data.app, data.entryQuery, data.globalQuery);
70
+ };
71
+
72
+ const { success, redirect, route, site, props } =
73
+ await crelte.router._internal.initServer(
74
+ data.serverData.url,
75
+ data.serverData.acceptLang,
76
+ );
77
+ if (!success) throw props;
78
+
79
+ if (redirect) {
80
+ return {
81
+ status: 302,
82
+ location: route.url.toString(),
83
+ };
84
+ }
85
+
86
+ const context = crelte._getContext();
87
+ const ssrComponents = new SsrComponents();
88
+ ssrComponents.addToContext(context);
89
+
90
+ pluginsBeforeRender(crelte.toRouted(route, site));
91
+ crelte.globals._updateSiteId(site.id);
92
+ // eslint-disable-next-line prefer-const
93
+ let { html, head } = data.app.default.render(props, { context });
94
+
95
+ head += ssrComponents.toHead(data.serverData.ssrManifest);
96
+ head += crelte.ssrCache._exportToHead();
97
+
98
+ let htmlTemplate = data.serverData.htmlTemplate;
99
+ htmlTemplate = htmlTemplate.replace('<!--page-lang-->', site.language);
100
+
101
+ const finalHtml = htmlTemplate
102
+ .replace('</head>', head + '\n\t</head>')
103
+ .replace('<!--ssr-body-->', html);
104
+
105
+ const entry = props.entry;
106
+
107
+ return {
108
+ status:
109
+ entry.sectionHandle === 'error' ? parseInt(entry.typeHandle) : 200,
110
+ html: finalHtml,
111
+ setCookies: (crelte.cookies as ServerCookies)._getSetCookiesHeaders(),
112
+ };
113
+ }
114
+
115
+ export type Error = {
116
+ status: number;
117
+ message: any;
118
+ };
119
+
120
+ export type MainErrorData = {
121
+ /// svelte app component
122
+ error: Error;
123
+ errorPage: any;
124
+
125
+ serverData: ServerData;
126
+ };
127
+
128
+ export async function mainError(
129
+ data: MainErrorData,
130
+ ): Promise<{ status: number; html?: string }> {
131
+ const ssrCache = new SsrCache();
132
+
133
+ const context = new Map();
134
+ const ssrComponents = new SsrComponents();
135
+ ssrComponents.addToContext(context);
136
+
137
+ ssrCache.set('ERROR', data.error);
138
+
139
+ // eslint-disable-next-line prefer-const
140
+ let { html, head } = data.errorPage.default.render(data.error, { context });
141
+
142
+ head += ssrComponents.toHead(data.serverData.ssrManifest);
143
+ head += ssrCache._exportToHead();
144
+
145
+ let htmlTemplate = data.serverData.htmlTemplate;
146
+ htmlTemplate = htmlTemplate.replace('<!--page-lang-->', 'de');
147
+
148
+ const finalHtml = htmlTemplate
149
+ .replace('<!--ssr-head-->', head)
150
+ .replace('<!--ssr-body-->', html);
151
+
152
+ return {
153
+ status: data.error.status,
154
+ html: finalHtml,
155
+ };
156
+ }
157
+
158
+ // requires, GraphQl, SsrCache
159
+ async function loadSites(builder: CrelteBuilder): Promise<SiteFromGraphQl[]> {
160
+ if (!builder.graphQl) throw new Error();
161
+
162
+ if ('CRAFT_SITES_CACHED' in globalThis) {
163
+ // @ts-ignore
164
+ return globalThis['CRAFT_SITES_CACHED'];
165
+ }
166
+
167
+ const resp = (await builder.graphQl.request(
168
+ 'query { crelteSites { id baseUrl language name handle primary } }',
169
+ {},
170
+ // don't cache since we cache ourself
171
+ { caching: false },
172
+ )) as { crelteSites: SiteFromGraphQl[] };
173
+
174
+ // @ts-ignore
175
+ globalThis['CRAFT_SITES_CACHED'] = resp.crelteSites;
176
+ return resp.crelteSites;
177
+ }
@@ -0,0 +1,221 @@
1
+ import CrelteRouted, { GraphQlQuery } from '../CrelteRouted.js';
2
+ import Crelte from '../Crelte.js';
3
+ import { LoadData, callLoadData } from '../loadData/index.js';
4
+ import { PluginCreator } from '../plugins/Plugins.js';
5
+ import { LoadOptions } from '../routing/PageLoader.js';
6
+
7
+ interface App<D, E, T> {
8
+ loadGlobalData?: LoadData<D>;
9
+
10
+ // todo: add a generic
11
+ loadEntryData?: LoadData<any>;
12
+
13
+ templates?: Record<string, LazyTemplateModule<E, T>>;
14
+ }
15
+
16
+ interface TemplateModule<E, T> {
17
+ // svelte component
18
+ default: any;
19
+
20
+ loadData?(cr: CrelteRouted, entry: E): Promise<T>;
21
+ }
22
+
23
+ type LazyTemplateModule<E, T> =
24
+ | (() => Promise<TemplateModule<E, T>>)
25
+ | TemplateModule<E, T>;
26
+
27
+ export function setupPlugins(crelte: Crelte, plugins: PluginCreator[]) {
28
+ for (const plugin of plugins) {
29
+ const p = plugin(crelte);
30
+ crelte.plugins.add(p);
31
+ }
32
+ }
33
+
34
+ export function pluginsBeforeRender(cr: CrelteRouted): void {
35
+ cr.events.trigger('beforeRender', cr);
36
+ }
37
+
38
+ /**
39
+ * Get the entry from the page
40
+ *
41
+ * entries should export sectionHandle and typeHandle
42
+ *
43
+ * products should alias productTypeHandle with typeHandle,
44
+ * sectionHandle will be automatically set to product
45
+ */
46
+ export function getEntry(page: any): any {
47
+ if (page?.entry) return { ...page.entry };
48
+ if (page?.product)
49
+ return {
50
+ sectionHandle: 'product',
51
+ ...page.product,
52
+ };
53
+
54
+ return {
55
+ sectionHandle: 'error',
56
+ typeHandle: '404',
57
+ };
58
+ }
59
+
60
+ export async function loadFn<D, E, T>(
61
+ cr: CrelteRouted,
62
+ app: App<D, E, T>,
63
+ entryQuery: GraphQlQuery,
64
+ globalQuery?: GraphQlQuery,
65
+ loadOpts?: LoadOptions,
66
+ ): Promise<any> {
67
+ let dataProm: Promise<D> | null = null;
68
+ // @ts-ignore
69
+ if (app.loadData) {
70
+ throw new Error(
71
+ 'loadData is ambigous, choose loadGlobalData or ' +
72
+ 'loadEntryData depending on if you need access to entry or not?',
73
+ );
74
+ }
75
+
76
+ if (app.loadGlobalData) {
77
+ dataProm = callLoadData(app.loadGlobalData, cr) as any;
78
+ }
79
+
80
+ let globalProm: Promise<any> | null = null;
81
+ if (globalQuery && !cr.globals._wasLoaded()) {
82
+ globalProm = (async () => {
83
+ const res = await cr.query(globalQuery);
84
+ // we need to do this sorcery here and can't wait until all
85
+ // globals functions are done, because some global function
86
+ // might want to use globals, and for that the function
87
+ // getOrWait exists on Globals
88
+ cr.globals._setData(cr.site.id, res);
89
+ return res;
90
+ })();
91
+ }
92
+
93
+ let pageProm = null;
94
+ if (cr.route.site) {
95
+ let uri = decodeURI(cr.route.uri);
96
+ if (uri.startsWith('/')) uri = uri.substring(1);
97
+ if (uri === '' || uri === '/') uri = '__home__';
98
+
99
+ pageProm = cr.query(entryQuery, {
100
+ uri,
101
+ siteId: cr.route.site.id,
102
+ });
103
+ }
104
+
105
+ const pluginsLoadGlobalData = cr.events.trigger('loadGlobalData', cr);
106
+
107
+ // loading progress is at 20%
108
+ loadOpts?.setProgress(0.2);
109
+
110
+ const [data, global, page] = await Promise.all([
111
+ dataProm,
112
+ globalProm,
113
+ pageProm,
114
+ ...pluginsLoadGlobalData,
115
+ ]);
116
+
117
+ if (global) {
118
+ cr.globals._setData(cr.site.id, global);
119
+ } else if (!cr.globals._wasLoaded()) {
120
+ // we need to set the global data to an empty object
121
+ // so any waiters get's triggered
122
+ cr.globals._setData(cr.site.id, {});
123
+ }
124
+
125
+ // allow cr to get the global data
126
+ cr._globalDataLoaded();
127
+
128
+ const entry = getEntry(page);
129
+
130
+ let template;
131
+ if (app.templates) {
132
+ template = await loadTemplate(app.templates, entry);
133
+ } else {
134
+ throw new Error('App must have templates or loadTemplate method');
135
+ }
136
+
137
+ // loading progress is at 60%
138
+ loadOpts?.setProgress(0.6);
139
+
140
+ const pluginsLoadData = cr.events.trigger('loadData', cr, entry);
141
+
142
+ let loadDataProm = null;
143
+ if (template.loadData) {
144
+ loadDataProm = callLoadData(template.loadData, cr, entry);
145
+ }
146
+
147
+ let entryDataProm: Promise<any> | null = null;
148
+ if (app.loadEntryData) {
149
+ entryDataProm = callLoadData(app.loadEntryData, cr, entry) as any;
150
+ }
151
+
152
+ const [templateData, entryData] = await Promise.all([
153
+ loadDataProm,
154
+ entryDataProm,
155
+ ...pluginsLoadData,
156
+ ]);
157
+
158
+ // loading progress is at 100%
159
+ loadOpts?.setProgress(1);
160
+
161
+ return {
162
+ ...data,
163
+ ...entryData,
164
+ entry,
165
+ template: template.default,
166
+ templateData: templateData! as any,
167
+ };
168
+ }
169
+
170
+ function parseFilename(path: string): [string, string] {
171
+ // get filename with extension
172
+ const slash = path.lastIndexOf('/');
173
+ const filename = path.substring(slash + 1);
174
+
175
+ const extPos = filename.lastIndexOf('.');
176
+
177
+ const name = filename.substring(0, extPos);
178
+ const ext = filename.substring(extPos + 1);
179
+
180
+ return [name, ext];
181
+ }
182
+
183
+ async function loadTemplate<E, T>(
184
+ rawModules: Record<string, LazyTemplateModule<E, T>>,
185
+ entry: E,
186
+ ): Promise<TemplateModule<E, T>> {
187
+ // parse modules
188
+ const modules = new Map(
189
+ Object.entries(rawModules)
190
+ .map(([path, mod]) => {
191
+ const [name, _ext] = parseFilename(path);
192
+ return [name, mod] as [string, LazyTemplateModule<E, T>];
193
+ })
194
+ .filter(([name, _mod]) => !!name),
195
+ );
196
+
197
+ const entr = entry as any;
198
+ const handle = `${entr.sectionHandle}-${entr.typeHandle}`;
199
+
200
+ if (
201
+ // @ts-ignore
202
+ import.meta.env.DEV &&
203
+ !modules.has(handle) &&
204
+ !modules.has(entr.sectionHandle)
205
+ ) {
206
+ console.error(
207
+ `Template not found: <${handle}>, expecting file: ${handle}.svelte or ${entr.sectionHandle}.svelte`,
208
+ );
209
+ }
210
+
211
+ const loadMod =
212
+ modules.get(handle) ??
213
+ modules.get(entr.sectionHandle) ??
214
+ modules.get('error-404');
215
+ if (!loadMod) throw new Error('could not find error-404 template');
216
+
217
+ if (typeof loadMod === 'function') {
218
+ return await loadMod();
219
+ }
220
+ return loadMod;
221
+ }
@@ -0,0 +1,150 @@
1
+ /*
2
+ // returns a store which get's updated as soon as the site changes
3
+ const emergency = getGlobal('emergency');
4
+
5
+ // returns the data based on the current site (no store)
6
+ cr.getGlobal('emergency')
7
+ */
8
+
9
+ import { Writable } from 'crelte-std/stores';
10
+
11
+ export type GlobalWaiters = [(g: Global<any> | null) => void];
12
+
13
+ export default class Globals {
14
+ // 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;
20
+
21
+ constructor() {
22
+ this.waiters = new Map();
23
+ this.entries = new Map();
24
+ this.loaded = false;
25
+ this.prevSiteId = null;
26
+ }
27
+
28
+ get(name: string): Global<any> | null {
29
+ return this.entries.get(name) ?? null;
30
+ }
31
+
32
+ // call this only in loadGlobalData when by default the global
33
+ // is not available
34
+ getAsync(name: string): Promise<Global<any> | null> | Global<any> | null {
35
+ if (this.loaded) return this.get(name);
36
+
37
+ let waiter = this.waiters.get(name);
38
+ if (!waiter) {
39
+ waiter = [] as any;
40
+ this.waiters.set(name, waiter!);
41
+ }
42
+
43
+ return new Promise(resolve => {
44
+ waiter!.push(resolve);
45
+ });
46
+ }
47
+
48
+ // hidden
49
+ _wasLoaded(): boolean {
50
+ return this.loaded;
51
+ }
52
+
53
+ // hidden
54
+ // data is the data from the global graphql
55
+ // so it contains some keys and data which should be parsed
56
+ // and created a store for each key
57
+ _setData(siteId: number, data: any) {
58
+ const wasLoaded = this.loaded;
59
+ this.loaded = true;
60
+
61
+ for (const [key, value] of Object.entries(data)) {
62
+ this.entries.set(key, new Global(key, value as any, siteId));
63
+ }
64
+
65
+ if (!wasLoaded) {
66
+ this.waiters.forEach((waiters, key) => {
67
+ waiters.forEach(waiter => waiter(this.get(key)));
68
+ });
69
+ this.waiters.clear();
70
+ }
71
+ }
72
+
73
+ _globalsBySite(siteId: number): Map<string, any> {
74
+ const map = new Map();
75
+
76
+ for (const [key, global] of this.entries) {
77
+ map.set(key, global.bySiteId(siteId));
78
+ }
79
+
80
+ return map;
81
+ }
82
+
83
+ _updateSiteId(siteId: number) {
84
+ // todo we should only trigger
85
+ if (this.prevSiteId === siteId) return;
86
+
87
+ this.entries.forEach(global => global._updateSiteId(siteId));
88
+ }
89
+ }
90
+
91
+ export interface GlobalData {
92
+ siteId?: number;
93
+ }
94
+
95
+ export class Global<T extends GlobalData> {
96
+ private inner: Writable<T>;
97
+ /// if languages is null this means we always have the same data
98
+ private languages: T[] | null;
99
+
100
+ constructor(name: string, data: T[] | T, siteId: number) {
101
+ this.languages = null;
102
+
103
+ let inner: T;
104
+ if (Array.isArray(data)) {
105
+ // make sure the data contains an object with the property
106
+ // siteId
107
+ this.languages = data;
108
+ inner = data.find(d => d.siteId === siteId)!;
109
+
110
+ if (!inner?.siteId) {
111
+ throw new Error(
112
+ `The global query ${name} does not contain the required siteId property`,
113
+ );
114
+ }
115
+ } else {
116
+ inner = data;
117
+ }
118
+
119
+ this.inner = new Writable(inner);
120
+ }
121
+
122
+ /**
123
+ * The function get's called once with the current value and then when the
124
+ * values changes
125
+ *
126
+ * @return a function which should be called to unsubscribe
127
+ */
128
+ subscribe(fn: (val: T) => void): () => void {
129
+ return this.inner.subscribe(fn);
130
+ }
131
+
132
+ get(): T {
133
+ return this.inner.get();
134
+ }
135
+
136
+ /// if you pass a siteId which comes from craft then you will never receive null
137
+ bySiteId(siteId: number): T | null {
138
+ if (this.languages)
139
+ return this.languages.find(d => d.siteId === siteId) ?? null;
140
+
141
+ return this.inner.get();
142
+ }
143
+
144
+ _updateSiteId(siteId: number) {
145
+ if (!this.languages) return;
146
+
147
+ const inner = this.languages.find(d => d.siteId === siteId);
148
+ this.inner.set(inner!);
149
+ }
150
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ *
3
+ */
4
+
5
+ import type CrelteRouted from '../CrelteRouted.js';
6
+ import { isGraphQlQuery, type GraphQlQuery } from '../graphql/GraphQl.js';
7
+ import type Globals from './Globals.js';
8
+ import type { Global } from './Globals.js';
9
+
10
+ export type { Globals, Global };
11
+
12
+ export type LoadData<T> =
13
+ | ((cr: CrelteRouted, ...args: any[]) => Promise<T>)
14
+ | GraphQlQuery
15
+ | T;
16
+
17
+ export async function callLoadData(
18
+ ld: LoadData<unknown>,
19
+ cr: CrelteRouted,
20
+ ...args: any[]
21
+ ): Promise<unknown> {
22
+ // either we have a function
23
+ if (typeof ld === 'function') {
24
+ return await ld(cr, ...args);
25
+ }
26
+
27
+ // or a graphql query
28
+ if (isGraphQlQuery(ld)) {
29
+ return await cr.query(ld);
30
+ }
31
+
32
+ // or an object
33
+ if (typeof ld === 'object' && ld !== null) {
34
+ const data = await Promise.all(
35
+ Object.values(ld).map(nld => callLoadData(nld, cr, ...args)),
36
+ );
37
+
38
+ return Object.fromEntries(
39
+ Object.keys(ld).map((key, i) => [key, data[i]]),
40
+ );
41
+ }
42
+
43
+ return ld;
44
+ }
45
+
46
+ /**
47
+ * Spread the data of two loadData functions.
48
+ *
49
+ * ## Example
50
+ * ```
51
+ * export const loadData = mergeLoadData(
52
+ * {
53
+ * filter: (cr) => cr.route.search.get('filter'),
54
+ * },
55
+ * (cr) => cr.query(myQuery, { siteId: cr.site.id })
56
+ * )
57
+ * ```
58
+ */
59
+ export function mergeLoadData(...lds: LoadData<object>[]): LoadData<object> {
60
+ return async (cr: CrelteRouted, ...args: any[]) => {
61
+ const datas = await Promise.all(
62
+ lds.map(ld => callLoadData(ld, cr, ...args) as Promise<object>),
63
+ );
64
+
65
+ return datas.reduce((acc, data) => ({ ...acc, ...data }), {});
66
+ };
67
+ }
@@ -0,0 +1,50 @@
1
+ import { CrelteRouted } from '../index.js';
2
+
3
+ export default class Events {
4
+ inner: Map<string, Set<any>>;
5
+
6
+ constructor() {
7
+ this.inner = new Map();
8
+ }
9
+
10
+ /*
11
+ * Listens for an event.
12
+ */
13
+ // override this function to add your own function signatures
14
+ on(
15
+ ev: 'loadGlobalData',
16
+ fn: (cr: CrelteRouted) => Promise<any>,
17
+ ): () => void;
18
+ on(
19
+ ev: 'loadData',
20
+ fn: (cr: CrelteRouted, entry: any, data: any) => Promise<any>,
21
+ ): () => void;
22
+ on(ev: 'beforeRender', fn: (cr: CrelteRouted) => void): () => void;
23
+ on(ev: string, fn: (...args: any[]) => any): () => void {
24
+ let set = this.inner.get(ev);
25
+ if (!set) {
26
+ set = new Set();
27
+ this.inner.set(ev, set);
28
+ }
29
+
30
+ set.add(fn);
31
+
32
+ return () => {
33
+ set!.delete(fn);
34
+ };
35
+ }
36
+
37
+ remove(ev: string, fn: any) {
38
+ const set = this.inner.get(ev);
39
+ if (!set) return;
40
+
41
+ set.delete(fn);
42
+ }
43
+
44
+ trigger(ev: string, ...args: any[]): any[] {
45
+ const set = this.inner.get(ev);
46
+ if (!set) return [];
47
+
48
+ return Array.from(set).map(fn => fn(...args));
49
+ }
50
+ }
@@ -0,0 +1,23 @@
1
+ import Crelte from '../Crelte.js';
2
+
3
+ export interface Plugin {
4
+ name: string;
5
+ }
6
+
7
+ export type PluginCreator = (crelte: Crelte) => Plugin;
8
+
9
+ export default class Plugins {
10
+ plugins: Map<string, Plugin>;
11
+
12
+ constructor() {
13
+ this.plugins = new Map();
14
+ }
15
+
16
+ add(plugin: Plugin) {
17
+ this.plugins.set(plugin.name, plugin);
18
+ }
19
+
20
+ get(name: string): Plugin | null {
21
+ return this.plugins.get(name) ?? null;
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ import Events from './Events.js';
2
+ import { Plugin, PluginCreator } from './Plugins.js';
3
+
4
+ export type { Plugin, PluginCreator };
5
+ export { Events };