Coder Social home page Coder Social logo

remix-routes's Introduction

remix-routes

remix-routes automatically generates typesafe helper functions for manipulating internal links in your Remix apps.

video.mp4

remix-routes also works with remix-modules.

Installation

$ npm add remix-routes

Setup

With Vite

Add remix-routes plugin to your vite.config.ts:

import { defineConfig } from "vite";
import { vitePlugin as remix } from "@remix-run/dev";
import { remixRoutes } from "remix-routes/vite";

export default defineConfig({
  plugins: [
    remix(),
    remixRoutes(options?)
  ],
});

Supported config options:

  • strict: boolean
  • outDir: string

Without Vite

Add remix-routes to your dev and build script in package.json.

With concurrently package:

{
  "scripts": {
    "build": "remix-routes && remix build",
    "dev": "concurrently \"remix-routes -w\" \"remix dev\""
  }
}

With npm-run-all package:

{
  "scripts": {
    "build": "run-s build:*",
    "build:routes": "remix-routes",
    "dev": "run-p dev:*",
    "dev:routes": "remix-routes -w",
  }
}

Usage

Basic usage

import type { ActionFunction } from 'remix';
import { redirect } from 'remix';
import { $path } from 'remix-routes'; // <-- Import magical $path helper from remix-routes.

export const action: ActionFunction = async ({ request }) => {
  let formData = await request.formData();
  const post = await createPost(formData);

  return redirect($path('/posts/:id', { id: post.id })); // <-- It's type safe.
};

Appending query string:

import { $path } from 'remix-routes';

$path('/posts/:id', { id: 6 }, { version: 18 }); // => /posts/6?version=18
$path('/posts', { limit: 10 }); // => /posts?limit=10
// You can pass any URLSearchParams init as param
$path('/posts/delete', [['id', 1], ['id', 2]]); // => /posts/delete?id=1&id=2

Typed query string:

Define type of query string by exporting a type named SearchParams in route file:

// app/routes/posts.tsx

export type SearchParams = {
  view: 'list' | 'grid',
  sort?: 'date' | 'views',
  page?: number,
}
import { $path } from 'remix-routes';

// The query string is type-safe.
$path('/posts', { view: 'list', sort: 'date', page: 1 });

You can combine this feature with zod and remix-params-helper to add runtime params checking:

import { z } from "zod";
import { getSearchParams } from "remix-params-helper";

const SearchParamsSchema = z.object({
  view: z.enum(["list", "grid"]),
  sort: z.enum(["price", "size"]).optional(),
  page: z.number().int().optional(),
})

export type SearchParams = z.infer<typeof SearchParamsSchema>;

export const loader = async (request) => {
  const result = getSearchParams(request, SearchParamsSchema)
  if (!result.success) {
    return json(result.errors, { status: 400 })
  }
  const { view, sort, page } = result.data;
}

Checking params:

import type { ActionFunction } from 'remix';
import { useParams } from "remix";
import { $params } from 'remix-routes'; // <-- Import $params helper.

export const action: ActionFunction = async ({ params }) => {
  const { id } = $params("/posts/:id/update", params) // <-- It's type safe, try renaming `id` param.

  // ...
}

export default function Component() {
  const params = useParams();
  const { id } = $params("/posts/:id/update", params);
  ...
}

$routeId helper for useRouteLoaderData route ids

remix-routes exports the RouteId type definition with the list of all valid route ids for your repository, and has a helper function $routeId that tells typescript to restrict the given string to one of the valid RouteId values.

import type { RouteId } from 'remix-routes';
import type { loader as postsLoader } from './_layout.tsx';
import { useRouteLoaderData } from '@remix-run/react';
import { $routeId } from 'remix-routes';

