diff --git a/runtime/index.d.ts b/runtime/index.d.ts index b342e3cba..00ceb31eb 100644 --- a/runtime/index.d.ts +++ b/runtime/index.d.ts @@ -1,3 +1,10 @@ +/* + * This file declares all Sapper types that are accessible to project code. + * It is created in src/node_modules/@sapper in projects during the build. + * It must not import any internal Sapper types as it will not be possible for + * project code to reference those. + */ + declare module '@sapper/app' { export interface Redirect { statusCode: number @@ -12,16 +19,64 @@ declare module '@sapper/app' { } declare module '@sapper/server' { - import { Handler, Req, Res } from '@sapper/internal/manifest-server'; + import { IncomingMessage, ServerResponse } from 'http'; + import { TLSSocket } from 'tls'; export type Ignore = string | RegExp | ((uri: string) => boolean) | Ignore[]; + /** + * The request object passed to middleware and server-side routes. + * These fields are common to both Polka and Express, but you are free to + * instead use the typings that come with the server you use. + */ + export interface SapperRequest extends IncomingMessage { + url: string; + method: string; + baseUrl: string; + + /** + * The originally requested URL, including parent router segments. + */ + originalUrl: string; + + /** + * The path portion of the requested URL. + */ + path: string; + + /** + * The values of named parameters within your route pattern + */ + params: Record; + + /** + * The un-parsed querystring + */ + search: string | null; + + /** + * The parsed querystring + */ + query: Record; + + socket: TLSSocket; + } + + export interface SapperResponse extends ServerResponse { + locals?: { + nonce?: string; + name?: string; + }; + } + export interface MiddlewareOptions { - session?: (req: Req, res: Res) => unknown - ignore?: Ignore + session?: (req: SapperRequest, res: SapperResponse) => unknown; + ignore?: Ignore; } - export function middleware(opts: MiddlewareOptions): Handler; + export function middleware( + opts?: MiddlewareOptions + ): (req: SapperRequest, res: SapperResponse, next: () => void) => void; } declare module '@sapper/service-worker' { @@ -39,15 +94,26 @@ declare module '@sapper/common' { redirect: (statusCode: number, location: string) => void; } - export interface Page { + export type PageParams = Record; + export type Query = Record; + + export interface PageContext { host: string; path: string; - params: Record; - query: Record; + params: PageParams; + query: Query; + /** `error` is only set when the error page is being rendered. */ error?: Error; } + /** + * @deprecated PageContext is the preferred name. Page might be removed in the future. + */ + export { PageContext as Page }; + + export type PreloadResult = object | Promise + export interface Preload { - (this: PreloadContext, page: Page, session: any): object | Promise; + (this: PreloadContext, page: PageContext, session: any): PreloadResult; } } diff --git a/runtime/src/app/app.ts b/runtime/src/app/app.ts index a85d14e68..44e7c183f 100644 --- a/runtime/src/app/app.ts +++ b/runtime/src/app/app.ts @@ -18,9 +18,9 @@ import { Redirect, Branch, Page, - PageContext, InitialData } from './types'; +import { PageContext } from '@sapper/common'; import goto from './goto'; import { page_store } from './stores'; diff --git a/runtime/src/app/router/index.ts b/runtime/src/app/router/index.ts index 0ec240679..8eff0de8d 100644 --- a/runtime/src/app/router/index.ts +++ b/runtime/src/app/router/index.ts @@ -6,8 +6,8 @@ import { ignore, routes } from '@sapper/internal/manifest-client'; -import { Query } from '@sapper/internal/shared'; import find_anchor from './find_anchor'; +import { Page, Query } from '@sapper/common'; export let uid = 1; export function set_uid(n: number) { @@ -68,7 +68,7 @@ export function init(base: string, handler: (dest: Target) => Promise): vo } export function extract_query(search: string) { - const query = Object.create(null); + const query: Query = Object.create(null); if (search.length > 0) { search.slice(1).split('&').forEach(searchParam => { const [, key, value = ''] = /([^=]*)(?:=(.*))?/.exec(decodeURIComponent(searchParam.replace(/\+/g, ' '))); @@ -99,11 +99,11 @@ export function select_target(url: URL): Target { const match = route.pattern.exec(path); if (match) { - const query: Query = extract_query(url.search); + const query = extract_query(url.search); const part = route.parts[route.parts.length - 1]; const params = part.params ? part.params(match) : {}; - const page = { host: location.host, path, query, params }; + const page: Page = { host: location.host, path, query, params }; return { href: url.href, route, match, page }; } diff --git a/runtime/src/app/types.ts b/runtime/src/app/types.ts index 0769708fd..1c97ea728 100644 --- a/runtime/src/app/types.ts +++ b/runtime/src/app/types.ts @@ -45,11 +45,3 @@ export interface Page { params: Record; query: Record; } - -/** - * Information about the current page in the `page` store. - */ -export interface PageContext extends Page { - /** `error` is only set when the error page is being rendered. */ - error?: Error; -} diff --git a/runtime/src/common.ts b/runtime/src/common.ts new file mode 100644 index 000000000..ae78a93ab --- /dev/null +++ b/runtime/src/common.ts @@ -0,0 +1,6 @@ +/* + * This file exists to avoid errors on references to @sapper/common from inside Sapper. + * (@sapper/common is a namespace defined in index.d.ts) + */ + + export default null; \ No newline at end of file diff --git a/runtime/src/internal/manifest-client.d.ts b/runtime/src/internal/manifest-client.d.ts index b33cc39a4..1b482decf 100644 --- a/runtime/src/internal/manifest-client.d.ts +++ b/runtime/src/internal/manifest-client.d.ts @@ -1,3 +1,4 @@ +import { PageParams } from '@sapper/common'; import { Preload } from './shared'; @@ -24,7 +25,7 @@ export interface Route { pattern: RegExp; parts: Array<{ i: number; - params?: (match: RegExpExecArray) => Record; + params?: (match: RegExpExecArray) => PageParams; }>; } diff --git a/runtime/src/internal/manifest-server.d.ts b/runtime/src/internal/manifest-server.d.ts index 3586ec7d1..fae257b8d 100644 --- a/runtime/src/internal/manifest-server.d.ts +++ b/runtime/src/internal/manifest-server.d.ts @@ -1,5 +1,3 @@ -import { ClientRequest, ServerResponse } from 'http'; -import { TLSSocket } from 'tls'; import { Preload } from './shared'; @@ -9,6 +7,8 @@ export const build_dir: string; export const dev: boolean; export const manifest: Manifest; +export { SapperRequest, SapperResponse } from '@sapper/server'; + export interface SSRComponentModule { default: SSRComponent; preload?: Preload; @@ -42,27 +42,7 @@ export interface ManifestPagePart { params?: (match: RegExpMatchArray | null) => Record; } -export type Handler = (req: Req, res: Res, next: () => void) => void; - -export interface Req extends ClientRequest { - url: string; - baseUrl: string; - originalUrl: string; - method: string; - path: string; - params: Record; - query: Record; - headers: Record; - socket: TLSSocket; -} - -export interface Res extends ServerResponse { - write: (data: any) => boolean; - locals?: { - nonce?: string; - name?: string; - }; -} +export type Handler = (req: SapperRequest, res: SapperResponse, next: () => void) => void; export interface ServerRoute { pattern: RegExp; diff --git a/runtime/src/internal/shared.d.ts b/runtime/src/internal/shared.d.ts index d7b21f11f..511d6c91c 100644 --- a/runtime/src/internal/shared.d.ts +++ b/runtime/src/internal/shared.d.ts @@ -1,8 +1 @@ export const CONTEXT_KEY: unknown; - -export type Params = Record; -export type Query = Record; -export type PreloadResult = object | Promise; -export interface Preload { - (this: PreloadContext, page: PreloadPage, session: any): PreloadResult; -} diff --git a/runtime/src/server/middleware/get_page_handler.ts b/runtime/src/server/middleware/get_page_handler.ts index 09a0261cd..52ae7ad12 100644 --- a/runtime/src/server/middleware/get_page_handler.ts +++ b/runtime/src/server/middleware/get_page_handler.ts @@ -6,15 +6,22 @@ import devalue from 'devalue'; import fetch from 'node-fetch'; import URL from 'url'; import { sourcemap_stacktrace } from './sourcemap_stacktrace'; -import { Manifest, ManifestPage, Req, Res, build_dir, dev, src_dir } from '@sapper/internal/manifest-server'; -import { PreloadResult } from '@sapper/internal/shared'; +import { + Manifest, + ManifestPage, + SapperRequest, + SapperResponse, + build_dir, + dev, + src_dir +} from '@sapper/internal/manifest-server'; import App from '@sapper/internal/App.svelte'; -import { PageContext } from '@sapper/app/types'; +import { PageContext, PreloadResult } from '@sapper/common'; import detectClientOnlyReferences from './detect_client_only_references'; export function get_page_handler( manifest: Manifest, - session_getter: (req: Req, res: Res) => Promise + session_getter: (req: SapperRequest, res: SapperResponse) => Promise ) { const get_build_info = dev ? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8')) @@ -28,7 +35,7 @@ export function get_page_handler( const { pages, error: error_route } = manifest; - function bail(res: Res, err: Error | string) { + function bail(res: SapperResponse, err: Error | string) { console.error(err); const message = dev ? escape_html(typeof err === 'string' ? err : err.message) : 'Internal server error'; @@ -37,7 +44,7 @@ export function get_page_handler( res.end(`
${message}
`); } - function handle_error(req: Req, res: Res, statusCode: number, error: Error | string) { + function handle_error(req: SapperRequest, res: SapperResponse, statusCode: number, error: Error | string) { handle_page({ pattern: null, parts: [ @@ -46,7 +53,12 @@ export function get_page_handler( }, req, res, statusCode, error || 'Unknown error'); } - async function handle_page(page: ManifestPage, req: Req, res: Res, status = 200, error: Error | string = null) { + async function handle_page( + page: ManifestPage, + req: SapperRequest, + res: SapperResponse, + status = 200, + error: Error | string = null) { const is_service_worker_index = req.path === '/service-worker-index.html'; const build_info: { bundler: 'rollup' | 'webpack', @@ -382,7 +394,7 @@ export function get_page_handler( } } - return function find_route(req: Req, res: Res, next: () => void) { + return function find_route(req: SapperRequest, res: SapperResponse, next: () => void) { const req_path = req.path === '/service-worker-index.html' ? '/' : req.path; const page = pages.find(p => p.pattern.test(req_path)); diff --git a/runtime/src/server/middleware/get_server_route_handler.ts b/runtime/src/server/middleware/get_server_route_handler.ts index e3249c4fe..1aa30e64c 100644 --- a/runtime/src/server/middleware/get_server_route_handler.ts +++ b/runtime/src/server/middleware/get_server_route_handler.ts @@ -1,7 +1,7 @@ -import { Req, Res, ServerRoute } from '@sapper/internal/manifest-server'; +import { SapperRequest, SapperResponse, ServerRoute } from '@sapper/internal/manifest-server'; export function get_server_route_handler(routes: ServerRoute[]) { - async function handle_route(route: ServerRoute, req: Req, res: Res, next: () => void) { + async function handle_route(route: ServerRoute, req: SapperRequest, res: SapperResponse, next: () => void) { req.params = route.params(route.pattern.exec(req.path)); const method = req.method.toLowerCase(); @@ -63,7 +63,7 @@ export function get_server_route_handler(routes: ServerRoute[]) { } } - return function find_route(req: Req, res: Res, next: () => void) { + return function find_route(req: SapperRequest, res: SapperResponse, next: () => void) { for (const route of routes) { if (route.pattern.test(req.path)) { handle_route(route, req, res, next); diff --git a/runtime/src/server/middleware/index.ts b/runtime/src/server/middleware/index.ts index f57239829..743d95c80 100644 --- a/runtime/src/server/middleware/index.ts +++ b/runtime/src/server/middleware/index.ts @@ -1,14 +1,14 @@ import fs from 'fs'; import path from 'path'; import mime from 'mime/lite'; -import { Handler, Req, Res, build_dir, dev, manifest } from '@sapper/internal/manifest-server'; +import { Handler, SapperRequest, SapperResponse, build_dir, dev, manifest } from '@sapper/internal/manifest-server'; import { get_server_route_handler } from './get_server_route_handler'; import { get_page_handler } from './get_page_handler'; type IgnoreValue = IgnoreValue[] | RegExp | ((uri: string) => boolean) | string; export default function middleware(opts: { - session?: (req: Req, res: Res) => any, + session?: (req: SapperRequest, res: SapperResponse) => any, ignore?: IgnoreValue } = {}) { const { session, ignore } = opts; @@ -16,7 +16,7 @@ export default function middleware(opts: { let emitted_basepath = false; return compose_handlers(ignore, [ - (req: Req, res: Res, next: () => void) => { + (req: SapperRequest, res: SapperResponse, next: () => void) => { if (req.baseUrl === undefined) { let originalUrl = req.originalUrl || req.url; if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') { @@ -69,7 +69,7 @@ export default function middleware(opts: { export function compose_handlers(ignore: IgnoreValue, handlers: Handler[]): Handler { const total = handlers.length; - function nth_handler(n: number, req: Req, res: Res, next: () => void) { + function nth_handler(n: number, req: SapperRequest, res: SapperResponse, next: () => void) { if (n >= total) { return next(); } @@ -101,8 +101,8 @@ export function serve({ prefix, pathname, cache_control }: { cache_control: string }) { const filter = pathname - ? (req: Req) => req.path === pathname - : (req: Req) => req.path.startsWith(prefix); + ? (req: SapperRequest) => req.path === pathname + : (req: SapperRequest) => req.path.startsWith(prefix); const cache: Map = new Map(); @@ -110,7 +110,7 @@ export function serve({ prefix, pathname, cache_control }: { ? (file: string) => fs.readFileSync(path.join(build_dir, file)) : (file: string) => (cache.has(file) ? cache : cache.set(file, fs.readFileSync(path.join(build_dir, file)))).get(file); - return (req: Req, res: Res, next: () => void) => { + return (req: SapperRequest, res: SapperResponse, next: () => void) => { if (filter(req)) { const type = mime.getType(req.path); @@ -137,4 +137,4 @@ export function serve({ prefix, pathname, cache_control }: { }; } -function noop() {} +async function noop() {} diff --git a/site/content/docs/02-routing.md b/site/content/docs/02-routing.md index d9659ede4..cffccdc15 100644 --- a/site/content/docs/02-routing.md +++ b/site/content/docs/02-routing.md @@ -115,6 +115,15 @@ export async function get(req, res, next) { > `delete` is a reserved word in JavaScript. To handle DELETE requests, export a function called `del` instead. +If you are using TypeScript, use the following types: + +```js +import { SapperRequest, SapperResponse } from '@sapper/server'; + +function get(req: SapperRequest, res: SapperResponse, next: () => void) { ... } +``` + +`SapperRequest` and `SapperResponse` will work with both Polka and Express. You can replace them with the types specific to your server, which are `polka.Request` / `http.ServerResponse` and `express.Request` / `express.Response`, respectively. ### File naming rules