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

Fix: react resolution for interception, parallel and group routes #155

Closed
wants to merge 10 commits into from

Conversation

conico974
Copy link
Collaborator

Right now with app dir, interception, parallel and group routes doesn't work correctly.
This PR should solve this

@changeset-bot
Copy link

changeset-bot bot commented Jul 6, 2023

⚠️ No Changeset found

Latest commit: 1fc4ec5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Jul 6, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
open-next ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 29, 2023 10:20am

@khuezy
Copy link
Collaborator

khuezy commented Jul 17, 2023

@conico974 This appears to be broken in latest 13.4.10 😢
vercel/next.js#50243 (see latest posts)

// Get route pattern
if (rewrite) {
route = {
page: rewrite.destination,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have dynamic routes, the destination is /:dynamic, while the routes manifest is /[dynamic], and thus isApp will fail and cause 404 errors.

rewrite.destination.replace(/:([^\/]+)/g, '[$1]')

Copy link
Collaborator

@khuezy khuezy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! Fixes a few issues others have reported w/ the query stuff too 👍
I'll test this in a couple hours.

packages/open-next/src/adapters/routing.ts Outdated Show resolved Hide resolved
packages/open-next/src/adapters/routing.ts Show resolved Hide resolved
}
// Do we need to replace anything else here?
const rewrittenUrl =
rewrite?.destination.replace(/:[^\/]+/g, '[$1]') ?? rawPath;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that should be good, I've checked the Vercel deployed app and they contain the (.) in the route (so we don't need to replace the groups; they may change that in the future for all we know)

}
// Do we need to replace anything else here?
const rewrittenUrl =
rewrite?.destination.replace(/:[^\/]+/g, '[$1]') ?? rawPath;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rewrite?.destination.replace(/:[^\/]+/g, '[$1]') ?? rawPath;
rewrite?.destination.replace(/:([^\/]+)/g, '[$1]') ?? rawPath;

You forgot the grouping (), otherwise, it'll replace the word with a literal $1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i noticed that, testing this right now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't working finally, we need to replace the rawPath here, i used https://github.com/pillarjs/path-to-regexp. This is the lib they use internally inside next

Copy link
Collaborator

