@sveltejs/kit 1.0.0-next.52 → 1.0.0-next.520

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 (128) hide show
  1. package/README.md +6 -3
  2. package/package.json +93 -67
  3. package/postinstall.js +47 -0
  4. package/scripts/special-types/$env+dynamic+private.md +10 -0
  5. package/scripts/special-types/$env+dynamic+public.md +8 -0
  6. package/scripts/special-types/$env+static+private.md +19 -0
  7. package/scripts/special-types/$env+static+public.md +7 -0
  8. package/scripts/special-types/$lib.md +5 -0
  9. package/src/cli.js +108 -0
  10. package/src/constants.js +7 -0
  11. package/src/core/adapt/builder.js +215 -0
  12. package/src/core/adapt/index.js +31 -0
  13. package/src/core/config/default-error.html +56 -0
  14. package/src/core/config/index.js +110 -0
  15. package/src/core/config/options.js +504 -0
  16. package/src/core/config/types.d.ts +1 -0
  17. package/src/core/env.js +121 -0
  18. package/src/core/generate_manifest/index.js +94 -0
  19. package/src/core/prerender/crawl.js +198 -0
  20. package/src/core/prerender/entities.js +2252 -0
  21. package/src/core/prerender/prerender.js +458 -0
  22. package/src/core/prerender/queue.js +80 -0
  23. package/src/core/sync/create_manifest_data/conflict.js +0 -0
  24. package/src/core/sync/create_manifest_data/index.js +470 -0
  25. package/src/core/sync/create_manifest_data/sort.js +163 -0
  26. package/src/core/sync/create_manifest_data/types.d.ts +37 -0
  27. package/src/core/sync/sync.js +78 -0
  28. package/src/core/sync/utils.js +33 -0
  29. package/src/core/sync/write_ambient.js +53 -0
  30. package/src/core/sync/write_client_manifest.js +106 -0
  31. package/src/core/sync/write_matchers.js +25 -0
  32. package/src/core/sync/write_root.js +91 -0
  33. package/src/core/sync/write_tsconfig.js +195 -0
  34. package/src/core/sync/write_types/index.js +783 -0
  35. package/src/core/utils.js +70 -0
  36. package/src/exports/hooks/index.js +1 -0
  37. package/src/exports/hooks/sequence.js +44 -0
  38. package/src/exports/index.js +45 -0
  39. package/src/exports/node/index.js +161 -0
  40. package/src/exports/node/polyfills.js +28 -0
  41. package/src/exports/vite/build/build_server.js +378 -0
  42. package/src/exports/vite/build/build_service_worker.js +91 -0
  43. package/src/exports/vite/build/utils.js +181 -0
  44. package/src/exports/vite/dev/index.js +581 -0
  45. package/src/exports/vite/graph_analysis/index.js +277 -0
  46. package/src/exports/vite/graph_analysis/types.d.ts +5 -0
  47. package/src/exports/vite/graph_analysis/utils.js +30 -0
  48. package/src/exports/vite/index.js +603 -0
  49. package/src/exports/vite/preview/index.js +189 -0
  50. package/src/exports/vite/types.d.ts +3 -0
  51. package/src/exports/vite/utils.js +157 -0
  52. package/src/runtime/app/env.js +1 -0
  53. package/src/runtime/app/environment.js +11 -0
  54. package/src/runtime/app/forms.js +123 -0
  55. package/src/runtime/app/navigation.js +23 -0
  56. package/src/runtime/app/paths.js +1 -0
  57. package/src/runtime/app/stores.js +102 -0
  58. package/src/runtime/client/ambient.d.ts +30 -0
  59. package/src/runtime/client/client.js +1595 -0
  60. package/src/runtime/client/fetcher.js +107 -0
  61. package/src/runtime/client/parse.js +60 -0
  62. package/src/runtime/client/singletons.js +21 -0
  63. package/src/runtime/client/start.js +37 -0
  64. package/src/runtime/client/types.d.ts +84 -0
  65. package/src/runtime/client/utils.js +159 -0
  66. package/src/runtime/components/error.svelte +16 -0
  67. package/{assets → src/runtime}/components/layout.svelte +0 -0
  68. package/src/runtime/control.js +98 -0
  69. package/src/runtime/env/dynamic/private.js +1 -0
  70. package/src/runtime/env/dynamic/public.js +1 -0
  71. package/src/runtime/env-private.js +6 -0
  72. package/src/runtime/env-public.js +6 -0
  73. package/src/runtime/env.js +6 -0
  74. package/src/runtime/hash.js +20 -0
  75. package/src/runtime/paths.js +11 -0
  76. package/src/runtime/server/cookie.js +166 -0
  77. package/src/runtime/server/data/index.js +131 -0
  78. package/src/runtime/server/endpoint.js +92 -0
  79. package/src/runtime/server/fetch.js +174 -0
  80. package/src/runtime/server/index.js +355 -0
  81. package/src/runtime/server/page/actions.js +256 -0
  82. package/src/runtime/server/page/crypto.js +239 -0
  83. package/src/runtime/server/page/csp.js +250 -0
  84. package/src/runtime/server/page/index.js +304 -0
  85. package/src/runtime/server/page/load_data.js +215 -0
  86. package/src/runtime/server/page/render.js +346 -0
  87. package/src/runtime/server/page/respond_with_error.js +102 -0
  88. package/src/runtime/server/page/serialize_data.js +87 -0
  89. package/src/runtime/server/page/types.d.ts +35 -0
  90. package/src/runtime/server/utils.js +181 -0
  91. package/src/utils/array.js +9 -0
  92. package/src/utils/error.js +22 -0
  93. package/src/utils/escape.js +46 -0
  94. package/src/utils/filesystem.js +142 -0
  95. package/src/utils/functions.js +16 -0
  96. package/src/utils/http.js +72 -0
  97. package/src/utils/misc.js +1 -0
  98. package/src/utils/promises.js +17 -0
  99. package/src/utils/routing.js +130 -0
  100. package/src/utils/unit_test.js +11 -0
  101. package/src/utils/url.js +144 -0
  102. package/svelte-kit.js +1 -1
  103. package/types/ambient.d.ts +431 -0
  104. package/types/index.d.ts +477 -0
  105. package/types/internal.d.ts +380 -0
  106. package/types/private.d.ts +224 -0
  107. package/CHANGELOG.md +0 -496
  108. package/assets/components/error.svelte +0 -13
  109. package/assets/runtime/app/env.js +0 -5
  110. package/assets/runtime/app/navigation.js +0 -44
  111. package/assets/runtime/app/paths.js +0 -1
  112. package/assets/runtime/app/stores.js +0 -93
  113. package/assets/runtime/chunks/utils.js +0 -22
  114. package/assets/runtime/internal/singletons.js +0 -23
  115. package/assets/runtime/internal/start.js +0 -776
  116. package/assets/runtime/paths.js +0 -12
  117. package/dist/chunks/index.js +0 -3537
  118. package/dist/chunks/index2.js +0 -587
  119. package/dist/chunks/index3.js +0 -246
  120. package/dist/chunks/index4.js +0 -568
  121. package/dist/chunks/index5.js +0 -763
  122. package/dist/chunks/index6.js +0 -323
  123. package/dist/chunks/standard.js +0 -99
  124. package/dist/chunks/utils.js +0 -83
  125. package/dist/cli.js +0 -555
  126. package/dist/ssr.js +0 -2604
  127. package/types.d.ts +0 -73
  128. package/types.internal.d.ts +0 -222
@@ -0,0 +1,181 @@
1
+ import * as devalue from 'devalue';
2
+ import { DATA_SUFFIX } from '../../constants.js';
3
+ import { negotiate } from '../../utils/http.js';
4
+ import { HttpError } from '../control.js';
5
+
6
+ /** @param {any} body */
7
+ export function is_pojo(body) {
8
+ if (typeof body !== 'object') return false;
9
+
10
+ if (body) {
11
+ if (body instanceof Uint8Array) return false;
12
+ if (body instanceof ReadableStream) return false;
13
+
14
+ // if body is a node Readable, throw an error
15
+ // TODO remove this for 1.0
16
+ if (body._readableState && typeof body.pipe === 'function') {
17
+ throw new Error('Node streams are no longer supported — use a ReadableStream instead');
18
+ }
19
+ }
20
+
21
+ return true;
22
+ }
23
+
24
+ // TODO: Remove for 1.0
25
+ /** @param {Record<string, any>} mod */
26
+ export function check_method_names(mod) {
27
+ ['get', 'post', 'put', 'patch', 'del'].forEach((m) => {
28
+ if (m in mod) {
29
+ const replacement = m === 'del' ? 'DELETE' : m.toUpperCase();
30
+ throw Error(
31
+ `Endpoint method "${m}" has changed to "${replacement}". See https://github.com/sveltejs/kit/discussions/5359 for more information.`
32
+ );
33
+ }
34
+ });
35
+ }
36
+
37
+ /** @type {import('types').SSRErrorPage} */
38
+ export const GENERIC_ERROR = {
39
+ id: '__error'
40
+ };
41
+
42
+ /**
43
+ * @param {Partial<Record<import('types').HttpMethod, any>>} mod
44
+ * @param {import('types').HttpMethod} method
45
+ */
46
+ export function method_not_allowed(mod, method) {
47
+ return new Response(`${method} method not allowed`, {
48
+ status: 405,
49
+ headers: {
50
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
51
+ // "The server must generate an Allow header field in a 405 status code response"
52
+ allow: allowed_methods(mod).join(', ')
53
+ }
54
+ });
55
+ }
56
+
57
+ /** @param {Partial<Record<import('types').HttpMethod, any>>} mod */
58
+ export function allowed_methods(mod) {
59
+ const allowed = [];
60
+
61
+ for (const method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) {
62
+ if (method in mod) allowed.push(method);
63
+ }
64
+
65
+ if (mod.GET || mod.HEAD) allowed.push('HEAD');
66
+
67
+ return allowed;
68
+ }
69
+
70
+ /**
71
+ * @param {any} data
72
+ * @param {import('types').RequestEvent} event
73
+ */
74
+ export function data_response(data, event) {
75
+ const headers = {
76
+ 'content-type': 'application/json',
77
+ 'cache-control': 'private, no-store'
78
+ };
79
+
80
+ try {
81
+ return new Response(devalue.stringify(data), { headers });
82
+ } catch (e) {
83
+ const error = /** @type {any} */ (e);
84
+ const match = /\[(\d+)\]\.data\.(.+)/.exec(error.path);
85
+ const message = match
86
+ ? `Data returned from \`load\` while rendering /${event.routeId} is not serializable: ${error.message} (data.${match[2]})`
87
+ : error.message;
88
+ return new Response(JSON.stringify(message), { headers, status: 500 });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @template {'prerender' | 'ssr' | 'csr'} Option
94
+ * @template {Option extends 'prerender' ? import('types').PrerenderOption : boolean} Value
95
+ *
96
+ * @param {Array<import('types').SSRNode | undefined>} nodes
97
+ * @param {Option} option
98
+ *
99
+ * @returns {Value | undefined}
100
+ */
101
+ export function get_option(nodes, option) {
102
+ return nodes.reduce((value, node) => {
103
+ // TODO remove for 1.0
104
+ for (const thing of [node?.server, node?.shared]) {
105
+ if (thing && ('router' in thing || 'hydrate' in thing)) {
106
+ throw new Error(
107
+ '`export const hydrate` and `export const router` have been replaced with `export const csr`. See https://github.com/sveltejs/kit/pull/6446'
108
+ );
109
+ }
110
+ }
111
+
112
+ return /** @type {any} TypeScript's too dumb to understand this */ (
113
+ node?.shared?.[option] ?? node?.server?.[option] ?? value
114
+ );
115
+ }, /** @type {Value | undefined} */ (undefined));
116
+ }
117
+
118
+ /**
119
+ * Return as a response that renders the error.html
120
+ *
121
+ * @param {import('types').SSROptions} options
122
+ * @param {number} status
123
+ * @param {string} message
124
+ */
125
+ export function static_error_page(options, status, message) {
126
+ return new Response(options.error_template({ status, message }), {
127
+ headers: { 'content-type': 'text/html; charset=utf-8' },
128
+ status
129
+ });
130
+ }
131
+
132
+ /**
133
+ * @param {import('types').RequestEvent} event
134
+ * @param {import('types').SSROptions} options
135
+ * @param {Error | HttpError} error
136
+ */
137
+ export function handle_fatal_error(event, options, error) {
138
+ const status = error instanceof HttpError ? error.status : 500;
139
+ const body = handle_error_and_jsonify(event, options, error);
140
+
141
+ // ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it
142
+ const type = negotiate(event.request.headers.get('accept') || 'text/html', [
143
+ 'application/json',
144
+ 'text/html'
145
+ ]);
146
+
147
+ if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
148
+ return new Response(JSON.stringify(body), {
149
+ status,
150
+ headers: { 'content-type': 'application/json; charset=utf-8' }
151
+ });
152
+ }
153
+
154
+ return static_error_page(options, status, body.message);
155
+ }
156
+
157
+ /**
158
+ * @param {import('types').RequestEvent} event
159
+ * @param {import('types').SSROptions} options
160
+ * @param {any} error
161
+ * @returns {App.Error}
162
+ */
163
+ export function handle_error_and_jsonify(event, options, error) {
164
+ if (error instanceof HttpError) {
165
+ return error.body;
166
+ } else {
167
+ return options.handle_error(error, event);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * @param {number} status
173
+ * @param {string} location
174
+ */
175
+ export function redirect_response(status, location) {
176
+ const response = new Response(undefined, {
177
+ status,
178
+ headers: { location }
179
+ });
180
+ return response;
181
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes nullish values from an array.
3
+ *
4
+ * @template T
5
+ * @param {Array<T>} arr
6
+ */
7
+ export function compact(arr) {
8
+ return arr.filter(/** @returns {val is NonNullable<T>} */ (val) => val != null);
9
+ }
@@ -0,0 +1,22 @@
1
+ import { HttpError, Redirect } from '../runtime/control.js';
2
+
3
+ /**
4
+ * @param {unknown} err
5
+ * @return {Error}
6
+ */
7
+ export function coalesce_to_error(err) {
8
+ return err instanceof Error ||
9
+ (err && /** @type {any} */ (err).name && /** @type {any} */ (err).message)
10
+ ? /** @type {Error} */ (err)
11
+ : new Error(JSON.stringify(err));
12
+ }
13
+
14
+ /**
15
+ * This is an identity function that exists to make TypeScript less
16
+ * paranoid about people throwing things that aren't errors, which
17
+ * frankly is not something we should care about
18
+ * @param {unknown} error
19
+ */
20
+ export function normalize_error(error) {
21
+ return /** @type {Redirect | HttpError | Error} */ (error);
22
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * When inside a double-quoted attribute value, only `&` and `"` hold special meaning.
3
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
4
+ * @type {Record<string, string>}
5
+ */
6
+ const escape_html_attr_dict = {
7
+ '&': '&amp;',
8
+ '"': '&quot;'
9
+ };
10
+
11
+ const escape_html_attr_regex = new RegExp(
12
+ // special characters
13
+ `[${Object.keys(escape_html_attr_dict).join('')}]|` +
14
+ // high surrogate without paired low surrogate
15
+ '[\\ud800-\\udbff](?![\\udc00-\\udfff])|' +
16
+ // a valid surrogate pair, the only match with 2 code units
17
+ // we match it so that we can match unpaired low surrogates in the same pass
18
+ // TODO: use lookbehind assertions once they are widely supported: (?<![\ud800-udbff])[\udc00-\udfff]
19
+ '[\\ud800-\\udbff][\\udc00-\\udfff]|' +
20
+ // unpaired low surrogate (see previous match)
21
+ '[\\udc00-\\udfff]',
22
+ 'g'
23
+ );
24
+
25
+ /**
26
+ * Formats a string to be used as an attribute's value in raw HTML.
27
+ *
28
+ * It escapes unpaired surrogates (which are allowed in js strings but invalid in HTML), escapes
29
+ * characters that are special in attributes, and surrounds the whole string in double-quotes.
30
+ *
31
+ * @param {string} str
32
+ * @returns {string} Escaped string surrounded by double-quotes.
33
+ * @example const html = `<tag data-value=${escape_html_attr('value')}>...</tag>`;
34
+ */
35
+ export function escape_html_attr(str) {
36
+ const escaped_str = str.replace(escape_html_attr_regex, (match) => {
37
+ if (match.length === 2) {
38
+ // valid surrogate pair
39
+ return match;
40
+ }
41
+
42
+ return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`;
43
+ });
44
+
45
+ return `"${escaped_str}"`;
46
+ }
@@ -0,0 +1,142 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /** @param {string} dir */
5
+ export function mkdirp(dir) {
6
+ try {
7
+ fs.mkdirSync(dir, { recursive: true });
8
+ } catch (/** @type {any} */ e) {
9
+ if (e.code === 'EEXIST') {
10
+ if (!fs.statSync(dir).isDirectory()) {
11
+ throw new Error(`Cannot create directory ${dir}, a file already exists at this position`);
12
+ }
13
+ return;
14
+ }
15
+ throw e;
16
+ }
17
+ }
18
+
19
+ /** @param {string} path */
20
+ export function rimraf(path) {
21
+ fs.rmSync(path, { force: true, recursive: true });
22
+ }
23
+
24
+ /**
25
+ * @param {string} source
26
+ * @param {string} target
27
+ * @param {{
28
+ * filter?: (basename: string) => boolean;
29
+ * replace?: Record<string, string>;
30
+ * }} opts
31
+ */
32
+ export function copy(source, target, opts = {}) {
33
+ if (!fs.existsSync(source)) return [];
34
+
35
+ /** @type {string[]} */
36
+ const files = [];
37
+
38
+ const prefix = posixify(target) + '/';
39
+
40
+ const regex = opts.replace
41
+ ? new RegExp(`\\b(${Object.keys(opts.replace).join('|')})\\b`, 'g')
42
+ : null;
43
+
44
+ /**
45
+ * @param {string} from
46
+ * @param {string} to
47
+ */
48
+ function go(from, to) {
49
+ if (opts.filter && !opts.filter(path.basename(from))) return;
50
+
51
+ const stats = fs.statSync(from);
52
+
53
+ if (stats.isDirectory()) {
54
+ fs.readdirSync(from).forEach((file) => {
55
+ go(path.join(from, file), path.join(to, file));
56
+ });
57
+ } else {
58
+ mkdirp(path.dirname(to));
59
+
60
+ if (opts.replace) {
61
+ const data = fs.readFileSync(from, 'utf-8');
62
+ fs.writeFileSync(
63
+ to,
64
+ data.replace(
65
+ /** @type {RegExp} */ (regex),
66
+ (_match, key) => /** @type {Record<string, string>} */ (opts.replace)[key]
67
+ )
68
+ );
69
+ } else {
70
+ fs.copyFileSync(from, to);
71
+ }
72
+
73
+ files.push(to === target ? posixify(path.basename(to)) : posixify(to).replace(prefix, ''));
74
+ }
75
+ }
76
+
77
+ go(source, target);
78
+
79
+ return files;
80
+ }
81
+
82
+ /**
83
+ * Get a list of all files in a directory
84
+ * @param {string} cwd - the directory to walk
85
+ * @param {boolean} [dirs] - whether to include directories in the result
86
+ */
87
+ export function walk(cwd, dirs = false) {
88
+ /** @type {string[]} */
89
+ const all_files = [];
90
+
91
+ /** @param {string} dir */
92
+ function walk_dir(dir) {
93
+ const files = fs.readdirSync(path.join(cwd, dir));
94
+
95
+ for (const file of files) {
96
+ const joined = path.join(dir, file);
97
+ const stats = fs.statSync(path.join(cwd, joined));
98
+ if (stats.isDirectory()) {
99
+ if (dirs) all_files.push(joined);
100
+ walk_dir(joined);
101
+ } else {
102
+ all_files.push(joined);
103
+ }
104
+ }
105
+ }
106
+
107
+ return walk_dir(''), all_files;
108
+ }
109
+
110
+ /** @param {string} str */
111
+ export function posixify(str) {
112
+ return str.replace(/\\/g, '/');
113
+ }
114
+
115
+ /**
116
+ * Given an entry point like [cwd]/src/hooks, returns a filename like [cwd]/src/hooks.js or [cwd]/src/hooks/index.js
117
+ * @param {string} entry
118
+ * @returns {string|null}
119
+ */
120
+ export function resolve_entry(entry) {
121
+ if (fs.existsSync(entry)) {
122
+ const stats = fs.statSync(entry);
123
+ if (stats.isDirectory()) {
124
+ return resolve_entry(path.join(entry, 'index'));
125
+ }
126
+
127
+ return entry;
128
+ } else {
129
+ const dir = path.dirname(entry);
130
+
131
+ if (fs.existsSync(dir)) {
132
+ const base = path.basename(entry);
133
+ const files = fs.readdirSync(dir);
134
+
135
+ const found = files.find((file) => file.replace(/\.[^.]+$/, '') === base);
136
+
137
+ if (found) return path.join(dir, found);
138
+ }
139
+ }
140
+
141
+ return null;
142
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @template T
3
+ * @param {() => T} fn
4
+ */
5
+ export function once(fn) {
6
+ let done = false;
7
+
8
+ /** @type T */
9
+ let result;
10
+
11
+ return () => {
12
+ if (done) return result;
13
+ done = true;
14
+ return (result = fn());
15
+ };
16
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Given an Accept header and a list of possible content types, pick
3
+ * the most suitable one to respond with
4
+ * @param {string} accept
5
+ * @param {string[]} types
6
+ */
7
+ export function negotiate(accept, types) {
8
+ /** @type {Array<{ type: string, subtype: string, q: number, i: number }>} */
9
+ const parts = [];
10
+
11
+ accept.split(',').forEach((str, i) => {
12
+ const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str);
13
+
14
+ // no match equals invalid header — ignore
15
+ if (match) {
16
+ const [, type, subtype, q = '1'] = match;
17
+ parts.push({ type, subtype, q: +q, i });
18
+ }
19
+ });
20
+
21
+ parts.sort((a, b) => {
22
+ if (a.q !== b.q) {
23
+ return b.q - a.q;
24
+ }
25
+
26
+ if ((a.subtype === '*') !== (b.subtype === '*')) {
27
+ return a.subtype === '*' ? 1 : -1;
28
+ }
29
+
30
+ if ((a.type === '*') !== (b.type === '*')) {
31
+ return a.type === '*' ? 1 : -1;
32
+ }
33
+
34
+ return a.i - b.i;
35
+ });
36
+
37
+ let accepted;
38
+ let min_priority = Infinity;
39
+
40
+ for (const mimetype of types) {
41
+ const [type, subtype] = mimetype.split('/');
42
+ const priority = parts.findIndex(
43
+ (part) =>
44
+ (part.type === type || part.type === '*') &&
45
+ (part.subtype === subtype || part.subtype === '*')
46
+ );
47
+
48
+ if (priority !== -1 && priority < min_priority) {
49
+ accepted = mimetype;
50
+ min_priority = priority;
51
+ }
52
+ }
53
+
54
+ return accepted;
55
+ }
56
+
57
+ /**
58
+ * Returns `true` if the request contains a `content-type` header with the given type
59
+ * @param {Request} request
60
+ * @param {...string} types
61
+ */
62
+ export function is_content_type(request, ...types) {
63
+ const type = request.headers.get('content-type')?.split(';', 1)[0].trim() ?? '';
64
+ return types.includes(type);
65
+ }
66
+
67
+ /**
68
+ * @param {Request} request
69
+ */
70
+ export function is_form_content_type(request) {
71
+ return is_content_type(request, 'application/x-www-form-urlencoded', 'multipart/form-data');
72
+ }
@@ -0,0 +1 @@
1
+ export const s = JSON.stringify;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Given an object, return a new object where all top level values are awaited
3
+ *
4
+ * @param {Record<string, any>} object
5
+ * @returns {Promise<Record<string, any>>}
6
+ */
7
+ export async function unwrap_promises(object) {
8
+ for (const key in object) {
9
+ if (typeof object[key]?.then === 'function') {
10
+ return Object.fromEntries(
11
+ await Promise.all(Object.entries(object).map(async ([key, value]) => [key, await value]))
12
+ );
13
+ }
14
+ }
15
+
16
+ return object;
17
+ }
@@ -0,0 +1,130 @@
1
+ const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
2
+
3
+ /** @param {string} id */
4
+ export function parse_route_id(id) {
5
+ /** @type {string[]} */
6
+ const names = [];
7
+
8
+ /** @type {string[]} */
9
+ const types = [];
10
+
11
+ // `/foo` should get an optional trailing slash, `/foo.json` should not
12
+ // const add_trailing_slash = !/\.[a-z]+$/.test(key);
13
+ let add_trailing_slash = true;
14
+
15
+ const pattern =
16
+ id === ''
17
+ ? /^\/$/
18
+ : new RegExp(
19
+ `^${id
20
+ .split(/(?:\/|$)/)
21
+ .filter(affects_path)
22
+ .map((segment, i, segments) => {
23
+ const decoded_segment = decodeURIComponent(segment);
24
+ // special case — /[...rest]/ could contain zero segments
25
+ const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(decoded_segment);
26
+ if (rest_match) {
27
+ names.push(rest_match[1]);
28
+ types.push(rest_match[2]);
29
+ return '(?:/(.*))?';
30
+ }
31
+ // special case — /[[optional]]/ could contain zero segments
32
+ const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(decoded_segment);
33
+ if (optional_match) {
34
+ names.push(optional_match[1]);
35
+ types.push(optional_match[2]);
36
+ return '(?:/([^/]+))?';
37
+ }
38
+
39
+ const is_last = i === segments.length - 1;
40
+
41
+ if (!decoded_segment) {
42
+ return;
43
+ }
44
+
45
+ const parts = decoded_segment.split(/\[(.+?)\](?!\])/);
46
+ const result = parts
47
+ .map((content, i) => {
48
+ if (i % 2) {
49
+ const match = param_pattern.exec(content);
50
+ if (!match) {
51
+ throw new Error(
52
+ `Invalid param: ${content}. Params and matcher names can only have underscores and alphanumeric characters.`
53
+ );
54
+ }
55
+
56
+ const [, optional, rest, name, type] = match;
57
+ // It's assumed that the following invalid route id cases are already checked
58
+ // - unbalanced brackets
59
+ // - optional param following rest param
60
+
61
+ names.push(name);
62
+ types.push(type);
63
+ return rest ? '(.*?)' : optional ? '([^/]*)?' : '([^/]+?)';
64
+ }
65
+
66
+ if (is_last && content.includes('.')) add_trailing_slash = false;
67
+
68
+ return (
69
+ content // allow users to specify characters on the file system in an encoded manner
70
+ .normalize()
71
+ // We use [ and ] to denote parameters, so users must encode these on the file
72
+ // system to match against them. We don't decode all characters since others
73
+ // can already be epressed and so that '%' can be easily used directly in filenames
74
+ .replace(/%5[Bb]/g, '[')
75
+ .replace(/%5[Dd]/g, ']')
76
+ // '#', '/', and '?' can only appear in URL path segments in an encoded manner.
77
+ // They will not be touched by decodeURI so need to be encoded here, so
78
+ // that we can match against them.
79
+ // We skip '/' since you can't create a file with it on any OS
80
+ .replace(/#/g, '%23')
81
+ .replace(/\?/g, '%3F')
82
+ // escape characters that have special meaning in regex
83
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
84
+ ); // TODO handle encoding
85
+ })
86
+ .join('');
87
+
88
+ return '/' + result;
89
+ })
90
+ .join('')}${add_trailing_slash ? '/?' : ''}$`
91
+ );
92
+
93
+ return { pattern, names, types };
94
+ }
95
+
96
+ /**
97
+ * Returns `false` for `(group)` segments
98
+ * @param {string} segment
99
+ */
100
+ export function affects_path(segment) {
101
+ return !/^\([^)]+\)$/.test(segment);
102
+ }
103
+
104
+ /**
105
+ * @param {RegExpMatchArray} match
106
+ * @param {string[]} names
107
+ * @param {string[]} types
108
+ * @param {Record<string, import('types').ParamMatcher>} matchers
109
+ */
110
+ export function exec(match, names, types, matchers) {
111
+ /** @type {Record<string, string>} */
112
+ const params = {};
113
+
114
+ for (let i = 0; i < names.length; i += 1) {
115
+ const name = names[i];
116
+ const type = types[i];
117
+ let value = match[i + 1] || '';
118
+
119
+ if (type) {
120
+ const matcher = matchers[type];
121
+ if (!matcher) throw new Error(`Missing "${type}" param matcher`); // TODO do this ahead of time?
122
+
123
+ if (!matcher(value)) return;
124
+ }
125
+
126
+ params[name] = value;
127
+ }
128
+
129
+ return params;
130
+ }
@@ -0,0 +1,11 @@
1
+ import { suite } from 'uvu';
2
+
3
+ /**
4
+ * @param {string} name
5
+ * @param {(suite: import('uvu').Test<import('uvu').Context>) => void} fn
6
+ */
7
+ export function describe(name, fn) {
8
+ const s = suite(name);
9
+ fn(s);
10
+ s.run();
11
+ }