crelte 0.5.13 → 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 (44) hide show
  1. package/dist/queries/Queries.d.ts +3 -0
  2. package/dist/queries/Queries.d.ts.map +1 -1
  3. package/dist/queries/Queries.js +3 -0
  4. package/dist/queries/index.d.ts +21 -0
  5. package/dist/queries/index.d.ts.map +1 -1
  6. package/dist/queries/vars.d.ts +25 -2
  7. package/dist/queries/vars.d.ts.map +1 -1
  8. package/dist/queries/vars.js +42 -1
  9. package/dist/routing/route/BaseRoute.d.ts.map +1 -1
  10. package/dist/routing/route/BaseRoute.js +4 -17
  11. package/dist/server/queries/QueryGqlRoute.d.ts +3 -1
  12. package/dist/server/queries/QueryGqlRoute.d.ts.map +1 -1
  13. package/dist/server/queries/QueryGqlRoute.js +4 -2
  14. package/dist/server/queries/QueryHandleRoute.d.ts +3 -2
  15. package/dist/server/queries/QueryHandleRoute.d.ts.map +1 -1
  16. package/dist/server/queries/QueryHandleRoute.js +4 -2
  17. package/dist/server/queries/queries.d.ts.map +1 -1
  18. package/dist/server/queries/queries.js +12 -2
  19. package/dist/server/queries/routes.d.ts +3 -2
  20. package/dist/server/queries/routes.d.ts.map +1 -1
  21. package/dist/server/queries/routes.js +9 -3
  22. package/dist/std/index.d.ts +1 -0
  23. package/dist/std/index.d.ts.map +1 -1
  24. package/dist/std/index.js +1 -0
  25. package/dist/std/url/index.d.ts +40 -0
  26. package/dist/std/url/index.d.ts.map +1 -0
  27. package/dist/std/url/index.js +65 -0
  28. package/dist/std/url/utils.d.ts +19 -0
  29. package/dist/std/url/utils.d.ts.map +1 -0
  30. package/dist/std/url/utils.js +40 -0
  31. package/dist/vite/index.js +3 -2
  32. package/package.json +5 -1
  33. package/src/queries/Queries.ts +3 -0
  34. package/src/queries/index.ts +25 -0
  35. package/src/queries/vars.ts +58 -3
  36. package/src/routing/route/BaseRoute.ts +8 -22
  37. package/src/server/queries/QueryGqlRoute.ts +17 -2
  38. package/src/server/queries/QueryHandleRoute.ts +5 -2
  39. package/src/server/queries/queries.ts +29 -3
  40. package/src/server/queries/routes.ts +17 -3
  41. package/src/std/index.ts +1 -0
  42. package/src/std/url/index.ts +78 -0
  43. package/src/std/url/utils.ts +48 -0
  44. package/src/vite/index.ts +4 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/std/url/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,kCAM1E;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,GAAG,GAAG,MAAM,GAAG,GAAG,CAMxD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,OAAO,CAWxE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAGxD"}
