Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate types for paths #7110

Closed
Tracked by #11108
AlexRMU opened this issue Oct 1, 2022 · 6 comments
Closed
Tracked by #11108

Generate types for paths #7110

AlexRMU opened this issue Oct 1, 2022 · 6 comments

Comments

@AlexRMU
Copy link

AlexRMU commented Oct 1, 2022

Describe the problem

  • Consider the base variable
  • Consider other paths on the same site, but outside of kit (special type in App?)
  • Consider external links
  • Slugs types need to be taken from params
  • Consider hash, parameters, different protocols
  • Use these types in kit wherever possible (redirect, load params, ...)

Example:

/*
existing paths:
[
    "/",
    "/blog",
    "/blog/post/[post]",
    "/blog/tag/[tag=integer]",
    "/shop/[...slugs]",
]
*/

throw redirect(301, "/");
throw redirect(301, "/blog");
throw redirect(301, "/blog/qwe"); //type error
throw redirect(301, "/blog/post"); //type error
throw redirect(301, "/blog/post/qwe");
throw redirect(301, "/blog/tag/1");
throw redirect(301, "/blog/tag/qwe"); //type error
throw redirect(301, "/shop"); //type error
throw redirect(301, "/shop/a/b/c");
throw redirect(301, "https://www.google.com");
throw redirect(301, "/qwe"); //external

Describe the proposed solution

It is interesting:

Alternatives considered

No response

Importance

would make my life easier

Additional Information

No response

@benmccann
Copy link
Member

This would work in the short-term, but it would likely be difficult to make it work longer-term if we allow you to do things like have routes in multiple sub-projects and register routes from a different sub-project

@ghost
Copy link

ghost commented Oct 2, 2022

I agree that it's difficult to implement it before the more advanced routing features drop.

But I have a silly script as a workaround.
It does not handle parameter matching. It could be done if the match function can be expressed as a type.

import glob from "fast-glob"
import path from "path"
import fs from "fs"

const rootDir = path.resolve(__dirname, "..")
const routesDir = path.join(rootDir, "src", "routes")
const libDir = path.join(rootDir, "src", "lib")

// look for +page.svelte and +page.ts files in ./src/routes
const files = glob.sync("**/+page.{svelte,ts}", {
  cwd: routesDir,
  absolute: true,
  onlyFiles: true,
  ignore: ["**/node_modules/**", "**/.*"]
})

// make url parts out of the file paths
const pathParts = files.map((file) =>
  path.relative(routesDir, file).split(path.sep).slice(0, -1)
)

const routeTypesContent = `import { redirect } from "@sveltejs/kit"\n
export type TypedRoute = ${pathParts.map((parts) => {
    // in case it's root, we don't want to have an empty string
    if (!parts.length) return '"/"'
    // remove layout groups
    parts = parts.filter((part) => !part.startsWith("("))
    //replace params with string type
    parts = parts.map((part) => (part.startsWith("[") ? "${string}" : part))
    // make the route type
    return `\`/${parts.join("/")}\``
  }).join(" | ")}\n
