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(node-resolve): do not ignore exceptions #564

Merged
merged 11 commits into from Oct 25, 2020

Conversation

tjenkinson
Copy link
Member

@tjenkinson tjenkinson commented Sep 5, 2020

Rollup Plugin Name: node-resolve

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes kind of
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

If yes, then include "BREAKING CHANGES:" in the first commit message body, followed by a description of what is breaking.

Description

recommend viewing with indentation disabled on github

I was looking to add a feature to return false from resolveId for builtins when preferBuiltins was enabled but noticed code wasn't running that I was expecting. I will follow up with this in a different PR (which I'll rebase that when this PR is finished).

There was a try/catch which was ignoring any exception and returning null from resolveId. I tried removing that block and it caused test failures, and was why my changes weren't working 😱.

I fixed the hidden issues to make the tests pass again. The 'root-dir' test looked like it was a bug in the test itself so I updated that test.

I think it would be better to let any unexpected errors throw out from resolveId and fail a build instead of silently converting them to a null return. If you'd like that back maybe we can figure out a way to have that logic disabled for tests.

if (/\0/.test(importer)) {
// handle cases like common-js plugin which has form
// \u0000<id>?commonjs-proxy
importer = importer.slice(1);
Copy link
Member Author

Choose a reason for hiding this comment

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

not sure if there's a better way to handle this case?

If you remove this change you'll see the pkg.browser with mapping to prevent bundle by specifying a value of false test fail because the call to resolveId with the null byte is not allowed

Copy link
Member

Choose a reason for hiding this comment

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

That one is a really good question that got overlooked so far. I do not agree with the solution, though. Files starting with \0 are under the sole maintenance of some plugin, and we should not make assumptions about what the part behind the \0 means.
I think a valid approach might be to treat the importer like undefined, which is the value that gets passed for entry points. At least for the commonjs plugin, this should work nicely as all imports from those proxy files are fully resolved absolute paths anyway, and I would expect the same from other plugins as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think a valid approach might be to treat the importer like undefined, which is the value that gets passed for entry points

@lukastaegert I pushed a commit to do this

@tjenkinson tjenkinson marked this pull request as ready for review September 5, 2020 19:12
test('deduplicates modules from the given root directory', async (t) => {
const bundle = await rollup({
input: 'index.js',
input: './packages/package-a/index.js',
Copy link
Member Author

Choose a reason for hiding this comment

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

this was causing an error because the input could not be found. It should be relative to the rootDir right?

@tjenkinson
Copy link
Member Author

bump :)

@danielgindi
Copy link
Contributor

I'm a bit confused. I didn't see anywhere that you presented a specific use case, or your specific frontal-accident with this plugin...

Was it trying to somehow resolve modules that are actually internal commonjs proxies? Or was it just the importer being a commonjs proxy that broke things?

And are you now throwing the errors, failing the whole pipeline - or emitting warnings? (Because I see you removed the original try-catch, instead of altering it)

@tjenkinson
Copy link
Member Author

Hi @danielgindi

I didn't see anywhere that you presented a specific use case, or your specific frontal-accident with this plugin...

The thing I was trying to do which caused me to stumble into this was the linked PR. I was looking to make builtin modules be treated as external.

Was it trying to somehow resolve modules that are actually internal commonjs proxies?

Maybe I should have been clearer in the description, but the exact cause of the problem was that a builtin module id would currently cause realpathSync to throw, and that error would bubble all the way up to the catch which then returned null.

And are you now throwing the errors, failing the whole pipeline - or emitting warnings?

There isn't the global handler anymore that converts errors to null. IMO catch should be used to handle specific errors and not as a global catch all. If there's an unexpected error we're not expecting to handle I think letting it bubble up to rollup and probably fail the build is the best outcome. Otherwise it silently catches things and issues can go unnoticed, like the deduplicates modules from the given root directory test for example which was actually broken.

@danielgindi
Copy link
Contributor

@tjenkinson so basically avoid having to specify extra externals in the rollup level?

About the exceptions- I'm not sure I agree.
In general what you are saying is true.
But there are cases where a warning is more appropriate. If you failed to resolve a module (for whatever unknown reason), you don't necessarily want to stop everything.
Other modules print a warning, suggesting a possible root cause.
This means that if there is a failure in node-resolve, the user in some cases might be able to do nothing to bypass the error if he deems the error "ah, that's fine by me".

Also- removing the catch is a BREAKING CHANGE in that manner, and you did not mark it as one in the intro ;)

@tjenkinson
Copy link
Member Author

so basically avoid having to specify extra externals in the rollup level?

yep this was the idea (but I decided to keep that separate to this PR)

But there are cases where a warning is more appropriate. If you failed to resolve a module (for whatever unknown reason), you don't necessarily want to stop everything.

Happy to put the catch back and log a warning in there if people would prefer that. However IMO as a developer I'd want there to be a blocker if something unexpected went wrong. Especially when some of the options like jail seem like they're meant to to prevent things outside a certain location being imported? I'd prefer it to fail safe and stop on an issue then return null and hand off to the next resolver.

Also- removing the catch is a BREAKING CHANGE in that manner, and you did not mark it as one in the intro ;)

Yeh I wasn't sure on this. I'm fine with it being breaking (or putting the catch back but with at least a warning log :) ). Also disabling that catch for tests would be nice.

@danielgindi
Copy link
Contributor

Happy to put the catch back and log a warning in there if people would prefer that. However IMO as a developer I'd want there to be a blocker if something unexpected went wrong. Especially when some of the options like jail seem like they're meant to to prevent things outside a certain location being imported? I'd prefer it to fail safe and stop on an issue then return null and hand off to the next resolver.

I guess we need more opinions here

Yeh I wasn't sure on this. I'm fine with it being breaking (or putting the catch back but with at least a warning log :) ). Also disabling that catch for tests would be nice.