@khuezy khuezy Jul 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm getting 502's on prefetch now
Invoke Error {"errorType":"TypeError","errorMessage":"Expected \"0\" to be a string","stack":["TypeError: Expected \"0\" to be a string"," at file:///var/task/packages/app/index.mjs:27:37895"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's likely that it comes from here https://github.com/conico974/open-next/blob/b3060a37960e4a1305fe82d125d5003c7e0897be/packages/open-next/src/adapters/routing.ts#L74-L85. Could you try adding some log here to see what could be the cause

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated my comment w/ additional info.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have read the docs a little more... https://github.com/pillarjs/path-to-regexp#unnamed-parameters. So in compile it treats (.) as an unnamed parameter, and so it expects it to be present when we use it in toDestination. see https://github.com/pillarjs/path-to-regexp/blob/1cbb9f3d9c3bff97298ec45b1bb2b0beb879babf/src/index.spec.ts#L2892
We actually need the interception path (i.e. (.)|(..)|(...)) to be returned from our function so that it return the correct route. I think we need to escape those interception path for when we use toDestination and then replace those escaped path with (.)|(..)|(...) so that we return the expected path

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm but isn't that (.) regex, and not directory paths?

Copy link
Collaborator

@khuezy khuezy Jul 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a util function in utils.ts:

const replacePathRegex = /(\(?\w+\)?\/)?\(\.{2}\)\/?/;
export function resolvePaths(path: string) {
  const rootIndex = path.indexOf("(...)");
  // If path contains (...), replace everything before it, including itself
  if (~rootIndex) {
    path = path.substring(rootIndex + 5);
  }
  // Remove (.), since that resolves to nothing
  path = path.replace(/\(\.\)\/?/g, "");

  // Resolves each (..) path by consuming the path before it
  // eg: /page/(..)[dynamic] => /[dynamic]
  while (path.match(/\(\.{2}\)/)) {
    path = path.replace(replacePathRegex, "");
  }
  return path;
}

Then in routing.ts, I resolved the source & destination:

    const toDestination = compile(resolvePaths(rewrite?.destination) ?? "");
    const fromSource = match(resolvePaths(rewrite?.source) ?? "");

This works pretty well (for now, needs more testing)

Copy link
Collaborator

@khuezy khuezy Jul 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm, the modal's param.title is messed up for some reason, it has %3Atitle
The client side params is:

author: "%3Aauthor"
title: "%3Atitle"

The object returned has url: url: '/page/(.):author/:title?_rsc=3sl5w',

if (isUsingParams) {
rewrittenUrl = toDestination(params);
}
rewrittenUrl = rewrite.destination;
Copy link
Collaborator

@khuezy khuezy Jul 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be inside an else statement, otherwise the toDestination gets overridden. Hmmm, modal breaks again w/ the else statement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ho come on, how could i forget that..
Ok at least this way it makes more sense to me, i couldn't understand how your fix could resolve the issue, i'm pretty sure we need the (.) in the final url.
I've just tried something and it seems to work

I created these 2 functions

export function escapeRegex(str: string) {
  let path = str.replace(/\(\.\)/g, '%1');

  path = path.replace(/\(\.{2}\)/g, '%2');

  path = path.replace(/\(\.{3}\)/g, '%3');

  return path;
}

export function unescapeRegex(str: string) {
  let path = str.replace(/%1/g, '(.)');

  path = path.replace(/%2/g, '(..)');

  path = path.replace(/%3/g, '(...)');

  return path;
}

And for the rewrite part

const toDestination = compile(
      escapeRegex(rewrite?.destination ?? '') ?? ''
    );
    const fromSource = match(escapeRegex(rewrite?.source) ?? '');
    const _match = fromSource(rawPath);
    if (_match) {
      const { params } = _match;
      const isUsingParams = Object.keys(params).length > 0;
      if (isUsingParams) {
        rewrittenUrl = unescapeRegex(toDestination(params));
        console.log('rewrittenUrl1', rewrittenUrl);
      } else {
        rewrittenUrl = rewrite.destination;
      }
      console.log('rewrittenUrl2', rewrittenUrl);

Of course those escape character are pretty bad, but for testing it's ok, i need to figure out some escape that will not mess things up

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an extremely ugly hack to get the correct params from the modal:

In routing.ts:

  let params: { [key: string]: string } = {};

  if (rewrite) {
    const fromSource = match(resolvePaths(rewrite?.source) ?? "");
    const _match = fromSource(rawPath);
    if (_match) {
      params = _match.params as any;
      rewrittenUrl = rewrite.destination;
    }
  }

  return {
    rawPath: rewrittenUrl,
    url: `${rewrittenUrl}${urlQueryString ? `?${urlQueryString}` : ""}`,
    params,
    __rewrite: rewrite,
  };

In server-adapter:

  const { rawPath, url, params } = handleRewrites(
    internalEvent,
    routesManifest.rewrites
  );
....
  const body = ServerResponse.body(res)
    .toString(encoding)
    .replace(/%3A([^"]+)/g, (_, match: string) => {
      return params[match] || `%3A${match}`;
    });

But there's gotta be a correct way to do this...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, let me give that a go.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Modal interception + client parameters works now :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would fromSource response ever need to be unescapeRegex'd?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes otherwise next-server will not find the correct interception route path.
I'll change the escape character to something like_µ1_, it should be safe enough so that user will not have these in the url

@khuezy
Copy link
Collaborator

khuezy commented Jul 30, 2023

I tried rewrite and redirect. Redirect works but rewrite returns 404... I'll debug some more. My rewrite config is:

async rewrites() {
  return [{source: '/about', destination: '/'}]
}

This works locally but 404s on deployed app.

@conico974
Copy link
Collaborator Author

I tried rewrite and redirect. Redirect works but rewrite returns 404... I'll debug some more. My rewrite config is:

async rewrites() {
  return [{source: '/about', destination: '/'}]
}

This works locally but 404s on deployed app.

It might be that it's generated as afterFiles or as fallback (see https://github.com/conico974/open-next/blob/aec5166911b7887735d8546dbc108bd86bdf7a6f/packages/open-next/src/adapters/util.ts#L48). I'm not sure how we should handle that, especially when.

I tested mine this way and it works for me, even with dynamic path

rewrites: () => ({
    beforeFiles: [
      {
        source: '/shouldBeRewritten',
        destination: '/',
      },
      {
        source: '/shouldBeRewritten/:path',
        destination: '/:path',
      },
    ],
  }),

@khuezy
Copy link
Collaborator

khuezy commented Jul 30, 2023

Ah yes, it's generated as afterFiles. I did this and it worked so far:

  const { beforeFiles, afterFiles, fallback } = routesManifest.rewrites;
  const rewrites = [...beforeFiles, ...afterFiles, ...fallback];

cleanedKey = cleanedKey.replace(/\/\((?!\.)[^\)]*\)/g, '');

// Remove /page suffix
cleanedKey = cleanedKey.replace(/\/page$/g, '');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@conico974 I'm using app/api/something/route.tsx, but the key still contains app/api/something/route, should we also remove the route? The nextjs codebase deletes the last /<thing> from the key.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically we should, but I don't think it matters that much. This is used only to modify react resolution so that your page use the same version of react as next. I don't think they do anything related to react on route

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some people are having rewrite issues in next config. I think we should concat before/after files and fallback unless there's a gotcha w/ it. See: https://discord.com/channels/983865673656705025/1138321896816001086

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is actually a gotcha, https://nextjs.org/docs/app/building-your-application/routing/middleware#matching-paths. We should follow this execution order.
I think in order to do that we'll have to merge this with your middleware fix, i'll try to come up with something later today

@khuezy
Copy link
Collaborator

khuezy commented Sep 1, 2023

#208

@khuezy khuezy closed this Sep 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants