crelte 0.5.12 → 0.5.14

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 (78) hide show
  1. package/dist/blocks/Blocks.d.ts +1 -1
  2. package/dist/blocks/Blocks.d.ts.map +1 -1
  3. package/dist/bodyClass/BodyClass.d.ts +18 -1
  4. package/dist/bodyClass/BodyClass.d.ts.map +1 -1
  5. package/dist/bodyClass/BodyClass.js +21 -2
  6. package/dist/bodyClass/ClientBodyClass.d.ts +7 -2
  7. package/dist/bodyClass/ClientBodyClass.d.ts.map +1 -1
  8. package/dist/bodyClass/ClientBodyClass.js +29 -15
  9. package/dist/bodyClass/ServerBodyClass.d.ts +7 -1
  10. package/dist/bodyClass/ServerBodyClass.d.ts.map +1 -1
  11. package/dist/bodyClass/ServerBodyClass.js +19 -5
  12. package/dist/bodyClass/utils.d.ts +39 -0
  13. package/dist/bodyClass/utils.d.ts.map +1 -0
  14. package/dist/bodyClass/utils.js +84 -0
  15. package/dist/crelte.d.ts.map +1 -1
  16. package/dist/crelte.js +2 -0
  17. package/dist/init/client.js +1 -1
  18. package/dist/init/server.d.ts.map +1 -1
  19. package/dist/init/server.js +1 -0
  20. package/dist/init/shared.d.ts.map +1 -1
  21. package/dist/init/shared.js +3 -0
  22. package/dist/loadData/entry.d.ts +11 -2
  23. package/dist/loadData/entry.d.ts.map +1 -1
  24. package/dist/loadData/entry.js +11 -2
  25. package/dist/queries/Queries.d.ts +3 -0
  26. package/dist/queries/Queries.d.ts.map +1 -1
  27. package/dist/queries/Queries.js +3 -0
  28. package/dist/queries/index.d.ts +22 -1
  29. package/dist/queries/index.d.ts.map +1 -1
  30. package/dist/queries/vars.d.ts +25 -2
  31. package/dist/queries/vars.d.ts.map +1 -1
  32. package/dist/queries/vars.js +42 -1
  33. package/dist/routing/route/BaseRoute.d.ts.map +1 -1
  34. package/dist/routing/route/BaseRoute.js +4 -17
  35. package/dist/server/queries/QueryGqlRoute.d.ts +3 -1
  36. package/dist/server/queries/QueryGqlRoute.d.ts.map +1 -1
  37. package/dist/server/queries/QueryGqlRoute.js +7 -5
  38. package/dist/server/queries/QueryHandleRoute.d.ts +3 -2
  39. package/dist/server/queries/QueryHandleRoute.d.ts.map +1 -1
  40. package/dist/server/queries/QueryHandleRoute.js +4 -2
  41. package/dist/server/queries/queries.d.ts.map +1 -1
  42. package/dist/server/queries/queries.js +12 -2
  43. package/dist/server/queries/routes.d.ts +4 -3
  44. package/dist/server/queries/routes.d.ts.map +1 -1
  45. package/dist/server/queries/routes.js +9 -3
  46. package/dist/std/index.d.ts +1 -0
  47. package/dist/std/index.d.ts.map +1 -1
  48. package/dist/std/index.js +1 -0
  49. package/dist/std/url/index.d.ts +40 -0
  50. package/dist/std/url/index.d.ts.map +1 -0
  51. package/dist/std/url/index.js +65 -0
  52. package/dist/std/url/utils.d.ts +19 -0
  53. package/dist/std/url/utils.d.ts.map +1 -0
  54. package/dist/std/url/utils.js +40 -0
  55. package/dist/vite/index.js +3 -2
  56. package/package.json +5 -1
  57. package/src/blocks/Blocks.ts +1 -1
  58. package/src/bodyClass/BodyClass.ts +25 -3
  59. package/src/bodyClass/ClientBodyClass.ts +37 -20
  60. package/src/bodyClass/ServerBodyClass.ts +28 -7
  61. package/src/bodyClass/utils.ts +118 -0
  62. package/src/crelte.ts +3 -0
  63. package/src/init/client.ts +1 -1
  64. package/src/init/server.ts +1 -0
  65. package/src/init/shared.ts +3 -0
  66. package/src/loadData/entry.ts +12 -2
  67. package/src/queries/Queries.ts +3 -0
  68. package/src/queries/index.ts +30 -1
  69. package/src/queries/vars.ts +58 -3
  70. package/src/routing/route/BaseRoute.ts +8 -22
  71. package/src/server/queries/QueryGqlRoute.ts +20 -4
  72. package/src/server/queries/QueryHandleRoute.ts +5 -2
  73. package/src/server/queries/queries.ts +29 -3
  74. package/src/server/queries/routes.ts +18 -3
  75. package/src/std/index.ts +1 -0
  76. package/src/std/url/index.ts +78 -0
  77. package/src/std/url/utils.ts +48 -0
  78. package/src/vite/index.ts +4 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crelte",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "author": "Crelte <support@crelte.com>",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -107,6 +107,10 @@