export default function Post() {
  const postList = useRouteLoaderData<typeof postsLoader>($routeId('routes/posts/_layout'));

Command Line Options

  • -w: Watch for changes and automatically rebuild.
  • -s: Enale strict mode. In strict mode only routes that define SearchParams type are allowed to have query string.
  • -o: Specify the output path for remix-routes.d.ts. Defaults to ./node_modules if arg is not given.

TypeScript Integration

A TypeScript plugin is available to help you navigate between route files.

Installation

$ npm add -D typescript-remix-routes-plugin

Setup

Add the plugin to your tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-remix-routes-plugin"
      }
    ]
  }
}

Select workspace version of TypeScript in VSCode:

Screenshot 2022-12-02 at 5 56 39 pm

License

MIT

remix-routes's People

Contributors

callummr avatar chimame avatar danestves avatar dawnmist avatar github-actions[bot] avatar jlowhy avatar martinliptak avatar rossipedia avatar wolthers avatar yesmeck avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

remix-routes's Issues

Basename support

When using vite we can configure a basename in the remix plugin options.

However this project does not use it when generating paths.

[Bug]: Directory splash routes translate with errors.

Given the following directory, some.route.$ with an index.tsx file inside of it, CLI generates the following mess:

export declare function $params(
  route: "/some/route/:",
  params: { readonly [key: string]: string | undefined }
): {
  : string
};

Named routes?

I would love to be able to name a route (similar to django & laravel). Route names are used in these other mature frameworks to make url refactors easier.

I'm not sure exactly what the API would look like, but do you have any thoughts about this idea?

In Monorepo with multiple remix projects $path is typed for the last built project

Not sure if this is something you want to put the work into fixing - But we have a monorepo with 2 remix projects, both using remix-routes.

We noticed that the routes on $path reflect the last built project, not matter which project it has been called from. I think it's because the types are generated at the root level. I'll try and get a repo ready after work

Wrong path suggestion

Hi,
Thanks for creating such this awesome library, I love type-safe route utilities for all react frameworks like Next and this one for Remix.

This is my test project's working directory tree view (That __auth folder is created because I needed a layout to wrap all auth pages with):

├── __auth
│   └── login.tsx
└── __auth.tsx

Which I expect to suggest me /auth/login but it shows me /auth//login which is a bit wrong.
May you please take a look at this? Thanks!

Custom output path

Awesome library!

I am using NX for my remix project. Hence the node_modules does not exist in the project directory (but instead in the monorepo root). It would be preferable if I could change the output path to a custom one (eg: app/generated/routes.ts) and import that as import { path } from '~/app/generated/routes'.

No Types

No types. Not working.

"@remix-run/node": "^1.7.3",
"@remix-run/react": "^1.7.3",
"@remix-run/serve": "^1.7.3",
"remix-routes": "^1.0.0",

image

image

image

Remix 2.0 support

There seem to be a couple subtle type differences between the tests Remix 1.9.0 version and the latest 2.5.0. I figure it might be helpful to collect the breaking changes in an issue as they're discovered.

The only one I've encountered so far is with the newest useParams hook. I receive the following type error when attempting to follow the readme:

  const {accountId, propertyId} = useParams<Routes['/dashboard/accounts/:accountId/properties/:propertyId']['params']>()

image

This seems to be due to the fact that the latest Remix uses a more recent version of react router that has changed the type of useParams. Specifically it seems that params can no longer be string | number, but must be string | undefined.

The obvious fix would be to adjust the type of this block here:

params: {
<% params.forEach(param => { %>
<%- param %>: string | number;
<% }) %>
},

Of course, this would be a breaking change, so care would need to be taken.

How to pass an array as a query string

Not an issue, but a question? I have to pass an array of ID's as a query string. Up to now i've been mapping and appending them to the string like &id=1&id=2. Is there a way to achieve this with remix-routes?

[Suggestion]: Typechecking route id's

Suggestion description

Recently, Remix introduced a new helper hook, namely useRouteLoaderData (see docs) that requires a routeId parameter, typed unfortunately as a string.
I would like to suggest the addition of another helper (e.g. $routeId) into this library that type-checks said route id's.

v0.0.3

Where can be updated the version of NPM to use the fix of #2 😅 need it for a few projects

