wouter icon indicating copy to clipboard operation
wouter copied to clipboard

Feature request: generatePath for better consistency

Open max-mykhailenko opened this issue 9 months ago • 2 comments

Your bundle size is impressive! The only thing I’m missing is generatePath, similar to React Router’s utility. It’s incredibly helpful for maintaining consistent links in large projects, especially when routes change over time.

max-mykhailenko avatar Apr 06 '25 10:04 max-mykhailenko

@max-mykhailenko you can just grab a copy from packages/react-router/lib/router/utils.ts:859 and paste it into your projects:

export function generatePath<Path extends string>(
  originalPath: Path,
  params: {
    [key in PathParam<Path>]: string | null;
  } = {} as any
): string {
  let path: string = originalPath;
  if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
    warning(
      false,
      `Route path "${path}" will be treated as if it were ` +
        `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
        `always follow a \`/\` in the pattern. To get rid of this warning, ` +
        `please change the route path to "${path.replace(/\*$/, "/*")}".`
    );
    path = path.replace(/\*$/, "/*") as Path;
  }

  // ensure `/` is added at the beginning if the path is absolute
  const prefix = path.startsWith("/") ? "/" : "";

  const stringify = (p: any) =>
    p == null ? "" : typeof p === "string" ? p : String(p);

  const segments = path
    .split(/\/+/)
    .map((segment, index, array) => {
      const isLastSegment = index === array.length - 1;

      // only apply the splat if it's the last segment
      if (isLastSegment && segment === "*") {
        const star = "*" as PathParam<Path>;
        // Apply the splat
        return stringify(params[star]);
      }

      const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
      if (keyMatch) {
        const [, key, optional] = keyMatch;
        let param = params[key as PathParam<Path>];
        invariant(optional === "?" || param != null, `Missing ":${key}" param`);
        return stringify(param);
      }

      // Remove any optional markers from optional static segments
      return segment.replace(/\?$/g, "");
    })
    // Remove empty segments
    .filter((segment) => !!segment);

  return prefix + segments.join("/");
}

You'll only gonna need to introduce your own PathParam type, plus warning and invariant functions. Or remove those from snippet entirely. In fact you could even micro improve it by moving stringify outside and do segments in a single iteration

n1stre avatar Apr 08 '25 14:04 n1stre

Thank you for the suggestion. Only one thing that doesn't allow me to do that is the bigger possible options of how I can build the path with your library. That's why I've started this discussion. If you know any library reversed to your current parser, it would be awesome.

max-mykhailenko avatar Apr 09 '25 10:04 max-mykhailenko

Hey, sorry for the late reply. The regexparam library (which wouter uses internally for pattern matching) has an inject() function that does something similar:

import { inject } from 'regexparam';

inject('/users/:id', { id: '123' }); // => '/users/123'

Since it's already a dependency, you could use it directly. Not sure if it covers all the edge cases from React Router's implementation though.

molefrog avatar Nov 21 '25 13:11 molefrog