Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-cows-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Generate types for each page/endpoint
26 changes: 21 additions & 5 deletions documentation/docs/01-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,15 @@ declare module '$lib/database' {
export const get: (id: string) => Promise<Item>;
}

// @filename: [id].d.ts
import type { RequestHandler as GenericRequestHandler } from '@sveltejs/kit';
export type RequestHandler<Body = any> = GenericRequestHandler<{ id: string }, Body>;

// @filename: index.js
// ---cut---
import db from '$lib/database';

/** @type {import('@sveltejs/kit').RequestHandler} */
/** @type {import('./[id]').RequestHandler} */
export async function get({ params }) {
// `params.id` comes from [id].js
const item = await db.get(params.id);
Expand All @@ -74,11 +78,13 @@ export async function get({ params }) {
return {
status: 404
};
};
}
```

> All server-side code, including endpoints, has access to `fetch` in case you need to request data from external APIs. Don't worry about the `$lib` import, we'll get to that [later](/docs/modules#$lib).

The type of the `get` function above comes from `./[id].d.ts`, which is a file generated by SvelteKit (in a hidden directory, using the [`rootDirs`](https://www.typescriptlang.org/tsconfig#rootDirs) option) that provides type safety when accessing `params`. See the section on [generated types](/docs/types#generated-types) for more detail.

The job of a [request handler](/docs/types#sveltejs-kit-requesthandler) is to return a `{ status, headers, body }` object representing the response, where `status` is an [HTTP status code](https://httpstatusdogs.com):

- `2xx` — successful response (default is `200`)
Expand Down Expand Up @@ -148,11 +154,15 @@ declare module '$lib/database' {
export const create: (request: Request) => Promise<[Record<string, ValidationError>, Item]>;
}

// @filename: items.d.ts
import type { RequestHandler as GenericRequestHandler } from '@sveltejs/kit';
export type RequestHandler<Body = any> = GenericRequestHandler<{}, Body>;

// @filename: index.js
// ---cut---
import * as db from '$lib/database';

/** @type {import('@sveltejs/kit').RequestHandler} */
/** @type {import('./items').RequestHandler} */
export async function get() {
const items = await db.list();

Expand All @@ -161,7 +171,7 @@ export async function get() {
};
}

/** @type {import('@sveltejs/kit').RequestHandler} */
/** @type {import('./items').RequestHandler} */
export async function post({ request }) {
const [errors, item] = await db.create(request);

Expand Down Expand Up @@ -358,9 +368,15 @@ Higher priority routes can _fall through_ to lower priority routes by returning

```js
/// file: src/routes/[a].js

// @filename: [a].d.ts
import type { RequestHandler as GenericRequestHandler } from '@sveltejs/kit';
export type RequestHandler<Body = any> = GenericRequestHandler<{ a: string }, Body>;

