Skip to content

Commit

Permalink
[chore] improved typing for runtime and tests (#1995)
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccann authored Jul 22, 2021
1 parent de12888 commit bce1d76
Show file tree
Hide file tree
Showing 31 changed files with 203 additions and 153 deletions.
5 changes: 5 additions & 0 deletions .changeset/itchy-days-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[chore] improved typing for runtime and tests
2 changes: 1 addition & 1 deletion packages/kit/src/core/adapter-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* This is intended to be used with both requests and responses, to have a consistent body parsing across adapters.
*
* @param {string?} content_type The `content-type` header of a request/response.
* @param {string|undefined|null} content_type The `content-type` header of a request/response.
* @returns {boolean}
*/
export function isContentTypeTextual(content_type) {
Expand Down
8 changes: 4 additions & 4 deletions packages/kit/src/core/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { get_server } from '../server/index.js';
import { __fetch_polyfill } from '../../install-fetch.js';
import { SVELTE_KIT } from '../constants.js';

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

/** @param {Options} opts */
Expand Down Expand Up @@ -47,9 +47,8 @@ class Watcher extends EventEmitter {
this.config = config;

/**
* @type {vite.ViteDevServer}
* @type {vite.ViteDevServer | undefined}
*/
// @ts-ignore
this.vite;

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

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

/** @type {import('types/internal').Hooks} */
/** @type {Partial<import('types/internal').Hooks>} */
const hooks = resolve_entry(config.kit.files.hooks)
? await vite.ssrLoadModule(`/${config.kit.files.hooks}`)
: {};
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/start/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const mutable = (dir) =>
/**
* @param {{
* port: number;
* host: string;
* host?: string;
* config: import('types/config').ValidatedConfig;
* https?: boolean;
* cwd?: string;
Expand Down
16 changes: 12 additions & 4 deletions packages/kit/src/runtime/app/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const prefetchRoutes = import.meta.env.SSR ? guard('prefetchRoutes') : pr
* @type {import('$app/navigation').goto}
*/
async function goto_(href, opts) {
// @ts-ignore
return router.goto(href, opts, []);
}

Expand All @@ -27,13 +28,15 @@ async function goto_(href, opts) {
*/
async function invalidate_(resource) {
const { href } = new URL(resource, location.href);
// @ts-ignore
return router.renderer.invalidate(href);
}

/**
* @type {import('$app/navigation').prefetch}
*/
function prefetch_(href) {
// @ts-ignore
return router.prefetch(new URL(href, get_base_uri(document)));
}

Expand All @@ -42,10 +45,15 @@ function prefetch_(href) {
*/
async function prefetchRoutes_(pathnames) {
const matching = pathnames
? router.routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname)))
: router.routes;

const promises = matching.map((r) => r.length !== 1 && Promise.all(r[1].map((load) => load())));
? // @ts-ignore
router.routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname)))
: // @ts-ignore
router.routes;

const promises = matching
.filter((r) => r && r.length > 1)
// @ts-ignore
.map((r) => Promise.all(r[1].map((load) => load())));

await Promise.all(promises);
}
21 changes: 11 additions & 10 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function initial_fetch(resource, opts) {
}