Empty params always required

It seems the latest release introduced a bug that requires the second param to always be required, even when the route does not require any paths.

I was surprised the test-suite doesnt catch this, I'll try make a PR for it.

image

Wrapping $path function

Problem

Sometimes, you want to create a function that wraps the $path function. For example, given the following code:

import { redirect } from "@remix-run/node";
import { $path } from "remix-routes";

export let action = () => {
  return redirect(
    $path("user/:userId", { userId: 1 })
  )
}

// ...

In larger projects where you often follow this pattern, your natural instinct is to make a helper function that takes the same typed params as $path but returns a redirect. For example:

import { $path } from "remix-routes";
import { redirect as remixRedirect } from "@remix-run/node";

export function redirect(args: Parameters<typeof $path>) {
  return remixRedirect($path(...args));
}

However, something else is needed. Given the implementation uses function overloading, Parameters does not infer all the correct type narrows. Instead, it just picks the first option and only allows one route.

This is a famous issue in the typescript world. I'm not sure the best way to address it or if it's worth addressing, but I assume it would require restructuring how the types are stored.

Potential Solution/s

An alternative called routes-gen does this by storing all of the routes in a key-value pair and exposing that to the user, but they don't support all the extra features that remix-routes has such as typed query strings.

Side point, this is a great package I enjoy using a lot!

Catch all routes cannot handle "index route"

If I define routes/sign-in.$.tsx then this route also matches the path /sign-in. Remix routes generates as a path segment to match in $path /sign-in/*.
Given this, I cannot use $path to route to /sign-in:

My attempt:

$path("/sign-in/*", { "*": "" }) === "/sign-in/*?*="

Expectation:

$path("/sign-in/*", { "*": "" }) === "/sign-in"

Add SearchParams to $params

Motivation

Currently to get the Search Parameters of a given page you must useSearchParams().

Given a page which already exports a SearchParams type, we can add this to the returned data given to $params and also do it in a type-safe way!

Proposed Usage

/users.tsx

export type SearchParams = {
  sort?: 'date' | 'views',
  page?: number,
}

export const loader = async ({ params }: LoaderArgs) => {
  const { sort, page } = $params("/users", params) // Inferred and typed correctly because of 'SearchParams'
  return json({ sort, page })
}

export default function () {
  let { sort, page } = useLoaderData<typeof loader>() // Fully type-safe params too! 🥳
}

Type errors for all routes that do not export SearchParams

Hello 👋

I'm encountering some noisy type errors in my IDE when using this very helpful package. Specifically, routes that do not export SearchParams are being marked as errors (see screenshot):

image

Is it expected that every route must now export a SearchParams object? Maybe it might be possible to not include the query field for routes that do not export SearchParams? In my head this matches the type, ie "this route does not possess search params at all".

Thanks

Use template literal types to expose list of routes

Would you consider exposing a union typing w list of routes? Similar to the new nextjs typed routes approach. Here's what the generated nextjs types look like for an app with a few routes, including a dynamic /orgs/$orgSlug route:

declare namespace __next_route_internal_types__ {
  type SearchOrHash = `?${string}` | `#${string}`

  type Suffix = '' | SearchOrHash

  type SafeSlug<S extends string> = S extends `${string}/${string}`
    ? never
    : S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type CatchAllSlug<S extends string> = S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type OptionalCatchAllSlug<S extends string> =
    S extends `${string}${SearchOrHash}` ? never : S

  type StaticRoutes = 
    | `/`
    | `/orgs`
    | `/api/v1/protected`
  type DynamicRoutes<T extends string = string> = 
    | `/api/v1/auth/${CatchAllSlug<T>}`
    | `/orgs/${SafeSlug<T>}`
    | `/api/v1/orgs/${SafeSlug<T>}/traits`

  type RouteImpl<T> = 
    | StaticRoutes
    | `${StaticRoutes}${SearchOrHash}`
    | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)   
}

This would make it easy to do things like create a typesafe link component:

import type { LinkProps as BaseLinkProps } from '@remix-run/react';
import { Link as BaseLink } from '@remix-run/react';
import type { AllowedRoutes } from 'remix-routes'; // <- NEW export (whatever name, just a new name so it isn't breaking change)

export type LinkProps = Omit<BaseLinkProps, 'to'> & {
  href: AllowedRoutes;
};

function LinkInner({ href, ...rest }: LinkProps, ref: React.ForwardedRef<HTMLAnchorElement>) {
  return <BaseLink ref={ref} to={href} {...rest} />;
}

export const Link = forwardRef(LinkInner);

Usage

<Link href={`/users/${user.id}`}>{user.name}</Link>

The full generated link.d.ts nextjs file (for a simple app I have locally) in case it's helpful:

// Type definitions for Next.js routes

/**
 * Internal types used by the Next.js router and Link component.
 * These types are not meant to be used directly.
 * @internal
 */
