Skip to content

Commit

Permalink
Document types (#4011)
Browse files Browse the repository at this point in the history
* simplify types

* WIP document types

* example jsdoc comment

* extract types during build

* fix link

* silence typescript

* run snippets through prettier

* various style tweaks

* small refactor

* prevent self-linking

* add types to sidebar

* include types in search index

* tweak docs

* only show sub-sub-sections when on the page in question

* fix header margins

* remove unused properties

* fix diffs while we're in here

* links inside tooltips

* remove jsdoc comments, for now

* handle long comments

* fix some tooltip UX

* remove transition - apparently disrupts navigation. TODO investigate
  • Loading branch information
Rich-Harris committed Feb 21, 2022
1 parent 667a565 commit df6f126
Show file tree
Hide file tree
Showing 17 changed files with 416 additions and 233 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ test-results/
package-lock.json
yarn.lock
/packages/create-svelte/template/CHANGELOG.md
/documentation/types.js
.env
.vercel_build_output
.svelte-kit
Expand Down
54 changes: 3 additions & 51 deletions documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,15 @@ title: Loading

A component that defines a page or a layout can export a `load` function that runs before the component is created. This function runs both during server-side rendering and in the client, and allows you to fetch and manipulate data before the page is rendered, thus preventing loading spinners.

If the data for a page comes from its endpoint, you may not need a `load` function. It's useful when you need more flexibility, for example loading data from an external API.

```ts
// @filename: ambient.d.ts
declare namespace App {
interface Locals {}
interface Platform {}
interface Session {}
interface Stuff {}
}

type Either<T, U> = Only<T, U> | Only<U, T>;

// @filename: index.ts
// ---cut---
// Type declarations for `load` (declarations marked with
// an `export` keyword can be imported from `@sveltejs/kit`)

export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
}

export interface LoadInput<Params = Record<string, string>> {
url: URL;
params: Params;
props: Record<string, any>;
fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
session: App.Session;
stuff: Partial<App.Stuff>;
}

export interface LoadOutput<Props = Record<string, any>> {
status?: number;
error?: string | Error;
redirect?: string;
props?: Props;
stuff?: Partial<App.Stuff>;
maxage?: number;
}

type MaybePromise<T> = T | Promise<T>;

interface Fallthrough {
fallthrough: true;
}
```

> See the [TypeScript](/docs/typescript) section for information on `App.Session` and `App.Stuff`.
A page that loads data from an external API might look like this:
If the data for a page comes from its endpoint, you may not need a `load` function. It's useful when you need more flexibility, for example loading data from an external API, which might look like this:

```html
/// file: src/routes/blog/[slug].svelte
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ params, fetch, session, stuff }) {
const response = await fetch(`https://cms.example.com/article/${params.slug}.json`);
const url = `https://cms.example.com/article/${params.slug}.json`;
const response = await fetch(url);
return {
status: response.status,
Expand Down
2 changes: 1 addition & 1 deletion documentation/docs/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Hooks
---

An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exports four functions, all optional, that run on the server — **handle**, **handleError**, **getSession**, and **externalFetch**.
An optional `src/hooks.js` (or `src/hooks.ts`, or `src/hooks/index.js`) file exports four functions, all optional, that run on the server — `handle`, `handleError`, `getSession`, and `externalFetch`.

> The location of this file can be [configured](/docs/configuration) as `config.kit.files.hooks`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
---
title: TypeScript
title: Types
---

All APIs in SvelteKit are fully typed. Additionally, it's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following:
### @sveltejs/kit

All APIs in SvelteKit are fully typed. The following types can be imported from `@sveltejs/kit`:

**TYPES**

### The `App` namespace

It's possible to tell SvelteKit how to type objects inside your app by declaring the `App` namespace. By default, a new project will have a file called `src/app.d.ts` containing the following:

```ts
/// <reference types="@sveltejs/kit" />
Expand All @@ -20,18 +28,18 @@ declare namespace App {

By populating these interfaces, you will gain type safety when using `event.locals`, `event.platform`, `session` and `stuff`:

### App.Locals
#### App.Locals

The interface that defines `event.locals`, which can be accessed in [hooks](/docs/hooks) (`handle`, `handleError` and `getSession`) and [endpoints](/docs/routing#endpoints).

### App.Platform
#### App.Platform

If your adapter provides [platform-specific context](/docs/adapters#supported-environments-platform-specific-context) via `event.platform`, you can specify it here.

### App.Session
#### App.Session

The interface that defines `session`, both as an argument to [`load`](/docs/loading) functions and the value of the [session store](/docs/modules#$app-stores).

### App.Stuff
#### App.Stuff

The interface that defines `stuff`, as input or output to [`load`](/docs/loading) or as the value of the `stuff` property of the [page store](/docs/modules#$app-stores).
5 changes: 3 additions & 2 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"svelte-kit.js"
],
"scripts": {
"build": "rollup -c && node scripts/cp.js src/runtime/components assets/components",
"build": "rollup -c && node scripts/cp.js src/runtime/components assets/components && npm run types",
"dev": "rollup -cw",
"lint": "eslint --ignore-path .gitignore --ignore-pattern \"src/packaging/test/**\" \"{src,test}/**/*.{ts,mjs,js,svelte}\" && npm run check-format",
"check": "tsc && svelte-check --ignore test/prerendering,src/packaging/test",
Expand All @@ -74,7 +74,8 @@
"test:integration:amp": "cd test/apps/amp && pnpm test",
"test:integration:basics": "cd test/apps/basics && pnpm test",
"test:integration:options": "cd test/apps/options && pnpm test",
"test:integration:options-2": "cd test/apps/options-2 && pnpm test"
"test:integration:options-2": "cd test/apps/options-2 && pnpm test",
"types": "node scripts/extract-types.js"
},
"exports": {
"./package.json": "./package.json",
Expand Down
56 changes: 56 additions & 0 deletions packages/kit/scripts/extract-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import fs from 'fs';
import ts from 'typescript';
import prettier from 'prettier';

const code = fs.readFileSync('types/index.d.ts', 'utf-8');
const node = ts.createSourceFile('index.d.ts', code, ts.ScriptTarget.Latest);

const types = [];

for (const statement of node.statements) {
if (
ts.isClassDeclaration(statement) ||
ts.isInterfaceDeclaration(statement) ||
ts.isTypeAliasDeclaration(statement) ||
ts.isModuleDeclaration(statement)
) {
// @ts-ignore no idea why it's complaining here
const name = statement.name?.escapedText;

let start = statement.pos;
let comment = '';

// @ts-ignore i think typescript is bad at typescript
if (statement.jsDoc) {
// @ts-ignore
comment = statement.jsDoc[0].comment;
// @ts-ignore
start = statement.jsDoc[0].end;
}

const i = code.indexOf('export', start);
start = i + 6;

const snippet = prettier.format(code.slice(start, statement.end).trim(), {
parser: 'typescript',
printWidth: 60,
useTabs: true,
singleQuote: true,
trailingComma: 'none'
});

types.push({ name, comment, snippet });
}
}

// should already be sorted, but just in case
types.sort((a, b) => (a.name < b.name ? -1 : 1));

fs.writeFileSync(
'../../documentation/types.js',
`
/* This file is generated by running \`node scripts/extract-types.js\`
in the packages/kit directory — do not edit it */
export const types = ${JSON.stringify(types, null, ' ')};
`.trim()
);
2 changes: 1 addition & 1 deletion packages/kit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
"types": ["./types/internal"]
}
},
"include": ["src/**/*", "test/**/*", "types/**/*"],
"include": ["scripts/**/*", "src/**/*", "test/**/*", "types/**/*"],
"exclude": ["src/packaging/test/fixtures/**/*", "test/prerendering/*/build/**/*"]
}
87 changes: 39 additions & 48 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ export class App {

export interface Adapter {
name: string;
headers?: {
host?: string;
protocol?: string;
};
adapt(builder: Builder): Promise<void>;
}

Expand Down Expand Up @@ -150,55 +146,44 @@ export interface Config {
preprocess?: any;
}

/**
* Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts
*
* MIT License
*
* Copyright (c) 2021-present, Joshua Hemphill
* Copyright (c) 2021, Tecnico Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts
//
// MIT License
//
// Copyright (c) 2021-present, Joshua Hemphill
// Copyright (c) 2021, Tecnico Corporation
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

export namespace Csp {
type SchemeSource = 'http:' | 'https:' | 'data:' | 'mediastream:' | 'blob:' | 'filesystem:';

type HostProtocolSchemes = `${string}://` | '';
type PortScheme = `:${number}` | '' | ':*';
/** Can actually be any string, but typed more explicitly to
* restrict the combined optional types of Source from collapsing to just bing `string` */
type HostNameScheme = `${string}.${string}` | `localhost`;
type HostSource = `${HostProtocolSchemes}${HostNameScheme}${PortScheme}`;

type CryptoSource = `${'nonce' | 'sha256' | 'sha384' | 'sha512'}-${string}`;

type BaseSource = 'self' | 'unsafe-eval' | 'unsafe-hashes' | 'unsafe-inline' | 'none';

export type Source = HostSource | SchemeSource | CryptoSource | BaseSource;
type Sources = Source[];

type ActionSource = 'strict-dynamic' | 'report-sample';

type BaseSource = 'self' | 'unsafe-eval' | 'unsafe-hashes' | 'unsafe-inline' | 'none';
type CryptoSource = `${'nonce' | 'sha256' | 'sha384' | 'sha512'}-${string}`;
type FrameSource = HostSource | SchemeSource | 'self' | 'none';

type HostNameScheme = `${string}.${string}` | `localhost`;
type HostSource = `${HostProtocolSchemes}${HostNameScheme}${PortScheme}`;
type HostProtocolSchemes = `${string}://` | '';
type HttpDelineator = '/' | '?' | '#' | '\\';
type PortScheme = `:${number}` | '' | ':*';
type SchemeSource = 'http:' | 'https:' | 'data:' | 'mediastream:' | 'blob:' | 'filesystem:';
type Source = HostSource | SchemeSource | CryptoSource | BaseSource;
type Sources = Source[];
type UriPath = `${HttpDelineator}${string}`;
}

Expand Down Expand Up @@ -368,6 +353,12 @@ export interface RequestEvent {
platform: Readonly<App.Platform>;
}

/**
* A function exported from an endpoint that corresponds to an
* HTTP verb (get, put, patch, etc) and handles requests with
* that method. Note that since 'delete' is a reserved word in
* JavaScript, delete handles are called 'del' instead.
*/
export interface RequestHandler<Output extends Body = Body> {
(event: RequestEvent): MaybePromise<
Either<Output extends Response ? Response : EndpointOutput<Output>, Fallthrough>
Expand Down
30 changes: 16 additions & 14 deletions sites/kit.svelte.dev/src/lib/docs/Contents.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,22 @@
{subsection.title}
</a>

<ul>
{#each subsection.sections as subsection}
<li>
<a
class="nested subsection"
class:active={subsection.path === path}
href={subsection.path}
sveltekit:prefetch
>
{subsection.title}
</a>
</li>
{/each}
</ul>
{#if section.path === $page.url.pathname}
<ul>
{#each subsection.sections as subsection}
<li>
<a
class="nested subsection"
class:active={subsection.path === path}
href={subsection.path}
sveltekit:prefetch
>
{subsection.title}
</a>
</li>
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
Expand Down
Loading

0 comments on commit df6f126

Please sign in to comment.