// @filename: index.js
// @errors: 2366
/** @type {import('@sveltejs/kit').RequestHandler} */
// ---cut---
/** @type {import('./[a]').RequestHandler} */
export function get({ params }) {
if (params.a === 'foo-def') {
return { fallthrough: true };
Expand Down
4 changes: 3 additions & 1 deletion documentation/docs/03-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ If the data for a page comes from its endpoint, you may not need a `load` functi
```html
/// file: src/routes/blog/[slug].svelte
<script context="module">
/** @type {import('@sveltejs/kit').Load} */
/** @type {import('./[slug]').Load} */
export async function load({ params, fetch, session, stuff }) {
const url = `https://cms.example.com/article/${params.slug}.json`;
const response = await fetch(url);
Expand All @@ -26,6 +26,8 @@ If the data for a page comes from its endpoint, you may not need a `load` functi

> Note the `<script context="module">` — this is necessary because `load` runs before the component is rendered. Code that is per-component instance should go into a second `<script>` tag.

As with [endpoints](/docs/routing#endpoints), pages can import [generated types](/docs/types#generated) — the `./[slug]` in the example above — to ensure that `params` are correctly typed.

`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that `load` runs on both the server and the client. In the example above, if a user clicks on a link to this page the data will be fetched from `cms.example.com` without going via our server.

If `load` returns `{fallthrough: true}`, SvelteKit will [fall through](/docs/routing#advanced-routing-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.
Expand Down
71 changes: 71 additions & 0 deletions documentation/docs/14-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,74 @@ title: Types
---

**TYPES**

### Generated types

The [`RequestHandler`](#sveltejs-kit-requesthandler) and [`Load`](#sveltejs-kit-load) types both accept a `Params` argument allowing you to type the `params` object. For example this endpoint expects `foo`, `bar` and `baz` params:

```js
/// file: src/routes/[foo]/[bar]/[baz].js
// @errors: 2355
/** @type {import('@sveltejs/kit').RequestHandler<{
* foo: string;
* bar: string;
* baz: string
* }>} */
export async function get({ params }) {
// ...
}
```

Needless to say, this is cumbersome to write out, and less portable (if you were to rename the `[foo]` directory to `[qux]`, the type would no longer reflect reality).

To solve this problem, SvelteKit generates `.d.ts` files for each of your endpoints and pages:

```ts
/// file: .svelte-kit/types/src/routes/[foo]/[bar]/[baz].d.ts
/// link: false
import type { RequestHandler as GenericRequestHandler, Load as GenericLoad } from '@sveltejs/kit';

export type RequestHandler<Body = any> = GenericRequestHandler<
{ foo: string; bar: string; baz: string },
Body
>;

export type Load<Props = Record<string, any>> = GenericLoad<
{ foo: string; bar: string; baz: string },
Props
>;
```

These files can be imported into your endpoints and pages as siblings, thanks to the [`rootDirs`](https://www.typescriptlang.org/tsconfig#rootDirs) option in your TypeScript configuration:

```js
/// file: src/routes/[foo]/[bar]/[baz].js
// @filename: [baz].d.ts
import type { RequestHandler as GenericRequestHandler, Load as GenericLoad } from '@sveltejs/kit';

export type RequestHandler<Body = any> = GenericRequestHandler<
{ foo: string, bar: string, baz: string },
Body
>;

// @filename: index.js
// @errors: 2355
// ---cut---
/** @type {import('./[baz]').RequestHandler} */
export async function get({ params }) {
// ...
}
```

```svelte
<script context="module">
/** @type {import('./[baz]').Load} */
export async function load({ params, fetch, session, stuff }) {
// ...
}
</script>
```

> For this to work, your own `tsconfig.json` or `jsconfig.json` should extend from the generated `.svelte-kit/tsconfig.json`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I guess we are mentioning .svelte-kit here. I dunno. It is officially part of the API now that we want people to extend the config in it. I'm not sure whether that changes my opinion about mentioning it up higher instead of calling it a hidden folder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also add a note that that folder is not available immediately, so you'll need to run a command first (we also need to restart the language-server for it to resolve the extended tsconfig again, adding it to this list https://github.com/sveltejs/language-tools/blob/master/packages/svelte-vscode/src/extension.ts#L144).
What if we add a postinstall hook to the package.json of the create-svelte templates to immediately generate the types?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do postinstall hooks run after you run npm install on the repo whose package.json they're in, or do they run when you've just installed a package whose package.json has one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked, they do. Will add a svelte-kit init command and a "postinstall": "svelte-kit init" run script

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually i'll do that in a separate PR, because there might be some bikeshedding involved

>
> { "extends": ".svelte-kit/tsconfig.json" }
29 changes: 1 addition & 28 deletions packages/create-svelte/templates/default/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,3 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020"],
"target": "es2020",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
"extends": "./.svelte-kit/tsconfig.json"
}
10 changes: 9 additions & 1 deletion packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@
"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",
"check": "tsc && npm run check:integration && npm run check:prerendering",
"check:integration": "npm run check:integration:amp && npm run check:integration:basics && npm run check:integration:options && npm run check:integration:options-2",
"check:integration:amp": "cd test/apps/amp && pnpm check",
"check:integration:basics": "cd test/apps/basics && pnpm check",
"check:integration:options": "cd test/apps/options && pnpm check",
"check:integration:options-2": "cd test/apps/options-2 && pnpm check",
"check:prerendering": "npm run check:prerendering:basics && npm run check:prerendering:options",
"check:prerendering:basics": "cd test/prerendering/basics && pnpm check",
"check:prerendering:options": "cd test/prerendering/options && pnpm check",
"format": "npm run check-format -- --write",
"check-format": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore",
"prepublishOnly": "npm run build",
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/core/build/build_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export async function build_client({
process.env.VITE_SVELTEKIT_APP_VERSION_POLL_INTERVAL = `${config.kit.version.pollInterval}`;

create_app({
config,
manifest_data,
output: `${SVELTE_KIT}/generated`,
cwd
});

Expand Down
77 changes: 75 additions & 2 deletions packages/kit/src/core/create_app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import { s } from '../../utils/misc.js';
import { mkdirp } from '../../utils/filesystem.js';
import { SVELTE_KIT } from '../constants.js';

/** @type {Map<string, string>} */
const previous_contents = new Map();
Expand All @@ -22,16 +23,19 @@ export function write_if_changed(file, code) {

/**
* @param {{
* config: import('types').ValidatedConfig;
* manifest_data: ManifestData;
* output: string;
* cwd: string;
* }} options
*/
export function create_app({ manifest_data, output, cwd = process.cwd() }) {
export function create_app({ config, manifest_data, cwd = process.cwd() }) {
const output = `${SVELTE_KIT}/generated`;
const base = path.relative(cwd, output);

write_if_changed(`${output}/manifest.js`, generate_client_manifest(manifest_data, base));
write_if_changed(`${output}/root.svelte`, generate_app(manifest_data));

create_types(config, manifest_data);
}

/**
Expand Down Expand Up @@ -189,3 +193,72 @@ function generate_app(manifest_data) {
{/if}
`);
}

/**
* @param {import('types').ValidatedConfig} config
* @param {ManifestData} manifest_data
*/
function create_types(config, manifest_data) {
/** @type {Map<string, { params: string[], type: 'page' | 'endpoint' | 'both' }>} */
const shadow_types = new Map();

/** @param {string} key */
function extract_params(key) {
/** @type {string[]} */
const params = [];

const pattern = /\[([^\]]+)\]/g;
let match;

while ((match = pattern.exec(key))) {
params.push(match[1]);
}

return params;
}

manifest_data.routes.forEach((route) => {
if (route.type === 'endpoint') {
const key = route.file.slice(0, -path.extname(route.file).length);
shadow_types.set(key, { params: extract_params(key), type: 'endpoint' });
} else if (route.shadow) {
const key = route.shadow.slice(0, -path.extname(route.shadow).length);
shadow_types.set(key, { params: extract_params(key), type: 'both' });
}
});

manifest_data.components.forEach((component) => {
if (component.startsWith('.')) return; // exclude fallback components

const ext = /** @type {string} */ (config.extensions.find((ext) => component.endsWith(ext)));
const key = component.slice(0, -ext.length);

if (!shadow_types.has(key)) {
shadow_types.set(key, { params: extract_params(key), type: 'page' });
}
});

shadow_types.forEach(({ params, type }, key) => {
const arg = `{ ${params.map((param) => `${param}: string`).join('; ')} }`;

const imports = [
type !== 'page' && 'RequestHandler as GenericRequestHandler',
type !== 'endpoint' && 'Load as GenericLoad'
]
.filter(Boolean)
.join(', ');

const file = `${SVELTE_KIT}/types/${key || 'index'}.d.ts`;
const content = [
'// this file is auto-generated',
`import type { ${imports} } from '@sveltejs/kit';`,
type !== 'page' && `export type RequestHandler = GenericRequestHandler<${arg}>;`,
type !== 'endpoint' &&
`export type Load<Props = Record<string, any>> = GenericLoad<${arg}, Props>;`
]
.filter(Boolean)
.join('\n');

write_if_changed(file, content);
});
}
3 changes: 1 addition & 2 deletions packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function create_plugin(config, cwd) {
function update_manifest() {
const manifest_data = create_manifest_data({ config, cwd });

create_app({ manifest_data, output: `${SVELTE_KIT}/generated`, cwd });
create_app({ config, manifest_data, cwd });

manifest = {
appDir: config.kit.appDir,
Expand Down Expand Up @@ -200,7 +200,6 @@ export async function create_plugin(config, cwd) {

/** @type {import('types').Hooks} */
const hooks = {
// @ts-expect-error this picks up types that belong to the tests
getSession: user_hooks.getSession || (() => ({})),
handle: amp ? sequence(amp, handle) : handle,
handleError:
Expand Down
Loading