declare namespace __next_route_internal_types__ {
  type SearchOrHash = `?${string}` | `#${string}`

  type Suffix = '' | SearchOrHash

  type SafeSlug<S extends string> = S extends `${string}/${string}`
    ? never
    : S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type CatchAllSlug<S extends string> = S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type OptionalCatchAllSlug<S extends string> =
    S extends `${string}${SearchOrHash}` ? never : S

  type StaticRoutes = 
    | `/`
    | `/orgs`
    | `/api/v1/protected`
  type DynamicRoutes<T extends string = string> = 
    | `/api/v1/auth/${CatchAllSlug<T>}`
    | `/orgs/${SafeSlug<T>}`
    | `/api/v1/orgs/${SafeSlug<T>}/traits`

  type RouteImpl<T> = 
    | StaticRoutes
    | `${StaticRoutes}${SearchOrHash}`
    | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)
    
}

declare module 'next' {
  export { default } from 'next/types'
  export * from 'next/types'

  export type Route<T extends string = string> =
    __next_route_internal_types__.RouteImpl<T>
}

declare module 'next/link' {
  import type { LinkProps as OriginalLinkProps } from 'next/dist/client/link'
  import type { AnchorHTMLAttributes } from 'react'
  import type { UrlObject } from 'url'
  
  type LinkRestProps = Omit<
    Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof OriginalLinkProps> &
      OriginalLinkProps,
    'href'
  >

  export type LinkProps<T> = LinkRestProps & {
    /**
     * The path or URL to navigate to. This is the only required prop. It can also be an object.
     * @see https://nextjs.org/docs/api-reference/next/link
     */
    href: __next_route_internal_types__.RouteImpl<T> | UrlObject
  }

  export default function Link<RouteType>(props: LinkProps<RouteType>): JSX.Element
}

declare module 'next/navigation' {
  export * from 'next/dist/client/components/navigation'

  import type { NavigateOptions, AppRouterInstance as OriginalAppRouterInstance } from 'next/dist/shared/lib/app-router-context'
  interface AppRouterInstance extends OriginalAppRouterInstance {
    /**
     * Navigate to the provided href.
     * Pushes a new history entry.
     */
    push<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void
    /**
     * Navigate to the provided href.
     * Replaces the current history entry.
     */
    replace<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void
    /**
     * Prefetch the provided href.
     */
    prefetch<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>): void
  }

  export declare function useRouter(): AppRouterInstance;
}

Broken on 1.4.0

Still investigating to figure out what happened but all usage of $path(...) is broken in 1.4.0 but works again when I revert to 1.3.1.

Sample Error

app/routes/org/$orgId/app/index.tsx:351:23 - error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type '"/org/:orgId/app/:appId/:branch/:componentType"' is not assignable to parameter of type '"/org"'.