Btw, if only addings warnings to rollup, you could look at other test (i.e in commonjs) - some of them test for the existing of a warning.
So if it's the expected flow, to have a warning - or even an exception - then you should be expecting that in a test and make sure it happens.

@LarsDenBakker
Copy link
Contributor

In my opinion a failure to resolve a module should result in an error and not into making the module an external, that's something that confuses users a lot currently. However this is not something that should be handled by the node resolve plugin, it should be on the level of rollup. There could be multiple plugins who resolve an id, and node-resolve shouldn't block further resolving.

@tjenkinson
Copy link
Member Author

In my opinion a failure to resolve a module should result in an error and not into making the module an external, that's something that confuses users a lot currently.

AFAIK this becomes a warning from rollup if all resolveId hooks have returned null. Do you mean you'd prefer rollup to error there instead of warn? I think I agree, but not sure how it relates to this PR?

Also by "failure to resolve a module" do you mean a controlled failure such as the module not existing, or both controlled and uncontrolled failures like the file system throwing an error?

There could be multiple plugins who resolve an id, and node-resolve shouldn't block further resolving.

Also agree. The question is if an unexpected error occurs internally to the plugin, should we propagate this up to rollup, or convert this to null like we do now to hand over to the other resolvers (and log a warning)?

@LarsDenBakker
Copy link
Contributor

An unexpected error should result in a failure, but I thought the try/catch catches errors thrown by the resolve package when a module couldn't by found. That's how it relates to this PR.

@tjenkinson
Copy link
Member Author

tjenkinson commented Oct 11, 2020

but I thought the try/catch catches errors thrown by the resolve package when a module couldn't by found

Yep it looks that way, but IMO it's clearer to handle a module not being found in the main code path as an expected case (which is what this PR does), not the exception path.

When I started my PR to support automatically making builtins external, I was surprised when my code wasn't running because we were silently ending up in that catch block as the result of trying to read fs for example throwing given it's not a real file.

I guess I could take a different approach, and put the logic in the catch block to see if it's a builtin module, but that doesn't feel nice.

@lukastaegert
Copy link
Member

I agree that this big catch was really bad for testing, but the general idea of course was that when this plugin cannot resolve a module, other resolvers should get a chance to do so before throwing.

So the question to me is that is not entirely clear to me from looking at the code: Will this still be the case for all "relevant" cases? And by that I mean it is definitely ok to throw if there is a weird file system error or some error that results from data in an unexpected format being passed to this plugin.

If you think this is the case, we could try to release this fix but be prepared to do a quick rollback in case we missed something important here.

In my opinion a failure to resolve a module should result in an error and not into making the module an external, that's something that confuses users a lot currently. However this is not something that should be handled by the node resolve plugin, it should be on the level of rollup. There could be multiple plugins who resolve an id, and node-resolve shouldn't block further resolving.

@LarsDenBakker If you say so, we can talk about this, could be a change in the next major version. Personally, I like this behaviour for quick prototyping, but if it confuses newcomers, then that is bad. Rollup could augment the error with instructions on how to mark the specific import as external to solve the issue.

I also think a lot of the woes that make this feature helpful is that builtins are not marked as external when using the preferBuiltins option in this plugin. As I understand @tjenkinson is working on a fix for just that, then one more argument for making this an error in Rollup.

@tjenkinson
Copy link
Member Author

Will this still be the case for all "relevant" cases? And by that I mean it is definitely ok to throw if there is a weird file system error or some error that results from data in an unexpected format being passed to this plugin.

From my understanding yes it will. I've gone through everything that was in the try block looking at what could throw and nothing jumps out.

The one I'd like confirmation on is

      if (options.modulesOnly) {
        const code = await readFile(resolved, 'utf-8');
        if (isModule(code)) {
          return {
            id: `${resolved}${importSuffix}`,
            moduleSideEffects: hasModuleSideEffects(resolved)
          };
        }
        return null;
      }

Do we need to check a file at resolved exists here?

I also think a lot of the woes that make this feature helpful is that builtins are not marked as external when using the preferBuiltins option in this plugin. As I understand @tjenkinson is working on a fix for just that, then one more argument for making this an error in Rollup.

Yeh that's what I started here but thought I'd defer that until after this one.

@lukastaegert
Copy link
Member

Do we need to check a file at resolved exists here?

Looking at what this option is supposed to be doing and the current implementation, I am leaning towards file-not-found errors should also return null instead of throwing here (while other errors should throw), but that is an opinion.

@tjenkinson
Copy link
Member Author

Done.

Wondering if

      if (/\0/.test(importer)) {
        importer = undefined;
      }

is a breaking change?

@lukastaegert
Copy link
Member

Hard to say. I am not aware of any official plugins for which this would be breaking, but it is possible for third party plugins.

@tjenkinson
Copy link
Member Author

Soooooo. I think this PR is ready to go? Don't see any other suggestions/concerns atm.

@shellscape shellscape merged commit 1459cf0 into rollup:master Oct 25, 2020
@shellscape
Copy link
Collaborator

thanks!

@tjenkinson
Copy link
Member Author

Here is the next PR to make builtins external :)

#627

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants