@sveltejs/kit 1.0.0-next.46 → 1.0.0-next.460
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.
- package/README.md +12 -9
- package/package.json +93 -65
- package/scripts/special-types/$env+dynamic+private.md +8 -0
- package/scripts/special-types/$env+dynamic+public.md +8 -0
- package/scripts/special-types/$env+static+private.md +19 -0
- package/scripts/special-types/$env+static+public.md +7 -0
- package/scripts/special-types/$lib.md +1 -0
- package/src/cli.js +112 -0
- package/src/constants.js +7 -0
- package/src/core/adapt/builder.js +207 -0
- package/src/core/adapt/index.js +31 -0
- package/src/core/config/default-error.html +56 -0
- package/src/core/config/index.js +105 -0
- package/src/core/config/options.js +498 -0
- package/src/core/config/types.d.ts +1 -0
- package/src/core/env.js +112 -0
- package/src/core/generate_manifest/index.js +78 -0
- package/src/core/prerender/crawl.js +194 -0
- package/src/core/prerender/prerender.js +425 -0
- package/src/core/prerender/queue.js +80 -0
- package/src/core/sync/create_manifest_data/index.js +452 -0
- package/src/core/sync/create_manifest_data/types.d.ts +37 -0
- package/src/core/sync/sync.js +59 -0
- package/src/core/sync/utils.js +33 -0
- package/src/core/sync/write_ambient.js +53 -0
- package/src/core/sync/write_client_manifest.js +94 -0
- package/src/core/sync/write_matchers.js +25 -0
- package/src/core/sync/write_root.js +91 -0
- package/src/core/sync/write_tsconfig.js +195 -0
- package/src/core/sync/write_types/index.js +595 -0
- package/src/core/utils.js +70 -0
- package/src/exports/hooks/index.js +1 -0
- package/src/exports/hooks/sequence.js +44 -0
- package/src/exports/index.js +45 -0
- package/src/exports/node/index.js +145 -0
- package/src/exports/node/polyfills.js +40 -0
- package/src/exports/vite/build/build_server.js +349 -0
- package/src/exports/vite/build/build_service_worker.js +90 -0
- package/src/exports/vite/build/utils.js +160 -0
- package/src/exports/vite/dev/index.js +543 -0
- package/src/exports/vite/index.js +595 -0
- package/src/exports/vite/preview/index.js +186 -0
- package/src/exports/vite/types.d.ts +3 -0
- package/src/exports/vite/utils.js +345 -0
- package/src/runtime/app/env.js +1 -0
- package/src/runtime/app/environment.js +11 -0
- package/src/runtime/app/navigation.js +22 -0
- package/src/runtime/app/paths.js +1 -0
- package/src/runtime/app/stores.js +102 -0
- package/src/runtime/client/ambient.d.ts +24 -0
- package/src/runtime/client/client.js +1387 -0
- package/src/runtime/client/fetcher.js +60 -0
- package/src/runtime/client/parse.js +60 -0
- package/src/runtime/client/singletons.js +21 -0
- package/src/runtime/client/start.js +45 -0
- package/src/runtime/client/types.d.ts +88 -0
- package/src/runtime/client/utils.js +140 -0
- package/src/runtime/components/error.svelte +16 -0
- package/{assets → src/runtime}/components/layout.svelte +0 -0
- package/src/runtime/control.js +33 -0
- package/src/runtime/env/dynamic/private.js +1 -0
- package/src/runtime/env/dynamic/public.js +1 -0
- package/src/runtime/env-private.js +6 -0
- package/src/runtime/env-public.js +6 -0
- package/src/runtime/env.js +6 -0
- package/src/runtime/hash.js +16 -0
- package/src/runtime/paths.js +11 -0
- package/src/runtime/server/data/index.js +146 -0
- package/src/runtime/server/endpoint.js +63 -0
- package/src/runtime/server/index.js +339 -0
- package/src/runtime/server/page/cookie.js +25 -0
- package/src/runtime/server/page/crypto.js +239 -0
- package/src/runtime/server/page/csp.js +249 -0
- package/src/runtime/server/page/fetch.js +269 -0
- package/src/runtime/server/page/index.js +404 -0
- package/src/runtime/server/page/load_data.js +124 -0
- package/src/runtime/server/page/render.js +358 -0
- package/src/runtime/server/page/respond_with_error.js +92 -0
- package/src/runtime/server/page/serialize_data.js +58 -0
- package/src/runtime/server/page/types.d.ts +44 -0
- package/src/runtime/server/utils.js +209 -0
- package/src/utils/array.js +9 -0
- package/src/utils/error.js +22 -0
- package/src/utils/escape.js +46 -0
- package/src/utils/filesystem.js +108 -0
- package/src/utils/functions.js +16 -0
- package/src/utils/http.js +55 -0
- package/src/utils/misc.js +1 -0
- package/src/utils/routing.js +137 -0
- package/src/utils/url.js +142 -0
- package/svelte-kit.js +1 -1
- package/types/ambient.d.ts +332 -0
- package/types/index.d.ts +343 -0
- package/types/internal.d.ts +383 -0
- package/types/private.d.ts +213 -0
- package/CHANGELOG.md +0 -456
- package/assets/components/error.svelte +0 -13
- package/assets/runtime/app/env.js +0 -5
- package/assets/runtime/app/navigation.js +0 -44
- package/assets/runtime/app/paths.js +0 -1
- package/assets/runtime/app/stores.js +0 -93
- package/assets/runtime/chunks/utils.js +0 -22
- package/assets/runtime/internal/singletons.js +0 -23
- package/assets/runtime/internal/start.js +0 -771
- package/assets/runtime/paths.js +0 -12
- package/dist/chunks/index.js +0 -3522
- package/dist/chunks/index2.js +0 -587
- package/dist/chunks/index3.js +0 -246
- package/dist/chunks/index4.js +0 -539
- package/dist/chunks/index5.js +0 -763
- package/dist/chunks/index6.js +0 -322
- package/dist/chunks/standard.js +0 -99
- package/dist/chunks/utils.js +0 -83
- package/dist/cli.js +0 -546
- package/dist/ssr.js +0 -2581
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { 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
|
+
/**
|
|
25
|
+
* Serialize an error into a JSON string through `error_to_pojo`.
|
|
26
|
+
* This is necessary because `JSON.stringify(error) === '{}'`
|
|
27
|
+
*
|
|
28
|
+
* @param {Error | HttpError} error
|
|
29
|
+
* @param {(error: Error) => string | undefined} get_stack
|
|
30
|
+
*/
|
|
31
|
+
export function serialize_error(error, get_stack) {
|
|
32
|
+
return JSON.stringify(error_to_pojo(error, get_stack));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Transform an error into a POJO, by copying its `name`, `message`
|
|
37
|
+
* and (in dev) `stack`, plus any custom properties, plus recursively
|
|
38
|
+
* serialized `cause` properties.
|
|
39
|
+
* Our own HttpError gets a meta property attached so we can identify it on the client.
|
|
40
|
+
*
|
|
41
|
+
* @param {HttpError | Error } error
|
|
42
|
+
* @param {(error: Error) => string | undefined} get_stack
|
|
43
|
+
*/
|
|
44
|
+
export function error_to_pojo(error, get_stack) {
|
|
45
|
+
if (error instanceof HttpError) {
|
|
46
|
+
return /** @type {import('./page/types').SerializedHttpError} */ ({
|
|
47
|
+
message: error.message,
|
|
48
|
+
status: error.status,
|
|
49
|
+
__is_http_error: true // TODO we should probably make this unnecessary
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
name,
|
|
55
|
+
message,
|
|
56
|
+
stack,
|
|
57
|
+
// @ts-expect-error i guess typescript doesn't know about error.cause yet
|
|
58
|
+
cause,
|
|
59
|
+
...custom
|
|
60
|
+
} = error;
|
|
61
|
+
|
|
62
|
+
/** @type {Record<string, any>} */
|
|
63
|
+
const object = { name, message, stack: get_stack(error) };
|
|
64
|
+
|
|
65
|
+
if (cause) object.cause = error_to_pojo(cause, get_stack);
|
|
66
|
+
|
|
67
|
+
for (const key in custom) {
|
|
68
|
+
// @ts-expect-error
|
|
69
|
+
object[key] = custom[key];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return object;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// TODO: Remove for 1.0
|
|
76
|
+
/** @param {Record<string, any>} mod */
|
|
77
|
+
export function check_method_names(mod) {
|
|
78
|
+
['get', 'post', 'put', 'patch', 'del'].forEach((m) => {
|
|
79
|
+
if (m in mod) {
|
|
80
|
+
const replacement = m === 'del' ? 'DELETE' : m.toUpperCase();
|
|
81
|
+
throw Error(
|
|
82
|
+
`Endpoint method "${m}" has changed to "${replacement}". See https://github.com/sveltejs/kit/discussions/5359 for more information.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {import('types').SSRErrorPage} */
|
|
89
|
+
export const GENERIC_ERROR = {
|
|
90
|
+
id: '__error'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {Partial<Record<import('types').HttpMethod, any>>} mod
|
|
95
|
+
* @param {import('types').HttpMethod} method
|
|
96
|
+
*/
|
|
97
|
+
export function method_not_allowed(mod, method) {
|
|
98
|
+
return new Response(`${method} method not allowed`, {
|
|
99
|
+
status: 405,
|
|
100
|
+
headers: {
|
|
101
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405
|
|
102
|
+
// "The server must generate an Allow header field in a 405 status code response"
|
|
103
|
+
allow: allowed_methods(mod).join(', ')
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @param {Partial<Record<import('types').HttpMethod, any>>} mod */
|
|
109
|
+
export function allowed_methods(mod) {
|
|
110
|
+
const allowed = [];
|
|
111
|
+
|
|
112
|
+
for (const method in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) {
|
|
113
|
+
if (method in mod) allowed.push(method);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (mod.GET || mod.HEAD) allowed.push('HEAD');
|
|
117
|
+
|
|
118
|
+
return allowed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @param {any} data */
|
|
122
|
+
export function data_response(data) {
|
|
123
|
+
try {
|
|
124
|
+
return new Response(`window.__sveltekit_data = ${devalue(data)}`, {
|
|
125
|
+
headers: {
|
|
126
|
+
'content-type': 'application/javascript'
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
} catch (e) {
|
|
130
|
+
const error = /** @type {any} */ (e);
|
|
131
|
+
const match = /\[(\d+)\]\.data\.(.+)/.exec(error.path);
|
|
132
|
+
const message = match ? `${error.message} (data.${match[2]})` : error.message;
|
|
133
|
+
return new Response(`throw new Error(${JSON.stringify(message)})`, {
|
|
134
|
+
headers: {
|
|
135
|
+
'content-type': 'application/javascript'
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @template {'prerender' | 'ssr' | 'csr'} Option
|
|
143
|
+
* @template {Option extends 'prerender' ? import('types').PrerenderOption : boolean} Value
|
|
144
|
+
*
|
|
145
|
+
* @param {Array<import('types').SSRNode | undefined>} nodes
|
|
146
|
+
* @param {Option} option
|
|
147
|
+
*
|
|
148
|
+
* @returns {Value | undefined}
|
|
149
|
+
*/
|
|
150
|
+
export function get_option(nodes, option) {
|
|
151
|
+
return nodes.reduce((value, node) => {
|
|
152
|
+
// TODO remove for 1.0
|
|
153
|
+
for (const thing of [node?.server, node?.shared]) {
|
|
154
|
+
if (thing && ('router' in thing || 'hydrate' in thing)) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
'`export const hydrate` and `export const router` have been replaced with `export const csr`. See https://github.com/sveltejs/kit/pull/6446'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return /** @type {any} TypeScript's too dumb to understand this */ (
|
|
162
|
+
node?.shared?.[option] ?? node?.server?.[option] ?? value
|
|
163
|
+
);
|
|
164
|
+
}, /** @type {Value | undefined} */ (undefined));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Return as a response that renders the error.html
|
|
169
|
+
*
|
|
170
|
+
* @param {import('types').SSROptions} options
|
|
171
|
+
* @param {number} status
|
|
172
|
+
* @param {string} message
|
|
173
|
+
*/
|
|
174
|
+
export function static_error_page(options, status, message) {
|
|
175
|
+
return new Response(options.error_template({ status, message }), {
|
|
176
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
177
|
+
status
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {import('types').RequestEvent} event
|
|
183
|
+
* @param {import('types').SSROptions} options
|
|
184
|
+
* @param {Error} error
|
|
185
|
+
*/
|
|
186
|
+
export function handle_fatal_error(event, options, error) {
|
|
187
|
+
let status = 500;
|
|
188
|
+
|
|
189
|
+
if (error instanceof HttpError) {
|
|
190
|
+
status = error.status;
|
|
191
|
+
} else {
|
|
192
|
+
options.handle_error(error, event);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ideally we'd use sec-fetch-dest instead, but Safari — quelle surprise — doesn't support it
|
|
196
|
+
const type = negotiate(event.request.headers.get('accept') || 'text/html', [
|
|
197
|
+
'text/html',
|
|
198
|
+
'application/json'
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
if (event.url.pathname.endsWith(DATA_SUFFIX) || type === 'application/json') {
|
|
202
|
+
return new Response(serialize_error(error, options.get_stack), {
|
|
203
|
+
status,
|
|
204
|
+
headers: { 'content-type': 'application/json; charset=utf-8' }
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return static_error_page(options, status, error.message);
|
|
209
|
+
}
|
|
@@ -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
|
+
'&': '&',
|
|
8
|
+
'"': '"'
|
|
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,108 @@
|
|
|
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') return;
|
|
10
|
+
throw e;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {string} path */
|
|
15
|
+
export function rimraf(path) {
|
|
16
|
+
fs.rmSync(path, { force: true, recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} source
|
|
21
|
+
* @param {string} target
|
|
22
|
+
* @param {{
|
|
23
|
+
* filter?: (basename: string) => boolean;
|
|
24
|
+
* replace?: Record<string, string>;
|
|
25
|
+
* }} opts
|
|
26
|
+
*/
|
|
27
|
+
export function copy(source, target, opts = {}) {
|
|
28
|
+
if (!fs.existsSync(source)) return [];
|
|
29
|
+
|
|
30
|
+
/** @type {string[]} */
|
|
31
|
+
const files = [];
|
|
32
|
+
|
|
33
|
+
const prefix = posixify(target) + '/';
|
|
34
|
+
|
|
35
|
+
const regex = opts.replace
|
|
36
|
+
? new RegExp(`\\b(${Object.keys(opts.replace).join('|')})\\b`, 'g')
|
|
37
|
+
: null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} from
|
|
41
|
+
* @param {string} to
|
|
42
|
+
*/
|
|
43
|
+
function go(from, to) {
|
|
44
|
+
if (opts.filter && !opts.filter(path.basename(from))) return;
|
|
45
|
+
|
|
46
|
+
const stats = fs.statSync(from);
|
|
47
|
+
|
|
48
|
+
if (stats.isDirectory()) {
|
|
49
|
+
fs.readdirSync(from).forEach((file) => {
|
|
50
|
+
go(path.join(from, file), path.join(to, file));
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
mkdirp(path.dirname(to));
|
|
54
|
+
|
|
55
|
+
if (opts.replace) {
|
|
56
|
+
const data = fs.readFileSync(from, 'utf-8');
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
to,
|
|
59
|
+
data.replace(
|
|
60
|
+
/** @type {RegExp} */ (regex),
|
|
61
|
+
(_match, key) => /** @type {Record<string, string>} */ (opts.replace)[key]
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
fs.copyFileSync(from, to);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
files.push(to === target ? posixify(path.basename(to)) : posixify(to).replace(prefix, ''));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
go(source, target);
|
|
73
|
+
|
|
74
|
+
return files;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get a list of all files in a directory
|
|
79
|
+
* @param {string} cwd - the directory to walk
|
|
80
|
+
* @param {boolean} [dirs] - whether to include directories in the result
|
|
81
|
+
*/
|
|
82
|
+
export function walk(cwd, dirs = false) {
|
|
83
|
+
/** @type {string[]} */
|
|
84
|
+
const all_files = [];
|
|
85
|
+
|
|
86
|
+
/** @param {string} dir */
|
|
87
|
+
function walk_dir(dir) {
|
|
88
|
+
const files = fs.readdirSync(path.join(cwd, dir));
|
|
89
|
+
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const joined = path.join(dir, file);
|
|
92
|
+
const stats = fs.statSync(path.join(cwd, joined));
|
|
93
|
+
if (stats.isDirectory()) {
|
|
94
|
+
if (dirs) all_files.push(joined);
|
|
95
|
+
walk_dir(joined);
|
|
96
|
+
} else {
|
|
97
|
+
all_files.push(joined);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return walk_dir(''), all_files;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {string} str */
|
|
106
|
+
export function posixify(str) {
|
|
107
|
+
return str.replace(/\\/g, '/');
|
|
108
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const s = JSON.stringify;
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
if (/\]\[/.test(id)) {
|
|
16
|
+
throw new Error(`Invalid route ${id} — parameters must be separated`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (count_occurrences('[', id) !== count_occurrences(']', id)) {
|
|
20
|
+
throw new Error(`Invalid route ${id} — brackets are unbalanced`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pattern =
|
|
24
|
+
id === ''
|
|
25
|
+
? /^\/$/
|
|
26
|
+
: new RegExp(
|
|
27
|
+
`^${id
|
|
28
|
+
.split(/(?:\/|$)/)
|
|
29
|
+
.filter(affects_path)
|
|
30
|
+
.map((segment, i, segments) => {
|
|
31
|
+
const decoded_segment = decodeURIComponent(segment);
|
|
32
|
+
// special case — /[...rest]/ could contain zero segments
|
|
33
|
+
const match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(decoded_segment);
|
|
34
|
+
if (match) {
|
|
35
|
+
names.push(match[1]);
|
|
36
|
+
types.push(match[2]);
|
|
37
|
+
return '(?:/(.*))?';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const is_last = i === segments.length - 1;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
decoded_segment &&
|
|
44
|
+
'/' +
|
|
45
|
+
decoded_segment
|
|
46
|
+
.split(/\[(.+?)\]/)
|
|
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 [, rest, name, type] = match;
|
|
57
|
+
names.push(name);
|
|
58
|
+
types.push(type);
|
|
59
|
+
return rest ? '(.*?)' : '([^/]+?)';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (is_last && content.includes('.')) add_trailing_slash = false;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
content // allow users to specify characters on the file system in an encoded manner
|
|
66
|
+
.normalize()
|
|
67
|
+
// We use [ and ] to denote parameters, so users must encode these on the file
|
|
68
|
+
// system to match against them. We don't decode all characters since others
|
|
69
|
+
// can already be epressed and so that '%' can be easily used directly in filenames
|
|
70
|
+
.replace(/%5[Bb]/g, '[')
|
|
71
|
+
.replace(/%5[Dd]/g, ']')
|
|
72
|
+
// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
|
|
73
|
+
// They will not be touched by decodeURI so need to be encoded here, so
|
|
74
|
+
// that we can match against them.
|
|
75
|
+
// We skip '/' since you can't create a file with it on any OS
|
|
76
|
+
.replace(/#/g, '%23')
|
|
77
|
+
.replace(/\?/g, '%3F')
|
|
78
|
+
// escape characters that have special meaning in regex
|
|
79
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
80
|
+
); // TODO handle encoding
|
|
81
|
+
})
|
|
82
|
+
.join('')
|
|
83
|
+
);
|
|
84
|
+
})
|
|
85
|
+
.join('')}${add_trailing_slash ? '/?' : ''}$`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return { pattern, names, types };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns `false` for `(group)` segments
|
|
93
|
+
* @param {string} segment
|
|
94
|
+
*/
|
|
95
|
+
export function affects_path(segment) {
|
|
96
|
+
return !/^\([^)]+\)$/.test(segment);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {RegExpMatchArray} match
|
|
101
|
+
* @param {string[]} names
|
|
102
|
+
* @param {string[]} types
|
|
103
|
+
* @param {Record<string, import('types').ParamMatcher>} matchers
|
|
104
|
+
*/
|
|
105
|
+
export function exec(match, names, types, matchers) {
|
|
106
|
+
/** @type {Record<string, string>} */
|
|
107
|
+
const params = {};
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
110
|
+
const name = names[i];
|
|
111
|
+
const type = types[i];
|
|
112
|
+
const value = match[i + 1] || '';
|
|
113
|
+
|
|
114
|
+
if (type) {
|
|
115
|
+
const matcher = matchers[type];
|
|
116
|
+
if (!matcher) throw new Error(`Missing "${type}" param matcher`); // TODO do this ahead of time?
|
|
117
|
+
|
|
118
|
+
if (!matcher(value)) return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
params[name] = value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return params;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} needle
|
|
129
|
+
* @param {string} haystack
|
|
130
|
+
*/
|
|
131
|
+
function count_occurrences(needle, haystack) {
|
|
132
|
+
let count = 0;
|
|
133
|
+
for (let i = 0; i < haystack.length; i += 1) {
|
|
134
|
+
if (haystack[i] === needle) count += 1;
|
|
135
|
+
}
|
|
136
|
+
return count;
|
|
137
|
+
}
|