351             to={$path("/org/:orgId/app/:appId/:branch/:componentType", {
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  node_modules/remix-routes.d.ts:521:25
    521 export declare function $path(
                                ~~~~~
    The last overload is declared here.

app/routes/org/$orgId/index.tsx:8:25 - error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type '"/org/:orgId/app"' is not assignable to parameter of type '"/org"'.

8   return redirect($path("/org/:orgId/app", { orgId: orgId as string }));
                          ~~~~~~~~~~~~~~~~~

  node_modules/remix-routes.d.ts:521:25
    521 export declare function $path(
                                ~~~~~
    The last overload is declared here.

Notes

I'm also using Typescript "strict" mode

use strict: command not found

At the moment of run the script, all these errors are triggered

❯ yarn remix-routes

yarn run v1.22.1
$ /Users/danestves/code/mastertailwind.dev/node_modules/.bin/remix-routes
/Users/danestves/code/mastertailwind.dev/node_modules/.bin/remix-routes: line 1: use strict: command not found
/Users/danestves/code/mastertailwind.dev/node_modules/.bin/remix-routes: line 2: syntax error near unexpected token `('
/Users/danestves/code/mastertailwind.dev/node_modules/.bin/remix-routes: line 2: `var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {'

Feature Request: Optional exporting query param type and making the $path more type safe

Hey,

I am using remix-routes in my project and it's helpful, but I find a problem with query param typing.

So my whole idea is the following:

// routes/search.tsx

const Page = () => {

}

export type TQuerySearch = {
  productId?: string;
  category?: string;
}

export default Page

// some other file
$path("/search", {
  productId: "okay",
  categoryId: "no okay" // Dude it's an error
})

I can help developing it.

Module '"remix-routes"' has no exported member '$path'.

I just wanted to try out this package as I'm searching for a way to have type-safety on routes.

Unfortunately, I'm getting a Typescript error when I want to import $path.

Module '"remix-routes"' has no exported member '$path'.

When going to remix-routes/index.d.ts in node-modules I'm getting this error:

Cannot find module '.remix-routes/types' or its corresponding type declarations.ts

Any idea how to fix this? I would really like to test this.

I installed it with yarn and didn't get any errors during installation.

Helper to validate params

Hey, thanks for this incredible library! Refactoring gets so much easier when I can rely on $path helper to check route and param names when generating paths. I was wondering if it makes sense to have a similar helper to bring type safety to reading params.

export const action: ActionFunction = async ({ params }) => {
  const { topicId, articleId } = params

  if (!topicId) throw new Error("Missing param: topicId")
  if (!articleId) throw new Error("Missing param: articleId")

  // Use topicId and articleId.
}

I could imagine:

export const action: ActionFunction = async ({ params }) => {
   const { topicId, articleId } = $params(
    "/user/topics/:topicId/articles/:articleId/finish",
    params
  )

  // Use topicId and articleId.
}

Moving the file to a different location would make you update /user/topics/:topicId/articles/:articleId/finish and possibly fix changed param names.

Generated code would look like this:

const $params = (
  _path: "/user/topics/:topicId/articles/:articleId/finish",
  params: Params<string>
) => params as { topicId: string; articleId: string }

If we don't want to rely on Remix that all params from URL are present, we can add a runtime check:

const $params = (
  path: "/user/topics/:topicId/articles/:articleId/finish",
  params: Params<string>
) => {
  for (const param of routes[path]) {
    if (!params[param]) {
      throw new Error(`Missing param: ${param}`)
    }
  }

  return params as { topicId: string; articleId: string }
}

Nested route params broken

Hi,
I'm having an issue with nested routes as below:

├── people
│   └──$personId
│              └── $planId
│                         └── remove-plan.tsx

The generated types are:

export declare function $path(
  route: "/people/:personId/:planId/remove-plan",
  params: { personId: string | number; planId: string | number; remove-plan: string | number },
  query?: Record<string, string | number>
): string;

export declare function $params(
  route: "/people/:personId/:planId/remove-plan",
  params: { readonly [key: string]: string | undefined }
): {
  personId: string,
  planId: string,
  remove-plan: string
};

Not sure why 'remove-plan' is being added as a param here

Support for vite

Remix now had integration with vite. Does this project work with it?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.