@@ -0,0 +1,40 @@
1
+ import { BaseRoute } from '../../routing/index.js';
2
+ /**
3
+ * Checks if a search param should be removed.
4
+ * This is the case if the value is `null`, `undefined`, or an empty string.
5
+ */
6
+ export function deleteSearchParam(value) {
7
+ return (typeof value === 'undefined' ||
8
+ value === null ||
9
+ (typeof value === 'string' && value === ''));
10
+ }
11
+ /**
12
+ * Converts a `BaseRoute`, `URL`, or string to a `URL` object.
13
+ */
14
+ export function toUrl(url) {
15
+ if (typeof url === 'string')
16
+ return new URL(url);
17
+ if (url instanceof BaseRoute)
18
+ return new URL(url.url);
19
+ return url;
20
+ }
21
+ /**
22
+ * Compares two `URLSearchParams` objects for equality.
23
+ */
24
+ export function searchEq(a, b) {
25
+ if (a.size !== b.size)
26
+ return false;
27
+ // Clone to avoid mutating the original objects
28
+ const cloneA = new URLSearchParams(a);
29
+ const cloneB = new URLSearchParams(b);
30
+ cloneA.sort();
31
+ cloneB.sort();
32
+ return cloneA.toString() === cloneB.toString();
33
+ }
34
+ /**
35
+ * Compares two pathnames for equality, ignoring trailing slashes.
36
+ */
37
+ export function pathnameEq(a, b) {
38
+ // check for trailing slashes
39
+ return a === b || a === b + '/' || a + '/' === b;
40
+ }
@@ -126,7 +126,7 @@ export default function crelte(opts) {
126
126
  publicDir: isSsrBuild ? false : 'public',
127
127
  base: '/',
128
128
  server: {
129
- port: 8080,
129
+ port: config.server?.port ?? 8080,
130
130
  },
131
131
  optimizeDeps: {
132
132
  exclude: ['crelte'],
@@ -256,7 +256,8 @@ export default function crelte(opts) {
256
256
  async function serveVite(env, vite) {
257
257
  let sites = null;
258
258
  const handle = async (nReq, res) => {
259
- const protocol = vite.config.server.https ? 'https' : 'http';
259
+ const protocol = nReq.headers['x-forwarded-proto'] ??
260
+ (vite.config.server.https ? 'https' : 'http');
260
261
  const baseUrl = protocol + '://' + nReq.headers['host'];
261
262
  const req = requestToWebRequest(baseUrl, nReq);
262
263
  const serverMod = await vite.ssrLoadModule('./src/server.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crelte",
3
- "version": "0.5.13",
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": {
@@ -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
  *
@@ -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
 
@@ -3,10 +3,17 @@ import QueriesCaching from './QueriesCaching.js';
3
3
  import { QueryVar, vars } from '../../queries/vars.js';
4
4
  import { extractEntry } from '../../loadData/index.js';
5
5
  import { calcKey } from '../../ssr/index.js';
6
- import { CacheIfFn, newError, TransformFn, validateVars } from './routes.js';
6
+ import {
7
+ CacheIfFn,
8
+ newError,
9
+ TransformFn,
10
+ validateVars,
11
+ ValidIfFn,
12
+ } from './routes.js';
7
13
 
8
14
  export type QueryGqlArgs = {
9
15
  vars: Record<string, QueryVar> | null;
16
+ validIfFn: ValidIfFn | null;
10
17
  cacheIfFn: CacheIfFn | null;
11
18
  preventCaching: boolean;
12
19
  transformFn: TransformFn | null;
@@ -17,6 +24,7 @@ export default class QueryGqlRoute {
17
24
  name: string;
18
25
  query: string;
19
26
  vars: Record<string, QueryVar> | null;
27
+ validIfFn: ValidIfFn | null;
20
28
  cacheIfFn: CacheIfFn | null;
21
29
  transformFn: TransformFn | null;
22
30
 
@@ -32,6 +40,7 @@ export default class QueryGqlRoute {
32
40
  this.name = name;
33
41
  this.query = query;
34
42
  this.vars = args.vars;
43
+ this.validIfFn = args.validIfFn;
35
44
  this.cacheIfFn = args.cacheIfFn;
36
45
  this.transformFn = args.transformFn;
37
46
 
@@ -112,7 +121,13 @@ export default class QueryGqlRoute {
112
121
  let vars: Record<string, any>;
113
122
  try {
114
123
  const reqVars = await csr.req.json();
115
- vars = validateVars(this.vars, reqVars, caching.router);
124
+ vars = validateVars(
125
+ this.vars,
126
+ reqVars,
127
+ this.validIfFn,
128
+ caching.router,
129
+ );
130
+
116
131
  if ('qName' in vars || 'xCraftSite' in vars)
117
132
  throw new Error(
118
133
  'qName and xCraftSite are reserved variable names',
@@ -1,20 +1,23 @@
1
1
  import { QueryVar } from '../../queries/vars.js';
2
2
  import CrelteServerRequest from '../CrelteServer.js';
3
3
  import ServerRouter from '../ServerRouter.js';
4
- import { HandleFn, newError, validateVars } from './routes.js';
4
+ import { HandleFn, newError, validateVars, ValidIfFn } from './routes.js';
5
5
 
6
6
  // only internal
7
7
  export default class QueryHandleRoute {
8
8
  name: string;
9
+ validIfFn: ValidIfFn | null;
9
10
  handleFn: HandleFn;
10
11
  vars: Record<string, QueryVar> | null;
11
12
 
12
13
  constructor(
13
14
  name: string,
15
+ validIfFn: ValidIfFn | null,
14
16
  handleFn: HandleFn,
15
17
  vars: Record<string, QueryVar> | null,
16
18
  ) {
17
19
  this.name = name;
20
+ this.validIfFn = validIfFn;
18
21
  this.handleFn = handleFn;
19
22
  this.vars = vars;
20
23
  }
@@ -26,7 +29,7 @@ export default class QueryHandleRoute {
26
29
  let vars: Record<string, any>;
27
30
  try {
28
31
  const reqVars = await csr.req.json();
29
- vars = validateVars(this.vars, reqVars, cs);
32
+ vars = validateVars(this.vars, reqVars, this.validIfFn, cs);
30
33
  } catch (e) {
31
34
  return newError(e, 400);
32
35
  }
@@ -1,6 +1,6 @@
1
1
  import ServerRouter from '../ServerRouter.js';
2
2
  import QueriesCaching from './QueriesCaching.js';
3
- import { CacheIfFn, HandleFn, TransformFn } from './routes.js';
3
+ import { CacheIfFn, HandleFn, TransformFn, ValidIfFn } from './routes.js';
4
4
  import { isQueryVar, QueryVar } from '../../queries/vars.js';
5
5
  import { Platform } from '../platform.js';
6
6
  import QueryHandleRoute from './QueryHandleRoute.js';
@@ -13,6 +13,11 @@ type ModQuery = {
13
13
 
14
14
  type ModTs = {
15
15
  variables?: any;
16
+ // only to hint that its validVars
17
+ validVariables?: any;
18
+ // only to hint that its validVars
19
+ validIf?: any;
20
+ validVars?: any;
16
21
  caching?: any;
17
22
  transform?: any;
18
23
  handle?: any;
@@ -24,6 +29,7 @@ type RouteBuilder = {
24
29
  query: string | null;
25
30
  jsFile: string | null;
26
31
  vars: Record<string, QueryVar> | null;
32
+ validIfFn: ValidIfFn | null;
27
33
  /** corresponds to the caching export */
28
34
  cacheIfFn: CacheIfFn | null;
29
35
  preventCaching: boolean;
@@ -59,6 +65,7 @@ export async function initQueryRoutes(
59
65
  query: null,
60
66
  jsFile: null,
61
67
  vars: null,
68
+ validIfFn: null,
62
69
  cacheIfFn: null,
63
70
  preventCaching: false,
64
71
  transformFn: null,
@@ -87,6 +94,17 @@ export async function initQueryRoutes(
87
94
  routeBuilder.vars = parseVars(mts.variables);
88
95
  }
89
96
 
97
+ if (mts.validVariables || mts.validIf) {
98
+ throw new Error(`use the function validVars in ${filename}`);
99
+ }
100
+
101
+ if (mts.validVars) {
102
+ if (typeof mts.validVars !== 'function') {
103
+ throw new Error('validVars should be a function');
104
+ }
105
+ routeBuilder.validIfFn = mts.validVars;
106
+ }
107
+
90
108
  if (mts.caching) {
91
109
  routeBuilder.cacheIfFn = parseCaching(mts.caching);
92
110
  } else if (typeof mts.caching === 'boolean') {
@@ -108,7 +126,10 @@ export async function initQueryRoutes(
108
126
 
109
127
  for (const [name, rb] of routeBuilders.entries()) {
110
128
  if (rb.query) {
111
- if (rb.handleFn) throw new Error('handle function not supported');
129
+ if (rb.handleFn)
130
+ throw new Error(
131
+ 'cannot have a handle function if a query is present',
132
+ );
112
133
 
113
134
  const route = new QueryGqlRoute(name, rb.query, rb);
114
135
 
@@ -119,7 +140,12 @@ export async function initQueryRoutes(
119
140
  if (rb.cacheIfFn || rb.transformFn || rb.preventCaching)
120
141
  throw new Error('caching or transform not supported');
121
142
 
122
- const route = new QueryHandleRoute(name, rb.handleFn, rb.vars);
143
+ const route = new QueryHandleRoute(
144
+ name,
145
+ rb.validIfFn,
146
+ rb.handleFn,
147
+ rb.vars,
148
+ );
123
149
 
124
150
  router.post('/queries/' + route.name, csr =>
125
151
  route.handle(caching.router, csr),
@@ -4,6 +4,12 @@ import ServerRouter from '../ServerRouter.js';
4
4
 
5
5
  export type CacheIfFn = (response: any, vars: Record<string, any>) => boolean;
6
6
 
7
+ /// Either throw or return a boolean
8
+ export type ValidIfFn = (
9
+ vars: Record<string, any>,
10
+ sr: ServerRouter,
11
+ ) => void | boolean;
12
+
7
13
  /// Anything other than returning undefined will replace the response
8
14
  //
9
15
  // Note that even if you return undefined since the response is by reference
@@ -20,13 +26,14 @@ export type HandleFn = (
20
26
  ) => Promise<any> | any;
21
27
 
22
28
  /**
23
- * Returns the validated variables if some vars where defined
29
+ * Returns the validated variables if some vars were defined
24
30
  * else just returns all vars
25
31
  */
26
32
  export function validateVars(
27
33
  qvars: Record<string, QueryVar> | null,
28
34
  vars: any,
29
- cs: ServerRouter,
35
+ validIfFn: ValidIfFn | null,
36
+ sr: ServerRouter,
30
37
  ): Record<string, any> {
31
38
  if (!vars || typeof vars !== 'object')
32
39
  throw new Error('expected an object as vars');
@@ -36,7 +43,14 @@ export function validateVars(
36
43
  const nVars: Record<string, any> = {};
37
44
 
38
45
  for (const [k, v] of Object.entries(qvars)) {
39
- nVars[k] = v.validValue(vars[k], cs);
46
+ nVars[k] = v.validValue(vars[k], sr);
47
+ }
48
+
49
+ if (validIfFn) {
50
+ // or throw
51
+ const valid = validIfFn(nVars, sr);
52
+ if (typeof valid === 'boolean' && !valid)
53
+ throw new Error('invalid variables for query');
40
54
  }
41
55
 
42
56
  return nVars;
package/src/std/index.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * - {@link std/stores}
11
11
  * - {@link std/sync}
12
12
  * - {@link std/rand}
13
+ * - {@link std/url}
13
14
  */
14
15
 
15
16
  /**
@@ -0,0 +1,78 @@
1
+ import { BaseRoute } from '../../routing/index.js';
2
+ import { deleteSearchParam, pathnameEq, searchEq, toUrl } from './utils.js';
3
+
4
+ /**
5
+ * Sets the search params of a URL based on the provided options.
6
+ *
7
+ * If a value is `null`, `undefined`, or an empty string, the corresponding
8
+ * search param will be deleted.
9
+ *
10
+ * #### Example
11
+ * ```js
12
+ * urlWithSearch(entry.url, { p: 1 });
13
+ * // or remove a value
14
+ * urlWithSearch(entry.url, { p: null });
15
+ * ```
16
+ */
17
+ export function urlWithSearch(
18
+ url: BaseRoute | URL | string | null | undefined,
19
+ opts: Record<string, string | number | null | undefined>,
20
+ ): string | null {
21
+ if (!url) return null;
22
+
23
+ url = toUrl(url);
24
+
25
+ for (const [k, v] of Object.entries(opts)) {
26
+ if (!deleteSearchParam(v)) {
27
+ url.searchParams.set(k, v as string);
28
+ } else {
29
+ url.searchParams.delete(k);
30
+ }
31
+ }
32
+
33
+ return url.href;
34
+ }
35
+
36
+ /**
37
+ * Compares two URLs for equality.
38
+ * Normally search and hash are ignored.
39
+ *
40
+ * If either or both url is `null` or `undefined`, the function will return
41
+ * `false`.
42
+ *
43
+ * #### Example
44
+ * ```svelte
45
+ * <script>
46
+ * import { getRoute } from 'crelte';
47
+ *
48
+ * const route = getRoute();
49
+ * </script>
50
+ *
51
+ * <a href={item.url} class:active={urlEq($route, item.url)}>
52
+ * {item.title}
53
+ * </a>
54
+ * ```
55
+ */
56
+ export function urlEq(
57
+ a: BaseRoute | URL | string | null | undefined,
58
+ b: BaseRoute | URL | string | null | undefined,
59
+ opts?: { search?: boolean; hash?: boolean },
60
+ ): boolean {
61
+ if (!a || !b) return false;
62
+
63
+ a = toUrl(a);
64
+ b = toUrl(b);
65
+
66
+ // check origin and pathname
67
+ const baseMatches =
68
+ a.origin === b.origin && pathnameEq(a.pathname, b.pathname);
69
+ if (!baseMatches) return false;
70
+
71
+ // check search
72
+ if (opts?.search && !searchEq(a.searchParams, b.searchParams)) return false;
73
+
74
+ // check hash
75
+ if (opts?.hash && a.hash !== b.hash) return false;
76
+
77
+ return true;
78
+ }
@@ -0,0 +1,48 @@
1
+ import { BaseRoute } from '../../routing/index.js';
2
+
3
+ /**
4
+ * Checks if a search param should be removed.
5
+ * This is the case if the value is `null`, `undefined`, or an empty string.
6
+ */
7
+ export function deleteSearchParam(value: string | number | null | undefined) {
8
+ return (
9
+ typeof value === 'undefined' ||
10
+ value === null ||
11
+ (typeof value === 'string' && value === '')
12
+ );
13
+ }
14
+
15
+ /**
16
+ * Converts a `BaseRoute`, `URL`, or string to a `URL` object.
17
+ */
18
+ export function toUrl(url: BaseRoute | URL | string): URL {
19
+ if (typeof url === 'string') return new URL(url);
20
+
21
+ if (url instanceof BaseRoute) return new URL(url.url);
22
+
23
+ return url;
24
+ }
25
+
26
+ /**
27
+ * Compares two `URLSearchParams` objects for equality.
28
+ */
29
+ export function searchEq(a: URLSearchParams, b: URLSearchParams): boolean {
30
+ if (a.size !== b.size) return false;
31
+
32
+ // Clone to avoid mutating the original objects
33
+ const cloneA = new URLSearchParams(a);
34
+ const cloneB = new URLSearchParams(b);
35
+
36
+ cloneA.sort();
37
+ cloneB.sort();
38
+
39
+ return cloneA.toString() === cloneB.toString();
40
+ }
41
+
42
+ /**
43
+ * Compares two pathnames for equality, ignoring trailing slashes.
44
+ */
45
+ export function pathnameEq(a: string, b: string): boolean {
46
+ // check for trailing slashes
47
+ return a === b || a === b + '/' || a + '/' === b;
48
+ }
package/src/vite/index.ts CHANGED
@@ -180,7 +180,7 @@ export default function crelte(opts?: CrelteOptions): Plugin {
180
180
  publicDir: isSsrBuild ? false : 'public',
181
181
  base: '/',
182
182
  server: {
183
- port: 8080,
183
+ port: config.server?.port ?? 8080,
184
184
  },
185
185
  optimizeDeps: {
186
186
  exclude: ['crelte'],
@@ -338,7 +338,9 @@ async function serveVite(env: EnvData, vite: ViteDevServer) {
338
338
  nReq: Connect.IncomingMessage,
339
339
  res: ServerResponse,
340
340
  ) => {
341
- const protocol = vite.config.server.https ? 'https' : 'http';
341
+ const protocol =
342
+ (nReq.headers['x-forwarded-proto'] as string | undefined) ??
343
+ (vite.config.server.https ? 'https' : 'http');
342
344
  const baseUrl = protocol + '://' + nReq.headers['host'];
343
345
 
344
346
  const req = requestToWebRequest(baseUrl, nReq);