107
107
  "./std/rand": {
108
108
  "types": "./dist/std/rand/index.d.ts",
109
109
  "default": "./dist/std/rand/index.js"
110
+ },
111
+ "./std/url": {
112
+ "types": "./dist/std/url/index.d.ts",
113
+ "default": "./dist/std/url/index.js"
110
114
  }
111
115
  },
112
116
  "dependencies": {
@@ -125,7 +125,7 @@ export class BlockModules {
125
125
  * ```
126
126
  */
127
127
  export function blockModules(
128
- modules: Record<string, AsyncModule>,
128
+ modules: Record<string, (() => Promise<any>) | Module>,
129
129
  opts: BlockModulesOptions = {},
130
130
  ): BlockModules {
131
131
  return new BlockModules(modules, opts);
@@ -41,15 +41,33 @@ export default class BodyClass {
41
41
  * it.
42
42
  */
43
43
  toggle(cls: string, force?: boolean): void {
44
- this.inner.toggle(cls, force);
45
- this.store.set();
44
+ if (this.inner.toggle(cls, force)) this.store.set();
46
45
  }
47
46
 
47
+ /**
48
+ * Removes the given classes from the body
49
+ */
48
50
  remove(...classes: string[]): void {
49
51
  this.inner.remove(...classes);
50
52
  this.store.set();
51
53
  }
52
54
 
55
+ /**
56
+ * Sets the class for the given variant removing the old class for that
57
+ * variant, if cls is null it will remove the variant class
58
+ *
59
+ * If you just have like a dark or light mode and only for the dark mode a
60
+ * class, **prefer** `toggle('dark', isDarkMode)` over
61
+ * `setVariant('mode', isDarkMode ? 'dark' : null)`
62
+ *
63
+ * ## Note
64
+ * The variant name is only used for the internal state management
65
+ * and has no inpact on the actual class name
66
+ */
67
+ setVariant(variant: string, cls: string | null): void {
68
+ if (this.inner.setVariant(variant, cls)) this.store.set();
69
+ }
70
+
53
71
  /** @hidden */
54
72
  z_toRequest(): BodyClass {
55
73
  return new BodyClass(this.inner.toRequest(), this.store.stage());
@@ -65,8 +83,12 @@ export default class BodyClass {
65
83
  export interface PlatformBodyClass {
66
84
  contains(cls: string): boolean;
67
85
  add(...classes: string[]): void;
68
- toggle(cls: string, force?: boolean): void;
86
+ // returns true if the value was changed (this is different to the
87
+ // DOMTokenList.toggle which returns true if the class is now present)
88
+ toggle(cls: string, force?: boolean): boolean;
69
89
  remove(...classes: string[]): void;
90
+ // returns true if the value was changed
91
+ setVariant(variant: string, cls: string | null): boolean;
70
92
  toRequest(): PlatformBodyClass;
71
93
  render?: () => void;
72
94
  }
@@ -1,12 +1,22 @@
1
+ import SsrCache from '../ssr/SsrCache.js';
1
2
  import { PlatformBodyClass } from './BodyClass.js';
3
+ import { ClassSet, ssrCacheToVariants, Variants } from './utils.js';
2
4
 
3
5
  export default class ClientBodyClass implements PlatformBodyClass {
4
- private inner: Set<string> | null;
6
+ private variants: Variants;
7
+ // during the request store the classes here
8
+ private inner: ClassSet | null;
5
9
 
6
- constructor(inner?: Set<string>) {
10
+ constructor(variants: Variants, inner?: ClassSet) {
11
+ this.variants = variants;
7
12
  this.inner = inner ?? null;
8
13
  }
9
14
 
15
+ static fromSsrCache(ssrCache: SsrCache): ClientBodyClass {
16
+ const variants = ssrCacheToVariants(ssrCache);
17
+ return new ClientBodyClass(variants);
18
+ }
19
+
10
20
  contains(cls: string): boolean {
11
21
  if (this.inner) return this.inner.has(cls);
12
22
  return cl().contains(cls);
@@ -17,16 +27,20 @@ export default class ClientBodyClass implements PlatformBodyClass {
17
27
  else cl().add(...classes);
18
28
  }
19
29
 
20
- toggle(cls: string, force?: boolean): void {
21
- if (this.inner) {
22
- const add =
23
- typeof force === 'boolean' ? force : !this.inner.has(cls);
30
+ // returns true if the value was changed
31
+ toggle(cls: string, force?: boolean): boolean {
32
+ const exists = this.contains(cls);
33
+ const shouldExist = typeof force === 'boolean' ? force : !exists;
34
+ const changed = shouldExist !== exists;
24
35
 
25
- if (add) this.inner.add(cls);
36
+ if (this.inner) {
37
+ if (shouldExist) this.inner.add(cls);
26
38
  else this.inner.delete(cls);
27
39
  } else {
28
40
  cl().toggle(cls, force);
29
41
  }
42
+
43
+ return changed;
30
44
  }
31
45
 
32
46
  remove(...classes: string[]): void {
@@ -34,26 +48,29 @@ export default class ClientBodyClass implements PlatformBodyClass {
34
48
  else cl().remove(...classes);
35
49
  }
36
50
 
51
+ setVariant(variant: string, cls: string | null): boolean {
52
+ if (this.inner) return this.inner.setVariant(variant, cls);
53
+
54
+ const { remove, add } = this.variants.set(variant, cls);
55
+
56
+ if (remove) cl().remove(remove);
57
+ if (add) cl().add(add);
58
+
59
+ return !!remove || !!add;
60
+ }
61
+
37
62
  toRequest(): ClientBodyClass {
38
- const inner = new Set(cl());
39
- return new ClientBodyClass(inner);
63
+ const inner = new ClassSet(cl(), this.variants.entries(), true);
64
+ return new ClientBodyClass(this.variants, inner);
40
65
  }
41
66
 
42
67
  render(): void {
43
68
  if (!this.inner) throw new Error('call toRequest first');
44
- const current = new Set(cl());
45
-
46
- for (const cls of this.inner) {
47
- const existed = current.delete(cls);
48
- if (!existed) cl().add(cls);
49
- }
50
-
51
- // now lets remove all classes that still exist in current
52
- for (const cls of current) {
53
- cl().remove(cls);
54
- }
55
69
 
70
+ const inner = this.inner;
56
71
  this.inner = null;
72
+
73
+ inner.applyHistory(this);
57
74
  }
58
75
  }
59
76
 
@@ -1,10 +1,12 @@
1
+ import SsrCache from '../ssr/SsrCache.js';
1
2
  import { PlatformBodyClass } from './BodyClass.js';
3
+ import { ClassSet, variantsToSsrCache } from './utils.js';
2
4
 
3
5
  export default class ServerBodyClass implements PlatformBodyClass {
4
- private inner: Set<string>;
6
+ private inner: ClassSet;
5
7
 
6
8
  constructor() {
7
- this.inner = new Set();
9
+ this.inner = new ClassSet();
8
10
  }
9
11
 
10
12
  contains(cls: string): boolean {
@@ -16,7 +18,7 @@ export default class ServerBodyClass implements PlatformBodyClass {
16
18
  classes.forEach(cls => this.inner.add(cls));
17
19
  }
18
20
 
19
- toggle(cls: string, force?: boolean): void {
21
+ toggle(cls: string, force?: boolean): boolean {
20
22
  validate([cls]);
21
23
 
22
24
  if (import.meta.env.DEV && typeof force !== 'boolean') {
@@ -27,10 +29,14 @@ export default class ServerBodyClass implements PlatformBodyClass {
27
29
  );
28
30
  }
29
31
 
30
- const add = typeof force === 'boolean' ? force : !this.inner.has(cls);
32
+ const exists = this.inner.has(cls);
33
+ const shouldExist = typeof force === 'boolean' ? force : !exists;
34
+ const changed = shouldExist !== exists;
31
35
 
32
- if (add) this.inner.add(cls);
36
+ if (shouldExist) this.inner.add(cls);
33
37
  else this.inner.delete(cls);
38
+
39
+ return changed;
34
40
  }
35
41
 
36
42
  remove(...classes: string[]): void {
@@ -38,19 +44,34 @@ export default class ServerBodyClass implements PlatformBodyClass {
38
44
  classes.forEach(cls => this.inner.delete(cls));
39
45
  }
40
46
 
47
+ /**
48
+ * @returns Returns true if the value was changed
49
+ */
50
+ setVariant(variant: string, cls: string | null): boolean {
51
+ return this.inner.setVariant(variant, cls);
52
+ }
53
+
41
54
  toRequest(): ServerBodyClass {
55
+ // no second request should ever start on the server
42
56
  return this;
43
57
  }
44
58
 
59
+ z_populateSsrCache(ssrCache: SsrCache): void {
60
+ variantsToSsrCache(this.inner.z_variants, ssrCache);
61
+ }
62
+
45
63
  z_processHtmlTemplate(html: string): string {
46
64
  const SEARCH_STR = '<!--body-class-->';
47
- if (this.inner.size && !html.includes(SEARCH_STR)) {
65
+ if (this.inner.length && !html.includes(SEARCH_STR)) {
48
66
  throw new Error(
49
67
  'index.html needs to contain `class="<!--body-class-->"`',
50
68
  );
51
69
  }
52
70
 
53
- return html.replace(SEARCH_STR, Array.from(this.inner).join(' '));
71
+ return html.replace(
72
+ SEARCH_STR,
73
+ Array.from(this.inner.classes()).join(' '),
74
+ );
54
75
  }
55
76
  }
56
77
 
@@ -0,0 +1,118 @@
1
+ import SsrCache from '../ssr/SsrCache.js';
2
+
3
+ export type HistoryClassSet = {
4
+ add(cls: string): void;
5
+ remove(cls: string): void;
6
+ setVariant(variant: string, cls: string | null): void;
7
+ };
8
+
9
+ export class ClassSet {
10
+ private _classes: Set<string>;
11
+ /** @hidden */
12
+ z_variants: Variants;
13
+ private history: ((cl: HistoryClassSet) => void)[] | null;
14
+
15
+ constructor(
16
+ classes?: Iterable<string>,
17
+ variants?: Iterable<[string, string]>,
18
+ history: boolean = false,
19
+ ) {
20
+ this._classes = new Set(classes);
21
+ this.z_variants = new Variants();
22
+ this.history = history ? [] : null;
23
+ }
24
+
25
+ classes(): Iterable<string> {
26
+ return this._classes;
27
+ }
28
+
29
+ get length(): number {
30
+ return this._classes.size;
31
+ }
32
+
33
+ has(cls: string): boolean {
34
+ return this._classes.has(cls);
35
+ }
36
+
37
+ add(cls: string): void {
38
+ this._classes.add(cls);
39
+ if (this.history) this.history.push(c => c.add(cls));
40
+ }
41
+
42
+ delete(cls: string): void {
43
+ this._classes.delete(cls);
44
+ if (this.history) this.history.push(c => c.remove(cls));
45
+ }
46
+
47
+ /**
48
+ * @returns Returns true if the value was changed
49
+ */
50
+ setVariant(variant: string, cls: string | null): boolean {
51
+ const { remove, add } = this.z_variants.set(variant, cls);
52
+
53
+ if (remove) this.delete(remove);
54
+ if (add) this.add(add);
55
+
56
+ const changed = !!remove || !!add;
57
+
58
+ if (this.history) this.history.push(c => c.setVariant(variant, cls));
59
+
60
+ return changed;
61
+ }
62
+
63
+ /**
64
+ * Fails if no history is active.
65
+ */
66
+ applyHistory(cl: HistoryClassSet): void {
67
+ this.history!.forEach(fn => fn(cl));
68
+ }
69
+ }
70
+
71
+ export type VariantSetReturn = {
72
+ remove?: string;
73
+ add?: string;
74
+ };
75
+
76
+ export class Variants {
77
+ // list of active variant classes
78
+ private inner: Map<string, string>;
79
+
80
+ constructor(entries?: Iterable<[string, string]>) {
81
+ this.inner = new Map(entries);
82
+ }
83
+
84
+ set(variant: string, cls: string | null): VariantSetReturn {
85
+ const current = this.inner.get(variant) ?? null;
86
+
87
+ if (current === cls) return {};
88
+
89
+ const obj: VariantSetReturn = {};
90
+
91
+ if (current) obj.remove = current;
92
+ if (cls) {
93
+ obj.add = cls;
94
+ this.inner.set(variant, cls);
95
+ } else {
96
+ this.inner.delete(variant);
97
+ }
98
+
99
+ return obj;
100
+ }
101
+
102
+ entries(): Iterable<[string, string]> {
103
+ return this.inner.entries();
104
+ }
105
+ }
106
+
107
+ // separate function for tree shaking
108
+ export function ssrCacheToVariants(ssrCache: SsrCache): Variants {
109
+ return new Variants(ssrCache.get('BODY_CLASS_VAR')!);
110
+ }
111
+
112
+ // separate function for tree shaking
113
+ export function variantsToSsrCache(
114
+ variants: Variants,
115
+ ssrCache: SsrCache,
116
+ ): void {
117
+ ssrCache.set('BODY_CLASS_VAR', Array.from(variants.entries()));
118
+ }
package/src/crelte.ts CHANGED
@@ -292,6 +292,9 @@ export function newCrelte({
292
292
  cookies,
293
293
  bodyClass,
294
294
 
295
+ // when adding a new helper function make sure to add it to
296
+ // onNewCrelteRequest if needed
297
+
295
298
  getPlugin: name => plugins.get(name),
296
299
  getEnv: key => ssrCache.get(key as any) as any,
297
300
  frontendUrl: path => urlWithPath(ssrCache.get('FRONTEND_URL')!, path),
@@ -88,7 +88,7 @@ export async function main(data: MainData) {
88
88
  router: new Router(router),
89
89
  queries,
90
90
  cookies: new Cookies(new ClientCookies()),
91
- bodyClass: new BodyClass(new ClientBodyClass()),
91
+ bodyClass: new BodyClass(ClientBodyClass.fromSsrCache(ssrCache)),
92
92
  });
93
93
 
94
94
  const app = new InternalApp(data.app);
@@ -152,6 +152,7 @@ export async function main(data: MainData): Promise<RenderResponse> {
152
152
  context,
153
153
  });
154
154
 
155
+ bodyClass.z_populateSsrCache(ssrCache);
155
156
  head += ssrComponents.toHead(data.serverData.ssrManifest);
156
157
  head += crelte.ssrCache.z_exportToHead();
157
158
 
@@ -74,6 +74,9 @@ export function onNewCrelteRequest(
74
74
  cookies: crelte.cookies.z_toRequest(),
75
75
  bodyClass: crelte.bodyClass.z_toRequest(),
76
76
  };
77
+ // make sure helper funcitons link to the correct instances
78
+ nCrelte.getPlugin = name => nCrelte.plugins.get(name);
79
+ nCrelte.getGlobalStore = name => nCrelte.globals.getStore(name);
77
80
  return crelteToRequest(nCrelte, req);
78
81
  }
79
82
 
@@ -1,5 +1,5 @@
1
1
  import { CrelteRequest } from '../index.js';
2
- import { Query } from '../queries/Queries.js';
2
+ import { Query, QueryOptions } from '../queries/Queries.js';
3
3
 
4
4
  export type Entry = {
5
5
  sectionHandle: string;
@@ -31,12 +31,22 @@ export function entryQueryVars(cr: CrelteRequest): EntryQueryVars {
31
31
  };
32
32
  }
33
33
 
34
+ /**
35
+ * ## Example
36
+ * `App.svelte`
37
+ * ```ts
38
+ * import entryQuery from '@/queries/entry.graphql';
39
+ *
40
+ * export const loadEntry => cr => queryEntry(cr, entryQuery, entryQueryVars(cr));
41
+ * ```
42
+ */
34
43
  export async function queryEntry(
35
44
  cr: CrelteRequest,
36
45
  entryQuery: Query,
37
46
  vars: EntryQueryVars,
47
+ opts?: QueryOptions,
38
48
  ): Promise<Entry> {
39
- const page = await cr.query(entryQuery, vars);
49
+ const page = await cr.query(entryQuery, vars, opts);
40
50
  return extractEntry(page) ?? ENTRY_ERROR_404;
41
51
  }
42
52
 
@@ -42,6 +42,9 @@ export type NamedQuery = { queryName: string };
42
42
  * Create a NamedQuery for the given server query name.
43
43
  *
44
44
  * Prefer importing a graphql file instead.
45
+ *
46
+ * #### Note
47
+ * This needs to match the name of the js/ts file.
45
48
  */
46
49
  export function namedQuery(name: string): NamedQuery {
47
50
  return { queryName: name };
@@ -8,6 +8,7 @@ import Queries, {
8
8
  QueryOptions,
9
9
  } from '../queries/Queries.js';
10
10
  import type CrelteServerRequest from '../server/CrelteServer.js';
11
+ import ServerRouter from '../server/ServerRouter.js';
11
12
  import { gql } from './gql.js';
12
13
  import QueryError, { QueryErrorResponse } from './QueryError.js';
13
14
  import { QueryVar, ValidIf, vars, varsIdsEqual } from './vars.js';
@@ -38,6 +39,30 @@ export type InferVariableTypes<T> = {
38
39
  [K in keyof T]: InferQueryVarType<T[K]>;
39
40
  };
40
41
 
42
+ /**
43
+ * Defines wether the query variables are valid.
44
+ *
45
+ * Either throw or return a boolean.
46
+ *
47
+ * #### Example
48
+ * ```ts
49
+ * import { vars, ValidVars } from 'crelte/queries';
50
+ *
51
+ * export const variables = {
52
+ * siteId: vars.siteId(),
53
+ * category: vars.id()
54
+ * };
55
+ *
56
+ * export const validVars: ValidVars<typeof variables> = (vars, sr) => {
57
+ * if (!vars.category === 5) throw new Error('category needs to be 5');
58
+ * };
59
+ * ```
60
+ */
61
+ export type ValidVars<T extends Record<string, QueryVar<any>>> = (
62
+ vars: InferVariableTypes<T>,
63
+ sr: ServerRouter,
64
+ ) => void | boolean;
65
+
41
66
  /**
42
67
  * Defines when a query can safely be cached.
43
68
  *
@@ -81,7 +106,11 @@ export type TransformFn<
81
106
  export type Transform<
82
107
  T extends Record<string, QueryVar<any>> = Record<string, QueryVar<any>>,
83
108
  F extends TransformFn<T> = TransformFn<T>,
84
- > = (response: any, vars: InferVariableTypes<T>) => Awaited<ReturnType<F>>;
109
+ > = (
110
+ response: any,
111
+ vars: InferVariableTypes<T>,
112
+ csr: CrelteServerRequest,
113
+ ) => Awaited<ReturnType<F>>;
85
114
 
86
115
  /** use {@link Handle} */
87
116
  export type HandleFn<
@@ -1,10 +1,32 @@
1
1
  import ServerRouter from '../server/ServerRouter.js';
2
2
 
3
3
  export const vars = {
4
+ /**
5
+ * Any is not validated except for nullability
6
+ */
4
7
  any: (): QueryVar<any> => new QueryVar(),
8
+
9
+ /**
10
+ * Number is a number, but will also parse strings to numbers
11
+ */
5
12
  number: (): QueryVar<number> => new QueryVar().number(),
13
+
14
+ /**
15
+ * String is a string, but will not parse numbers to strings
16
+ */
6
17
  string: (): QueryVar<string> => new QueryVar().string(),
7
18
 
19
+ /**
20
+ * Strings is an array of strings, but will also convert a single string to
21
+ * an array with one element
22
+ *
23
+ * #### Note
24
+ * The returned array will **never be empty**, but might be null if allowed.
25
+ * It is not deduped or sorted.
26
+ * If you have ids you should use `vars.ids()` instead.
27
+ */
28
+ strings: (): QueryVar<string[]> => new QueryVar().strings(),
29
+
8
30
  /**
9
31
  * Id is almost the same as number but will also parse
10
32
  * strings, but only allow non negative integers
@@ -37,6 +59,9 @@ export const vars = {
37
59
  */
38
60
  ids: (): QueryVar<number[]> => new QueryVar().ids(),
39
61
 
62
+ /**
63
+ * Checks for a valid site id which exists
64
+ */
40
65
  siteId: (): QueryVar<number> =>
41
66
  new QueryVar()
42
67
  .number()
@@ -47,11 +72,11 @@ export const vars = {
47
72
 
48
73
  /// either throw with an error which will get returned in a 400 response or
49
74
  // return false if the value is not valid
50
- export type ValidIf<T> = (v: T, cs: ServerRouter) => boolean;
75
+ export type ValidIf<T> = (v: T, sr: ServerRouter) => boolean;
51
76
 
52
77
  export class QueryVar<T = any> {
53
78
  private name: string | null;
54
- private type: 'any' | 'string' | 'number' | 'id' | 'ids';
79
+ private type: 'any' | 'string' | 'strings' | 'number' | 'id' | 'ids';
55
80
  private flagNullable: boolean;
56
81
  private defaultValue: T | undefined;
57
82
  private validIfFn: ValidIf<T>;
@@ -69,6 +94,11 @@ export class QueryVar<T = any> {
69
94
  return this as unknown as QueryVar<string>;
70
95
  }
71
96
 
97
+ strings(): QueryVar<string[]> {
98
+ this.type = 'strings';
99
+ return this as unknown as QueryVar<string[]>;
100
+ }
101
+
72
102
  number(): QueryVar<number> {
73
103
  this.type = 'number';
74
104
  return this as unknown as QueryVar<number>;
@@ -98,7 +128,7 @@ export class QueryVar<T = any> {
98
128
  * Set a validation function for this variable
99
129
  *
100
130
  * If the value is allowed to be null and it is null
101
- * valid will not be called.
131
+ * the passed function will not be called.
102
132
  */
103
133
  validIf(fn: ValidIf<T>): QueryVar<T> {
104
134
  this.validIfFn = fn;
@@ -127,6 +157,31 @@ export class QueryVar<T = any> {
127
157
  throw new Error(`variable ${this.name} is not a string`);
128
158
  break;
129
159
 
160
+ case 'strings':
161
+ if (typeof v === 'string') v = [v];
162
+
163
+ if (!Array.isArray(v))
164
+ throw new Error(
165
+ `variable ${this.name} is not a string or a list of strings`,
166
+ );
167
+
168
+ if (v.length <= 0) {
169
+ if (this.defaultValue !== undefined)
170
+ return this.defaultValue;
171
+
172
+ if (this.flagNullable) return null;
173
+
174
+ throw new Error(
175
+ `variable ${this.name} is not allowed to be empty`,
176
+ );
177
+ }
178
+
179
+ if (!v.every(s => typeof s === 'string'))
180
+ throw new Error(
181
+ `variable ${this.name} is not a list of strings`,
182
+ );
183
+ break;
184
+
130
185
  case 'number':
131
186
  if (typeof v !== 'number')
132
187
  throw new Error(`variable ${this.name} is not a number`);
@@ -1,3 +1,8 @@
1
+ import {
2
+ deleteSearchParam,
3
+ pathnameEq,
4
+ searchEq,
5
+ } from '../../std/url/utils.js';
1
6
  import { objClone } from '../../utils.js';
2
7
  import Site from '../Site.js';
3
8
  import { trimSlashEnd } from '../utils.js';
@@ -238,12 +243,7 @@ export default class BaseRoute {
238
243
  * ```
239
244
  */
240
245
  setSearchParam(key: string, value?: string | number | null) {
241
- const deleteValue =
242
- typeof value === 'undefined' ||
243
- value === null ||
244
- (typeof value === 'string' && value === '');
245
-
246
- if (!deleteValue) {
246
+ if (!deleteSearchParam(value)) {
247
247
  this.search.set(key, value as string);
248
248
  } else {
249
249
  this.search.delete(key);
@@ -344,8 +344,8 @@ export default class BaseRoute {
344
344
  eqUrl(route: BaseRoute | null) {
345
345
  return (
346
346
  route &&
347
- this.url.pathname === route.url.pathname &&
348
- this.url.origin === route.url.origin
347
+ this.url.origin === route.url.origin &&
348
+ pathnameEq(this.url.pathname, route.url.pathname)
349
349
  );
350
350
  }
351
351
 
@@ -353,20 +353,6 @@ export default class BaseRoute {
353
353
  * Checks if the search params are equal to another route
354
354
  */
355
355
  eqSearch(route: BaseRoute | null) {
356
- const searchEq = (a: URLSearchParams, b: URLSearchParams) => {
357
- if (a.size !== b.size) return false;
358
-
359
- a.sort();
360
- b.sort();
361
-
362
- const aEntries = Array.from(a.entries());
363
- const bEntries = Array.from(b.entries());
364
-
365
- return aEntries
366
- .map((a, i) => [a, bEntries[i]])
367
- .every(([[ak, av], [bk, bv]]) => ak === bk && av === bv);
368
- };
369
-
370
356
  return route && searchEq(this.search, route.search);
371
357
  }
372
358