Repository: remix-run/react-router
File: packages/react-router/lib/router/utils.ts (TypeScript)

Code before:

export function compilePath(
  path: string,
  caseSensitive = false,
  end = true,
): [RegExp, CompiledPathParam[]] {
  warning(
    path === "*" || !path.endsWith("*") || path.endsWith("/*"),
    `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(/\*$/, "/*")}".`,
  );

  let params: CompiledPathParam[] = [];
  let regexpSource =
    "^" +
    path
      .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
      .replace(/^\/*/, "/") // Make sure it has a leading /
      .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
      .replace(
        /\/:([\w-]+)(\?)?/g,
        (_: string, paramName: string, isOptional) => {
          params.push({ paramName, isOptional: isOptional != null });
          return isOptional ? "(?:/([^\\/]*))?" : "/([^\\/]+)";
        },
      ) // Dynamic segment
      .replace(/\/([\w-]+)\?(\/|$)/g, "(/$1)?$2"); // Optional static segment

  if (path.endsWith("*")) {
    params.push({ paramName: "*" });
    regexpSource +=
      path === "*" || path === "/*"
        ? "(.*)$" // Already matched the initial /, just match the rest
        : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
  } else if (end) {
    // When matching to the end, ignore trailing slashes
    regexpSource += "\\/*$";
  } else if (path !== "" && path !== "/") {
    // If our path is non-empty and contains anything beyond an initial slash,
    // then we have _some_ form of path in our regex, so we should expect to
    // match only if we find the end of this path segment.  Look for an optional
    // non-captured trailing slash (to match a portion of the URL) or the end
    // of the path (if we've matched to the end).  We used to do this with a
    // word boundary but that gives false positives on routes like
    // /user-preferences since `-` counts as a word boundary.
    regexpSource += "(?:(?=\\/|$))";
  } else {
    // Nothing to match for "" or "/"
  }

Review comment (bug):
Making the question mark optional can cause issues when there is a suffix after the param - here's a failing case:

```
expect(matchPath("/sitemap/:lang?.xml", "/sitemap.xml")?.params).toBeNull();
```

Normally we do require the params to be the full segment, but the dot notation slipped through out original implementations and enough apps rely on it that it would be a breaking change at this point so we plan to continue supporting it.

Can you see if there's a different solution that plays nicely with that format?

Code after:

export function compilePath(
  path: string,
  caseSensitive = false,
  end = true,
): [RegExp, CompiledPathParam[]] {
  warning(
    path === "*" || !path.endsWith("*") || path.endsWith("/*"),
    `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(/\*$/, "/*")}".`,
  );

  let params: CompiledPathParam[] = [];
  let regexpSource =
    "^" +
    path
      .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
      .replace(/^\/*/, "/") // Make sure it has a leading /
      .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
      .replace(
        /\/:([\w-]+)(\?)?/g,
        (
          match: string,
          paramName: string,
          isOptional: string | undefined,
          index: number,
          str: string,
        ) => {
          params.push({ paramName, isOptional: isOptional != null });

          if (isOptional) {
            let nextChar = str.charAt(index + match.length);
            if (nextChar && nextChar !== "/") {
              return "/([^\\/]*)";
            }

            return "(?:/([^\\/]*))?";
          }

          return "/([^\\/]+)";
        },
      ) // Dynamic segment
      .replace(/\/([\w-]+)\?(\/|$)/g, "(/$1)?$2"); // Optional static segment

  if (path.endsWith("*")) {
    params.push({ paramName: "*" });
    regexpSource +=
      path === "*" || path === "/*"
        ? "(.*)$" // Already matched the initial /, just match the rest