export const typedRedirect = (status: number, url: TypedRoute) => redirect(status, url)\n
export const typedHref = (url: TypedRoute) => url
`

// Write out $routes.d.ts
fs.writeFileSync(
  path.join(libDir, "$routes.ts"), 
  routeTypesContent, {
  encoding: "utf8",
  flag: "w"
})

It generates a $routes.ts file in the src/lib folder.

// imagine something like /foo/[bar]
import { redirect } from "@sveltejs/kit"
export type TypedRoute = "/" | `/foo` | `/foo/${string}`
export const typedRedirect = (status: number, url: TypedRoute) => redirect(status, url)
export const href = (url: TypedRoute) => url

It could be used as so:

<script lang="ts">
  import { typedRedirect, typedHref, type TypedRoute } from '$lib/$routes'
  
  const home: TypedRoute = '/'

  // redirect with
  typedRedirect(307, '/foo')
</script>

<a href={typedHref('/foo/bar')}>FooBar</a>

I added it to my package.json sync command for convenience.

Please let me know if I should make the above section collapsible.

@AlexRMU
Copy link
Author

AlexRMU commented Oct 5, 2022

Generate types for requests to +server and responses according to the same scheme

Fetch will have a response type not any, but the one that the function returns in the file.

  • Headers, cache, service worker, ...
  • Different methods and Content-Types

@sabine
Copy link

sabine commented Oct 21, 2022

I also use a script workaround to provide safe routes. But instead of trying to type-check the path-strings, I generate string constants (for routes without parameters) and functions (for routes with parameters). Then, I make sure that all paths used in my app are either given by these constants/functions or external URLs.

Given a directory

src/routes/
|-  project/[id]/+page.svelte
|-  +page.svelte

the script generates

export namespace Routes {
  export const Index = "/";

  export function Project(id: string): string {
    return `/project/${id}/`
  }
}

In the template...

<a href={Routes.Project(project_id)}>{project.title}</a>

Having all the routes in a shared namespace makes it quick and easy to import them, however, it comes at the cost of having to import all the routes when you want to use only one. And this is, unfortunately, a serious flaw.


For generating typed bindings for calls to +server.ts-endpoints, I write all my endpoints in this schema:

// src/routes/!/projects/+server.ts

import type { RequestEvent } from '@sveltejs/kit';
import { type EndpointResponse } from '@endpoint_utils';

type InputBody = { q: string };
type OutputBody = { projects: Project[] };
type ErrorType = void;

export async function POST({ request }: RequestEvent): Promise<EndpointResponse<OutputBody, ErrorType>> {
    // tell the compiler that the json body of the request has type InputBody
    let input: InputBody = await request.json();

    let result = ... // fetch projects from external API or database;
    ...
    // type EndpointResponse<OutputBody, ErrorType> enforces
    // that the endpoint returns either a Response with
    // body of type OutputBody or ErrorType
}

The script inspects /src/routes/!/projects/+server.ts, looking for InputBody, OutputBody and ErrorType and generates

export type ProjectsInput =  { q: string }
export type ProjectsOutput =  { projects: Project[] }
export type ProjectsError =  void

export function projects(body: ProjectsInput): { response: Promise<ProjectsOutput | EndpointError<ProjectsError>>, abort: () => void} {
    return fetch(`/!/projects/`, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
    });
}

However, this endpoint-bindings-generating script is just a band-aid.

In an ideal world, RequestEvent would somehow allow me to state the types of the input body / URL search params:

type JSONPostRequestEvent<InputBody> = { json: InputBody, ... }

type GetRequestEvent<SearchParams> = { search_params: SearchParams, ... }

But there is another aspect to this: path parameters, which should be automatically generated from the paths. E.g. src/routes/project/[id]/+server.ts. So all the flavors of RequestEvent used in +server.ts should already be generated automatically by SvelteKit to type params (similar to the generated PageServerLoad for endpoints belonging to pages).

Then, +server.ts could look like this:

import type { JSONPostRequestEvent } from './$types';

type OutputBody = { projects: Project[] };
type ErrorType = void;

export async function json_POST({ request, json }: JSONPostRequestEvent<{ q: string }>)
  : Promise<EndpointResponse<OutputBody, ErrorType>> {
    // compiler knows that the type of `json` is `{ q: string }`

    let result = ... // fetch projects from external API or database;
     ....
}

Then, a script could extract the type arguments of RequestEvent to generate typed endpoint bindings.


But here comes the big caveat: the approach of generating actual constants/functions for routes and typed endpoint bindings is pretty invasive in the sense that it's not just types. There's actual code being generated which is shipped to production.

However, it's also simpler to do than generating types and it's convenient in terms of code editors autocompleting bindings and routes.

To specialize / break up the type of RequestEvent in such a way that we could express the type of the JSON input body, it looks to me like the changes necessary would be quite invasive. In order to provide a type to request.json(), we'd need to wrap this call, so that the endpoint handler gets a typed json parameter. That's why I think there'd need to be functions like json_POST to tell SvelteKit that it should generate a type that has a JSON input body parameter.


Looking at this from the other direction: In the case of pages, it was possible to generate types, as we look at the type of data of the page, and then use that to generate the PageServerLoad type for that page.

However, with independent endpoints, there's no corresponding entity that requires data to be in a certain shape that we could inspect.

Maybe, if we would have to write a function for every endpoint that makes a call to that endpoint, we could annotate that with types, which are then used to generate a type for the handler in +server.ts? 🤔

@ghost
Copy link

ghost commented Nov 3, 2022

I stumbled upon this https://github.com/mattpocock/make-route-map

By replacing the param matcher :id with svelte [id] I can generate a routeMap.

For now, I crawl the routes folder and generate an index.ts. It would be great if this could be done by svelte in the sync stage and offer an import, and replace the function with static value similar to env/static

@eltigerchino
Copy link
Member

Closed as duplicate of #647

@eltigerchino eltigerchino closed this as not planned Won't fix, can't repro, duplicate, stale Nov 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants