@sveltejs/kit 3.0.0-next.6 → 3.0.0-next.7
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/package.json +7 -2
- package/src/core/adapt/builder.js +11 -39
- package/src/core/config/index.js +76 -71
- package/src/core/config/options.js +280 -285
- package/src/core/config/types.d.ts +1 -1
- package/src/core/env.js +85 -1
- package/src/core/generate_manifest/index.js +12 -15
- package/src/core/sync/create_manifest_data/index.js +6 -44
- package/src/core/sync/write_client_manifest.js +7 -14
- package/src/core/sync/write_non_ambient.js +10 -7
- package/src/core/sync/write_root.js +9 -30
- package/src/core/sync/write_server.js +0 -1
- package/src/core/sync/write_types/index.js +11 -10
- package/src/exports/index.js +3 -3
- package/src/exports/internal/client.js +5 -0
- package/src/exports/internal/index.js +1 -91
- package/src/exports/internal/{event.js → server/event.js} +1 -2
- package/src/exports/internal/server/index.js +33 -0
- package/src/exports/internal/shared.js +89 -0
- package/src/exports/node/index.js +1 -1
- package/src/exports/params.js +63 -0
- package/src/exports/public.d.ts +82 -32
- package/src/exports/url.js +84 -0
- package/src/exports/vite/dev/index.js +28 -17
- package/src/exports/vite/index.js +217 -156
- package/src/exports/vite/preview/index.js +1 -1
- package/src/exports/vite/utils.js +3 -5
- package/src/runtime/app/paths/client.js +3 -7
- package/src/runtime/app/paths/server.js +1 -1
- package/src/runtime/app/server/remote/query.js +6 -12
- package/src/runtime/app/state/client.js +1 -2
- package/src/runtime/app/stores.js +13 -76
- package/src/runtime/client/client.js +26 -79
- package/src/runtime/client/remote-functions/cache.svelte.js +3 -1
- package/src/runtime/client/remote-functions/form.svelte.js +5 -24
- package/src/runtime/client/remote-functions/prerender.svelte.js +10 -3
- package/src/runtime/client/remote-functions/query/instance.svelte.js +18 -9
- package/src/runtime/client/remote-functions/query-live/instance.svelte.js +16 -6
- package/src/runtime/client/remote-functions/shared.svelte.js +1 -2
- package/src/runtime/client/state.svelte.js +66 -49
- package/src/runtime/client/types.d.ts +1 -1
- package/src/runtime/client/utils.js +0 -96
- package/src/runtime/form-utils.js +15 -2
- package/src/runtime/server/index.js +1 -1
- package/src/runtime/server/page/load_data.js +1 -1
- package/src/runtime/server/page/render.js +15 -24
- package/src/runtime/server/page/server_routing.js +13 -9
- package/src/runtime/server/remote.js +21 -12
- package/src/runtime/server/respond.js +11 -8
- package/src/runtime/telemetry/otel.js +1 -1
- package/src/types/ambient.d.ts +5 -1
- package/src/types/global-private.d.ts +1 -1
- package/src/types/internal.d.ts +9 -10
- package/src/utils/error.js +1 -1
- package/src/utils/mime.js +9 -0
- package/src/utils/params.js +66 -0
- package/src/utils/routing.js +84 -38
- package/src/utils/streaming.js +14 -4
- package/src/utils/url.js +0 -79
- package/src/version.js +1 -1
- package/types/index.d.ts +98 -87
- package/types/index.d.ts.map +10 -7
- package/src/exports/internal/server.js +0 -22
- /package/src/exports/internal/{remote-functions.js → server/remote-functions.js} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('types').RouteData[]} routes
|
|
6
|
+
* @returns {Set<string>}
|
|
7
|
+
*/
|
|
8
|
+
export function collect_matcher_names(routes) {
|
|
9
|
+
/** @type {Set<string>} */
|
|
10
|
+
const names = new Set();
|
|
11
|
+
|
|
12
|
+
for (const route of routes) {
|
|
13
|
+
for (const param of route.params) {
|
|
14
|
+
if (param.matcher) names.add(param.matcher);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return names;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Record<string, unknown>} params
|
|
23
|
+
* @param {Set<string>} names
|
|
24
|
+
* @param {string} [file]
|
|
25
|
+
*/
|
|
26
|
+
export function validate_param_matchers(params, names, file) {
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
if (!(name in params)) {
|
|
29
|
+
throw new Error(`No matcher found for parameter '${name}'${file ? ` in ${file}` : ''}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {{
|
|
36
|
+
* routes: import('types').RouteData[];
|
|
37
|
+
* params_path: string | null;
|
|
38
|
+
* root: string;
|
|
39
|
+
* load?: (file: string) => Promise<Record<string, unknown>>;
|
|
40
|
+
* }} opts
|
|
41
|
+
* @returns {Promise<Record<string, import('@sveltejs/kit').ParamMatcher> | null>}
|
|
42
|
+
*/
|
|
43
|
+
export async function load_and_validate_params({ routes, params_path, root, load }) {
|
|
44
|
+
const names = collect_matcher_names(routes);
|
|
45
|
+
|
|
46
|
+
if (names.size === 0) return null;
|
|
47
|
+
|
|
48
|
+
if (!params_path) {
|
|
49
|
+
throw new Error(`No matcher found for parameter '${names.values().next().value}'`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const file = path.resolve(root, params_path);
|
|
53
|
+
const module = load ? await load(file) : await import(pathToFileURL(file).href);
|
|
54
|
+
|
|
55
|
+
if (!module.params || typeof module.params !== 'object') {
|
|
56
|
+
throw new Error(`${params_path} does not export \`params\` from \`defineParams\``);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
validate_param_matchers(
|
|
60
|
+
/** @type {Record<string, unknown>} */ (module.params),
|
|
61
|
+
names,
|
|
62
|
+
params_path
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return /** @type {Record<string, import('@sveltejs/kit').ParamMatcher>} */ (module.params);
|
|
66
|
+
}
|
package/src/utils/routing.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BROWSER } from 'esm-env';
|
|
2
|
-
import { decode_params } from './url.js';
|
|
3
2
|
|
|
4
3
|
const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
|
|
5
4
|
|
|
@@ -134,13 +133,43 @@ export function get_route_segments(route) {
|
|
|
134
133
|
return route.slice(1).split('/').filter(affects_path);
|
|
135
134
|
}
|
|
136
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @param {import('@sveltejs/kit').ParamMatcher} matcher
|
|
138
|
+
* @param {string} value
|
|
139
|
+
* @returns {{ success: true, value: any } | { success: false }}
|
|
140
|
+
*/
|
|
141
|
+
function run_matcher(matcher, value) {
|
|
142
|
+
const result = matcher['~standard'].validate(value);
|
|
143
|
+
|
|
144
|
+
if (result instanceof Promise) {
|
|
145
|
+
throw new Error('Async param matchers are not supported');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (result.issues) {
|
|
149
|
+
return { success: false };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parsed = result.value;
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
typeof parsed !== 'string' &&
|
|
156
|
+
typeof parsed !== 'number' &&
|
|
157
|
+
typeof parsed !== 'boolean' &&
|
|
158
|
+
typeof parsed !== 'bigint'
|
|
159
|
+
) {
|
|
160
|
+
throw new Error('Param matcher must return a string, number, boolean, or bigint');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { success: true, value: parsed };
|
|
164
|
+
}
|
|
165
|
+
|
|
137
166
|
/**
|
|
138
167
|
* @param {RegExpMatchArray} match
|
|
139
168
|
* @param {import('types').RouteParam[]} params
|
|
140
169
|
* @param {Record<string, import('@sveltejs/kit').ParamMatcher>} matchers
|
|
141
170
|
*/
|
|
142
171
|
export function exec(match, params, matchers) {
|
|
143
|
-
/** @type {Record<string,
|
|
172
|
+
/** @type {Record<string, any>} */
|
|
144
173
|
const result = {};
|
|
145
174
|
|
|
146
175
|
const values = match.slice(1);
|
|
@@ -173,37 +202,41 @@ export function exec(match, params, matchers) {
|
|
|
173
202
|
}
|
|
174
203
|
}
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
result[param.name] = value;
|
|
205
|
+
const decoded = decodeURIComponent(value);
|
|
178
206
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
if (param.matcher) {
|
|
208
|
+
const outcome = run_matcher(matchers[param.matcher], decoded);
|
|
209
|
+
|
|
210
|
+
if (!outcome.success) {
|
|
211
|
+
// in the `/[[a=b]]/...` case, if the value didn't satisfy the matcher,
|
|
212
|
+
// keep track of the number of skipped optional parameters and continue
|
|
213
|
+
if (param.optional && param.chained) {
|
|
214
|
+
buffered++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
186
217
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
!next_param &&
|
|
190
|
-
!next_value &&
|
|
191
|
-
Object.keys(result).length === values_needing_match.length
|
|
192
|
-
) {
|
|
193
|
-
buffered = 0;
|
|
218
|
+
// otherwise, if the matcher returns `false`, the route did not match
|
|
219
|
+
return;
|
|
194
220
|
}
|
|
195
|
-
|
|
221
|
+
|
|
222
|
+
result[param.name] = outcome.value;
|
|
223
|
+
} else {
|
|
224
|
+
result[param.name] = decoded;
|
|
196
225
|
}
|
|
197
226
|
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
// Now that the params match, reset the buffer if the next param isn't the [...rest]
|
|
228
|
+
// and the next value is defined, otherwise the buffer will cause us to skip values
|
|
229
|
+
const next_param = params[i + 1];
|
|
230
|
+
const next_value = values[i + 1];
|
|
231
|
+
if (next_param && !next_param.rest && next_param.optional && next_value && param.chained) {
|
|
232
|
+
buffered = 0;
|
|
203
233
|
}
|
|
204
234
|
|
|
205
|
-
//
|
|
206
|
-
|
|
235
|
+
// There are no more params and no more values, but all non-empty values have been matched
|
|
236
|
+
if (!next_param && !next_value && Object.keys(result).length === values_needing_match.length) {
|
|
237
|
+
buffered = 0;
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
207
240
|
}
|
|
208
241
|
|
|
209
242
|
if (buffered) return;
|
|
@@ -242,7 +275,7 @@ const basic_param_pattern = /\[(\[)?(\.\.\.)?(\w+?)(?:=(\w+))?\]\]?/g;
|
|
|
242
275
|
* ); // `/blog/hello-world/something/else`
|
|
243
276
|
* ```
|
|
244
277
|
* @param {string} id
|
|
245
|
-
* @param {Record<string,
|
|
278
|
+
* @param {Record<string, import('@sveltejs/kit').ParamValue | undefined>} params
|
|
246
279
|
* @returns {string}
|
|
247
280
|
*/
|
|
248
281
|
export function resolve_route(id, params) {
|
|
@@ -254,20 +287,33 @@ export function resolve_route(id, params) {
|
|
|
254
287
|
segments
|
|
255
288
|
.map((segment) =>
|
|
256
289
|
segment.replace(basic_param_pattern, (_, optional, rest, name) => {
|
|
257
|
-
const
|
|
290
|
+
const value = params[name];
|
|
258
291
|
|
|
259
|
-
|
|
260
|
-
if (!param_value) {
|
|
292
|
+
if (value === undefined || value === '') {
|
|
261
293
|
if (optional) return '';
|
|
262
|
-
if (rest &&
|
|
294
|
+
if (rest && value !== undefined) return '';
|
|
263
295
|
throw new Error(`Missing parameter '${name}' in route ${id}`);
|
|
264
296
|
}
|
|
265
297
|
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
298
|
+
if (typeof value === 'string') {
|
|
299
|
+
if (value.startsWith('/') || value.endsWith('/')) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Parameter '${name}' in route ${id} cannot start or end with a slash -- this would cause an invalid route like foo//bar`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (
|
|
309
|
+
typeof value === 'number' ||
|
|
310
|
+
typeof value === 'boolean' ||
|
|
311
|
+
typeof value === 'bigint'
|
|
312
|
+
) {
|
|
313
|
+
return String(value);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
throw new Error('Parameter values must be a string, number, boolean, or bigint');
|
|
271
317
|
})
|
|
272
318
|
)
|
|
273
319
|
.filter(Boolean)
|
|
@@ -290,7 +336,7 @@ export function has_server_load(node) {
|
|
|
290
336
|
* @param {string} path - The decoded pathname to match
|
|
291
337
|
* @param {Route[]} routes
|
|
292
338
|
* @param {Record<string, import('@sveltejs/kit').ParamMatcher>} matchers
|
|
293
|
-
* @returns {{ route: Route, params: Record<string,
|
|
339
|
+
* @returns {{ route: Route, params: Record<string, any> } | null}
|
|
294
340
|
*/
|
|
295
341
|
export function find_route(path, routes, matchers) {
|
|
296
342
|
for (const route of routes) {
|
|
@@ -301,7 +347,7 @@ export function find_route(path, routes, matchers) {
|
|
|
301
347
|
if (matched) {
|
|
302
348
|
return {
|
|
303
349
|
route,
|
|
304
|
-
params:
|
|
350
|
+
params: matched
|
|
305
351
|
};
|
|
306
352
|
}
|
|
307
353
|
}
|
package/src/utils/streaming.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { noop } from './functions.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Create an async iterator and a function to push values into it
|
|
3
5
|
* @template T
|
|
@@ -30,10 +32,18 @@ export function create_async_iterator() {
|
|
|
30
32
|
};
|
|
31
33
|
},
|
|
32
34
|
add: (promise) => {
|
|
33
|
-
|
|
34
|
-
void promise.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const next = Promise.withResolvers();
|
|
36
|
+
void next.promise.catch(noop); // prevent unhandled rejection potentially crashing the process
|
|
37
|
+
deferred.push(next);
|
|
38
|
+
|
|
39
|
+
void promise.then(
|
|
40
|
+
(value) => {
|
|
41
|
+
deferred[++resolved].resolve(value);
|
|
42
|
+
},
|
|
43
|
+
(error) => {
|
|
44
|
+
deferred[++resolved].reject(error);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
37
47
|
}
|
|
38
48
|
};
|
|
39
49
|
}
|
package/src/utils/url.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { BROWSER, DEV } from 'esm-env';
|
|
2
|
-
import { try_get_request_store } from '../exports/internal/event.js';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Matches a URI scheme. See https://www.rfc-editor.org/rfc/rfc3986#section-3.1
|
|
6
5
|
* @type {RegExp}
|
|
7
6
|
*/
|
|
8
7
|
export const SCHEME = /^[a-z][a-z\d+\-.]*:/i;
|
|
9
|
-
// See https://datatracker.ietf.org/doc/html/rfc2606 - no domains under the .invalid TLD can be registered
|
|
10
|
-
const REDIRECT_BASE = 'https://sveltekit-redirect.invalid';
|
|
11
8
|
|
|
12
9
|
const internal = new URL('a://');
|
|
13
10
|
|
|
@@ -30,32 +27,6 @@ export function is_root_relative(path) {
|
|
|
30
27
|
return path[0] === '/' && path[1] !== '/';
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
/**
|
|
34
|
-
* Whether a redirect location is absolute, i.e. not a root-relative or path-relative URL,
|
|
35
|
-
* and not pointing to the same origin as we're currently on (if determineable).
|
|
36
|
-
* @param {string} location
|
|
37
|
-
*/
|
|
38
|
-
export function is_external_location(location) {
|
|
39
|
-
const origin = BROWSER ? window.location.origin : try_get_request_store()?.event.url.origin;
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
return !matches_external_allowlist_entry(location, origin ?? REDIRECT_BASE);
|
|
43
|
-
} catch {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* @param {string} location
|
|
50
|
-
*/
|
|
51
|
-
function is_javascript_location(location) {
|
|
52
|
-
try {
|
|
53
|
-
return new URL(location, REDIRECT_BASE).protocol === 'javascript:';
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
30
|
/**
|
|
60
31
|
* @param {string} location
|
|
61
32
|
* @param {string} allowed
|
|
@@ -74,56 +45,6 @@ export function matches_external_allowlist_entry(location, allowed) {
|
|
|
74
45
|
}
|
|
75
46
|
}
|
|
76
47
|
|
|
77
|
-
/**
|
|
78
|
-
* @param {string} location
|
|
79
|
-
* @param {{ external?: boolean | string[] }} [options]
|
|
80
|
-
*/
|
|
81
|
-
export function validate_redirect_location(location, options) {
|
|
82
|
-
if (!is_external_location(location)) return;
|
|
83
|
-
|
|
84
|
-
const external = options?.external;
|
|
85
|
-
|
|
86
|
-
if (!external) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
DEV
|
|
89
|
-
? `Cannot redirect to external URL ${JSON.stringify(location)}. ` +
|
|
90
|
-
'To redirect to an external URL, pass `{ external: true }` or an allowlist of permitted origins as the third argument to `redirect`'
|
|
91
|
-
: 'Cannot redirect to external URL unless explicitly allowed'
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (external === true) {
|
|
96
|
-
if (is_javascript_location(location)) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
DEV
|
|
99
|
-
? `Cannot redirect to ${JSON.stringify(location)} with \`{ external: true }\`. ` +
|
|
100
|
-
'The `:javascript` protocol must be explicitly listed in the `external` allowlist'
|
|
101
|
-
: 'Cannot redirect to external URL unless explicitly allowed'
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (Array.isArray(external)) {
|
|
109
|
-
if (!external.some((allowed) => matches_external_allowlist_entry(location, allowed))) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
DEV
|
|
112
|
-
? `Cannot redirect to ${JSON.stringify(location)}: URL origin is not included in the \`external\` allowlist`
|
|
113
|
-
: 'Cannot redirect to external URL unless explicitly allowed'
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
throw new Error(
|
|
121
|
-
DEV
|
|
122
|
-
? '`redirect` options.external must be `true` or an array of allowed origins'
|
|
123
|
-
: 'Invalid redirect options.external value'
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
48
|
/**
|
|
128
49
|
* @param {string} path
|
|
129
50
|
* @param {import('types').TrailingSlash} trailing_slash
|
package/src/version.js
CHANGED