Skip to content

Commit bce1d76

Browse files
authored
[chore] improved typing for runtime and tests (#1995)
1 parent de12888 commit bce1d76

File tree

31 files changed

+203
-153
lines changed

31 files changed

+203
-153
lines changed

.changeset/itchy-days-wonder.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[chore] improved typing for runtime and tests

packages/kit/src/core/adapter-utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* This is intended to be used with both requests and responses, to have a consistent body parsing across adapters.
55
*
6-
* @param {string?} content_type The `content-type` header of a request/response.
6+
* @param {string|undefined|null} content_type The `content-type` header of a request/response.
77
* @returns {boolean}
88
*/
99
export function isContentTypeTextual(content_type) {

packages/kit/src/core/dev/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { get_server } from '../server/index.js';
1818
import { __fetch_polyfill } from '../../install-fetch.js';
1919
import { SVELTE_KIT } from '../constants.js';
2020

21-
/** @typedef {{ cwd?: string, port: number, host: string, https: boolean, config: import('types/config').ValidatedConfig }} Options */
21+
/** @typedef {{ cwd?: string, port: number, host?: string, https: boolean, config: import('types/config').ValidatedConfig }} Options */
2222
/** @typedef {import('types/internal').SSRComponent} SSRComponent */
2323

2424
/** @param {Options} opts */
@@ -47,9 +47,8 @@ class Watcher extends EventEmitter {
4747
this.config = config;
4848

4949
/**
50-
* @type {vite.ViteDevServer}
50+
* @type {vite.ViteDevServer | undefined}
5151
*/
52-
// @ts-ignore
5352
this.vite;
5453

5554
process.on('exit', () => {
@@ -198,6 +197,7 @@ class Watcher extends EventEmitter {
198197
pattern: route.pattern,
199198
params: get_params(route.params),
200199
load: async () => {
200+
if (!this.vite) throw new Error('Vite server has not been initialized');
201201
const url = path.resolve(this.cwd, route.file);
202202
return await this.vite.ssrLoadModule(url);
203203
}
@@ -281,7 +281,7 @@ async function create_handler(vite, config, dir, cwd, manifest) {
281281

282282
if (req.url === '/favicon.ico') return;
283283

284-
/** @type {import('types/internal').Hooks} */
284+
/** @type {Partial<import('types/internal').Hooks>} */
285285
const hooks = resolve_entry(config.kit.files.hooks)
286286
? await vite.ssrLoadModule(`/${config.kit.files.hooks}`)
287287
: {};

packages/kit/src/core/start/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const mutable = (dir) =>
1717
/**
1818
* @param {{
1919
* port: number;
20-
* host: string;
20+
* host?: string;
2121
* config: import('types/config').ValidatedConfig;
2222
* https?: boolean;
2323
* cwd?: string;

packages/kit/src/runtime/app/navigation.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const prefetchRoutes = import.meta.env.SSR ? guard('prefetchRoutes') : pr
1919
* @type {import('$app/navigation').goto}
2020
*/
2121
async function goto_(href, opts) {
22+
// @ts-ignore
2223
return router.goto(href, opts, []);
2324
}
2425

@@ -27,13 +28,15 @@ async function goto_(href, opts) {
2728
*/
2829
async function invalidate_(resource) {
2930
const { href } = new URL(resource, location.href);
31+
// @ts-ignore
3032
return router.renderer.invalidate(href);
3133
}
3234

3335
/**
3436
* @type {import('$app/navigation').prefetch}
3537
*/
3638
function prefetch_(href) {
39+
// @ts-ignore
3740
return router.prefetch(new URL(href, get_base_uri(document)));
3841
}
3942

@@ -42,10 +45,15 @@ function prefetch_(href) {
4245
*/
4346
async function prefetchRoutes_(pathnames) {
4447
const matching = pathnames
45-
? router.routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname)))
46-
: router.routes;
47-
48-
const promises = matching.map((r) => r.length !== 1 && Promise.all(r[1].map((load) => load())));
48+
? // @ts-ignore
49+
router.routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname)))
50+
: // @ts-ignore
51+
router.routes;
52+
53+
const promises = matching
54+
.filter((r) => r && r.length > 1)
55+
// @ts-ignore
56+
.map((r) => Promise.all(r[1].map((load) => load())));
4957

5058
await Promise.all(promises);
5159
}

packages/kit/src/runtime/client/renderer.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function initial_fetch(resource, opts) {
4646
}
4747

4848
const script = document.querySelector(selector);
49-
if (script) {
49+
if (script && script.textContent) {
5050
const { body, ...init } = JSON.parse(script.textContent);
5151
return Promise.resolve(new Response(body, init));
5252
}
@@ -69,8 +69,8 @@ export class Renderer {
6969
this.fallback = fallback;
7070
this.host = host;
7171

72-
/** @type {import('./router').Router} */
73-
this.router = null;
72+
/** @type {import('./router').Router | undefined} */
73+
this.router;
7474

7575
this.target = target;
7676

@@ -133,13 +133,13 @@ export class Renderer {
133133
/** @type {Record<string, any>} */
134134
let context = {};
135135

136-
/** @type {import('./types').NavigationResult} */
136+
/** @type {import('./types').NavigationResult | undefined} */
137137
let result;
138138

139-
/** @type {number} */
139+
/** @type {number | undefined} */
140140
let new_status;
141141

142-
/** @type {Error} new_error */
142+
/** @type {Error | undefined} new_error */
143143
let new_error;
144144

145145
try {
@@ -150,8 +150,8 @@ export class Renderer {
150150
module: await nodes[i],
151151
page,
152152
context,
153-
status: is_leaf && status,
154-
error: is_leaf && error
153+
status: is_leaf ? status : undefined,
154+
error: is_leaf ? error : undefined
155155
});
156156

157157
branch.push(node);
@@ -535,8 +535,9 @@ export class Renderer {
535535
async _load({ route, path, query }, no_cache) {
536536
const key = `${path}?${query}`;
537537

538-
if (!no_cache && this.cache.has(key)) {
539-
return this.cache.get(key);
538+
if (!no_cache) {
539+
const cached = this.cache.get(key);
540+
if (cached) return cached;
540541
}
541542

542543
const [pattern, a, b, get_params] = route;

packages/kit/src/runtime/client/router.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ function scroll_state() {
88
}
99

1010
/**
11-
* @param {Node} node
12-
* @returns {HTMLAnchorElement | SVGAElement}
11+
* @param {Node | null} node
12+
* @returns {HTMLAnchorElement | SVGAElement | null}
1313
*/
1414
function find_anchor(node) {
1515
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
@@ -121,7 +121,7 @@ export class Router {
121121
// Ignore if tag has
122122
// 1. 'download' attribute
123123
// 2. 'rel' attribute includes external
124-
const rel = a.getAttribute('rel') && a.getAttribute('rel').split(/\s+/);
124+
const rel = (a.getAttribute('rel') || '').split(/\s+/);
125125

126126
if (a.hasAttribute('download') || (rel && rel.includes('external'))) {
127127
return;
@@ -162,7 +162,7 @@ export class Router {
162162

163163
/**
164164
* @param {URL} url
165-
* @returns {import('./types').NavigationInfo}
165+
* @returns {import('./types').NavigationInfo | undefined}
166166
*/
167167
parse(url) {
168168
if (this.owns(url)) {
@@ -221,12 +221,13 @@ export class Router {
221221
throw new Error('Attempted to prefetch a URL that does not belong to this app');
222222
}
223223

224+
// @ts-ignore
224225
return this.renderer.load(info);
225226
}
226227

227228
/**
228229
* @param {URL} url
229-
* @param {{ x: number, y: number }} scroll
230+
* @param {{ x: number, y: number }?} scroll
230231
* @param {boolean} keepfocus
231232
* @param {string[]} chain
232233
* @param {string} [hash]
@@ -246,19 +247,21 @@ export class Router {
246247
(has_trailing_slash && this.trailing_slash === 'never') ||
247248
(!has_trailing_slash &&
248249
this.trailing_slash === 'always' &&
249-
!info.path.split('/').pop().includes('.'));
250+
!(info.path.split('/').pop() || '').includes('.'));
250251

251252
if (incorrect) {
252253
info.path = has_trailing_slash ? info.path.slice(0, -1) : info.path + '/';
253254
history.replaceState({}, '', `${this.base}${info.path}${location.search}`);
254255
}
255256
}
256257

258+
// @ts-ignore6
257259
this.renderer.notify({
258260
path: info.path,
259261
query: info.query
260262
});
261263

264+
// @ts-ignore
262265
await this.renderer.update(info, chain, false);
263266

264267
if (!keepfocus) {

packages/kit/src/runtime/client/singletons.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @type {import('./router').Router} */
1+
/** @type {import('./router').Router?} */
22
export let router;
33

44
/** @type {string} */
@@ -7,7 +7,7 @@ export let base = '';
77
/** @type {string} */
88
export let assets = '/.';
99

10-
/** @param {import('./router').Router} _ */
10+
/** @param {import('./router').Router?} _ */
1111
export function init(_) {
1212
router = _;
1313
}

packages/kit/src/runtime/client/start.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ export async function start({ paths, target, session, host, route, spa, trailing
3030
throw new Error('Missing target element. See https://kit.svelte.dev/docs#configuration-target');
3131
}
3232

33-
const router =
34-
route &&
35-
new Router({
36-
base: paths.base,
37-
routes,
38-
trailing_slash
39-
});
33+
const router = route
34+
? new Router({
35+
base: paths.base,
36+
routes,
37+
trailing_slash
38+
})
39+
: null;
4040

4141
const renderer = new Renderer({
4242
Root,
@@ -50,9 +50,10 @@ export async function start({ paths, target, session, host, route, spa, trailing
5050
set_paths(paths);
5151

5252
if (hydrate) await renderer.start(hydrate);
53-
if (route) router.init(renderer);
54-
55-
if (spa) router.goto(location.href, { replaceState: true }, []);
53+
if (router) {
54+
router.init(renderer);
55+
if (spa) router.goto(location.href, { replaceState: true }, []);
56+
}
5657

5758
dispatchEvent(new CustomEvent('sveltekit:start'));
5859
}

packages/kit/src/runtime/server/endpoint.js

Lines changed: 49 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,47 +26,54 @@ export default async function render_route(request, route) {
2626
/** @type {import('types/endpoint').RequestHandler} */
2727
const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word
2828

29-
if (handler) {
30-
const match = route.pattern.exec(request.path);
31-
const params = route.params(match);
32-
33-
const response = await handler({ ...request, params });
34-
const preface = `Invalid response from route ${request.path}`;
35-
36-
if (response) {
37-
if (typeof response !== 'object') {
38-
return error(`${preface}: expected an object, got ${typeof response}`);
39-
}
40-
41-
let { status = 200, body, headers = {} } = response;
42-
43-
headers = lowercase_keys(headers);
44-
const type = headers['content-type'];
45-
46-
const is_type_textual = isContentTypeTextual(type);
47-
48-
if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) {
49-
return error(
50-
`${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type`
51-
);
52-
}
53-
54-
/** @type {import('types/hooks').StrictBody} */
55-
let normalized_body;
56-
57-
// ensure the body is an object
58-
if (
59-
(typeof body === 'object' || typeof body === 'undefined') &&
60-
!(body instanceof Uint8Array) &&
61-
(!type || type.startsWith('application/json'))
62-
) {
63-
headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };
64-
normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body);
65-
} else {
66-
normalized_body = /** @type {import('types/hooks').StrictBody} */ (body);
67-
}
68-
69-
return { status, body: normalized_body, headers };
70-
}
29+
if (!handler) {
30+
return error('no handler');
7131
}
32+
33+
const match = route.pattern.exec(request.path);
34+
if (!match) {
35+
return error('could not parse parameters from request path');
36+
}
37+
38+
const params = route.params(match);
39+
40+
const response = await handler({ ...request, params });
41+
const preface = `Invalid response from route ${request.path}`;
42+
43+
if (!response) {
44+
return error('no response');
45+
}
46+
if (typeof response !== 'object') {
47+
return error(`${preface}: expected an object, got ${typeof response}`);
48+
}
49+
50+
let { status = 200, body, headers = {} } = response;
51+
52+
headers = lowercase_keys(headers);
53+
const type = headers['content-type'];
54+
55+
const is_type_textual = isContentTypeTextual(type);
56+
57+
if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) {
58+
return error(
59+
`${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type`
60+
);
61+
}
62+
63+
/** @type {import('types/hooks').StrictBody} */
64+
let normalized_body;
65+
66+
// ensure the body is an object
67+
if (
68+
(typeof body === 'object' || typeof body === 'undefined') &&
69+
!(body instanceof Uint8Array) &&
70+
(!type || type.startsWith('application/json'))
71+
) {
72+
headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };
73+
normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body);
74+
} else {
75+
normalized_body = /** @type {import('types/hooks').StrictBody} */ (body);
76+
}
77+
78+
return { status, body: normalized_body, headers };
7279
}

0 commit comments

Comments
 (0)