const script = document.querySelector(selector);
if (script) {
if (script && script.textContent) {
const { body, ...init } = JSON.parse(script.textContent);
return Promise.resolve(new Response(body, init));
}
Expand All @@ -69,8 +69,8 @@ export class Renderer {
this.fallback = fallback;
this.host = host;

/** @type {import('./router').Router} */
this.router = null;
/** @type {import('./router').Router | undefined} */
this.router;

this.target = target;

Expand Down Expand Up @@ -133,13 +133,13 @@ export class Renderer {
/** @type {Record<string, any>} */
let context = {};

/** @type {import('./types').NavigationResult} */
/** @type {import('./types').NavigationResult | undefined} */
let result;

/** @type {number} */
/** @type {number | undefined} */
let new_status;

/** @type {Error} new_error */
/** @type {Error | undefined} new_error */
let new_error;

try {
Expand All @@ -150,8 +150,8 @@ export class Renderer {
module: await nodes[i],
page,
context,
status: is_leaf && status,
error: is_leaf && error
status: is_leaf ? status : undefined,
error: is_leaf ? error : undefined
});

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

if (!no_cache && this.cache.has(key)) {
return this.cache.get(key);
if (!no_cache) {
const cached = this.cache.get(key);
if (cached) return cached;
}

const [pattern, a, b, get_params] = route;
Expand Down
15 changes: 9 additions & 6 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ function scroll_state() {
}

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

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

/**
* @param {URL} url
* @returns {import('./types').NavigationInfo}
* @returns {import('./types').NavigationInfo | undefined}
*/
parse(url) {
if (this.owns(url)) {
Expand Down Expand Up @@ -221,12 +221,13 @@ export class Router {
throw new Error('Attempted to prefetch a URL that does not belong to this app');
}

// @ts-ignore
return this.renderer.load(info);
}

/**
* @param {URL} url
* @param {{ x: number, y: number }} scroll
* @param {{ x: number, y: number }?} scroll
* @param {boolean} keepfocus
* @param {string[]} chain
* @param {string} [hash]
Expand All @@ -246,19 +247,21 @@ export class Router {
(has_trailing_slash && this.trailing_slash === 'never') ||
(!has_trailing_slash &&
this.trailing_slash === 'always' &&
!info.path.split('/').pop().includes('.'));
!(info.path.split('/').pop() || '').includes('.'));

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

// @ts-ignore6
this.renderer.notify({
path: info.path,
query: info.query
});

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

if (!keepfocus) {
Expand Down
4 changes: 2 additions & 2 deletions packages/kit/src/runtime/client/singletons.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @type {import('./router').Router} */
/** @type {import('./router').Router?} */
export let router;

/** @type {string} */
Expand All @@ -7,7 +7,7 @@ export let base = '';
/** @type {string} */
export let assets = '/.';

/** @param {import('./router').Router} _ */
/** @param {import('./router').Router?} _ */
export function init(_) {
router = _;
}
Expand Down
21 changes: 11 additions & 10 deletions packages/kit/src/runtime/client/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ export async function start({ paths, target, session, host, route, spa, trailing
throw new Error('Missing target element. See https://kit.svelte.dev/docs#configuration-target');
}

const router =
route &&
new Router({
base: paths.base,
routes,
trailing_slash
});
const router = route
? new Router({
base: paths.base,
routes,
trailing_slash
})
: null;

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

if (hydrate) await renderer.start(hydrate);
if (route) router.init(renderer);

if (spa) router.goto(location.href, { replaceState: true }, []);
if (router) {
router.init(renderer);
if (spa) router.goto(location.href, { replaceState: true }, []);
}

dispatchEvent(new CustomEvent('sveltekit:start'));
}
91 changes: 49 additions & 42 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,54 @@ export default async function render_route(request, route) {
/** @type {import('types/endpoint').RequestHandler} */
const handler = mod[request.method.toLowerCase().replace('delete', 'del')]; // 'delete' is a reserved word

if (handler) {
const match = route.pattern.exec(request.path);
const params = route.params(match);

const response = await handler({ ...request, params });
const preface = `Invalid response from route ${request.path}`;

if (response) {
if (typeof response !== 'object') {
return error(`${preface}: expected an object, got ${typeof response}`);
}

let { status = 200, body, headers = {} } = response;

headers = lowercase_keys(headers);
const type = headers['content-type'];

const is_type_textual = isContentTypeTextual(type);

if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) {
return error(
`${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type`
);
}

/** @type {import('types/hooks').StrictBody} */
let normalized_body;

// ensure the body is an object
if (
(typeof body === 'object' || typeof body === 'undefined') &&
!(body instanceof Uint8Array) &&
(!type || type.startsWith('application/json'))
) {
headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };
normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body);
} else {
normalized_body = /** @type {import('types/hooks').StrictBody} */ (body);
}

return { status, body: normalized_body, headers };
}
if (!handler) {
return error('no handler');
}

const match = route.pattern.exec(request.path);
if (!match) {
return error('could not parse parameters from request path');
}

const params = route.params(match);

const response = await handler({ ...request, params });
const preface = `Invalid response from route ${request.path}`;

if (!response) {
return error('no response');
}
if (typeof response !== 'object') {
return error(`${preface}: expected an object, got ${typeof response}`);
}

let { status = 200, body, headers = {} } = response;

headers = lowercase_keys(headers);
const type = headers['content-type'];

const is_type_textual = isContentTypeTextual(type);

if (!is_type_textual && !(body instanceof Uint8Array || is_string(body))) {
return error(
`${preface}: body must be an instance of string or Uint8Array if content-type is not a supported textual content-type`
);
}

/** @type {import('types/hooks').StrictBody} */
let normalized_body;

// ensure the body is an object
if (
(typeof body === 'object' || typeof body === 'undefined') &&
!(body instanceof Uint8Array) &&
(!type || type.startsWith('application/json'))
) {
headers = { ...headers, 'content-type': 'application/json; charset=utf-8' };
normalized_body = JSON.stringify(typeof body === 'undefined' ? {} : body);
} else {
normalized_body = /** @type {import('types/hooks').StrictBody} */ (body);
}

return { status, body: normalized_body, headers };
}
Loading

0 comments on commit bce1d76

Please sign in to comment.