Skip to content
This repository was archived by the owner on Sep 2, 2023. It is now read-only.
This repository was archived by the owner on Sep 2, 2023. It is now read-only.

import(cjs) messes with module.parent  #469

Description

@aduh95

Some packages use a check on module.parent to know if they are launched from CLI or required by another module:

// module.cjs
if (!module.parent) throw new Error("Running from CLI is not supported!");

This works fine if you use require (I tried from both CJS and ES modules):

$ node -p "assert.doesNotThrow(()=>require('./module.cjs')), 'No error!';"
No error!

However, when importing from ESM, that doesn't work:

// module.mjs
import './module.cjs'; // This will throw the "don't run from CLI" Error...
console.log('This is fine.'); // This is never reached

Same for dynamic imports:

// module.js // (same behaviour on ESM and CJS)
import('./module.cjs')
  .then(()=>console.log('This is fine.'))
  .catch(()=>console.log('Oh no!'))

I can see three ways to tackle this issue, but none is very satisfying to me:

  • Make module.parent truthy when a CJS is imported (although I have no idea what it could look like).
  • Deprecate module.parent? Packages using require.main===module are fine, so that's what library authors should be using to test this, right?
  • Disregard this issue, as users can use module.createRequire to workaround the issue.

Activity

  1. hybrist commented on Jan 15, 2020

    @hybrist
    Contributor

    Thanks for bringing this up, I don't think we were tracking this yet! Your options sound spot-on.

    Make module.parent truthy when a CJS is imported (although I have no idea what it could look like).

    Making module.parent could be tricky if any package actually tries to check it - it would have to be something that itself isn't a valid CJS module. So we'd have to select a value very carefully or risk breaking other uses of module.parent if those exist.

    Packages using require.main===module are fine, so that's what library authors should be using to test this, right?

    Yeah, I think to me require.main===module is the "canonical" check. That's also what the docs suggest using: https://nodejs.org/api/modules.html#modules_accessing_the_main_module. So for maintainers, the suggested path would be to migrate to that pattern instead.

    Disregard this issue, as users can use module.createRequire to workaround the issue.

    I think it'll depend on how widespread this issue is. Can you share which package (or packages) this happened with? Capturing a list here may make it easier for us to determine the impact.

  2. SimenB commented on Jan 15, 2020

    @SimenB
    Member

    One use case for module.parent, fwiw: https://github.com/hypercloud/import-dir/blob/afb1ef9ae1849e6ce8b535f69d412e38dda3115b/index.js#L14. Not a highly used module, but still

  3. bmeck commented on Jan 15, 2020

    @bmeck
    Member

    @SimenB not all ESM have a valid file path associated with them (data: in particular for now), and/or there may have multiple ESM with the same file path but different URLs (using URL fragments). I'm not sure we could preserve that through populating module.parent from an ESM base.

  4. aduh95 commented on Jan 15, 2020

    @aduh95
    Author

    For reference, I have seen is-port-available and pdf-parse use a similar pattern. Not highly used modules either, but there are certainly others.

  5. ExE-Boss commented on Feb 1, 2020

    @ExE-Boss

    I think we should deprecate module.parent.

  6. DerekNonGeneric commented on Mar 12, 2020

    @DerekNonGeneric
    Contributor

    Folks, I'm very much opposed to deprecating module.parent.

    In fact, I'm in need of this feature to be functional for ES modules too.

    The intended use-case is to be able to visualize module graphs. To be able to traverse these graphs, I'd need this parent-children relationship metadata. This also relates to APMs.

    Additionally, I do not think something so important should be deprecated merely to discourage a pattern.

  7. devsnek commented on Mar 12, 2020

    @devsnek
    Member

    @DerekNonGeneric parent does not give an accurate traversal of a graph anyway, because modules can actually have multiple parents. you should start at the entrypoint and traverse down.

  8. DerekNonGeneric commented on Mar 12, 2020

    @DerekNonGeneric
    Contributor

    I'm sorry, can you clarify? I'm aware CJS and ES modules use different algorithms, but module.parent shows undefined in my .mjs graphs anyways, so I was referring to CJS graphs. I'd appreciate a link to spec docs if possible.

  9. devsnek commented on Mar 12, 2020

    @devsnek
    Member

    @DerekNonGeneric like require('x.js') in a.js and require('x.js') in b.js. Both a.js and b.js are parents of x.js, but only the one that ran first would be the module.parent.

  10. DerekNonGeneric commented on Mar 12, 2020

    @DerekNonGeneric
    Contributor

    That's perfectly fine! I'm interested in visualizing the graph as it exists during execution (not conceptually).

  11. devsnek commented on Mar 12, 2020

    @devsnek
    Member

    @DerekNonGeneric ... during execution both a.js and b.js required x.js. a cjs module graph would show a directed edge from a.js to x.js and a directed edge from b.js to x.js.

  12. DerekNonGeneric commented on Mar 12, 2020

    @DerekNonGeneric
    Contributor

    I'm unclear why this information would be useful to a stack trace, but unavailable to userland. I'd have to do more research, but this seems like valuable information is being lost.

  13. devsnek commented on Mar 12, 2020

    @devsnek
    Member

    @DerekNonGeneric the graph structure is preserved (in a more accurate manner) via module.children.

  14. DerekNonGeneric commented on Mar 12, 2020

    @DerekNonGeneric
    Contributor

    @devsnek, thank you. I don't feel the ends justify the means here. It seems like the correct way to go about this would be to deprecate the module that's using the discouraged pattern, not deprecate a runtime feature because people are using it incorrectly.

  15. 21 remaining items

  16. DerekNonGeneric commented on May 6, 2020

    @DerekNonGeneric
    Contributor

    @aduh95, your PR is recommending to use …

    if (require.main === module) {
    }

    … as opposed to …

    if (!module.parent) {
    }

    These may appear to achieve the same desired effect, but that's not the case in
    every circumstance. The following points explain why I'm still not +1 and might
    be worth considering.

    • Because require can be “hijacked” (and often is), none of its behavior can
      be trusted with any degree of certainty. A good example is
      node -r esm.
    • Because module is a “free variable” in the global CommonJS scope, it never
      gets loaded with require(), which may provide more certainty that its
      contents are trustworthy (since require() never had the opportunity to
      tamper w/ it).

    Therefore, if I'm not mistaken, module.parent is currently the only credible
    way to determine whether or not the current module is the root node of the
    module graph.


    Ultimately, I'm not attached to the outcome of the deprecation PR since it's
    just documentation deprecation (for now). However, if the docs deprecation were
    to eventually become runtime deprecation, it seems to me like it would come at
    the expense of module graph observability, which doesn't seem like the intention.

  17. hybrist commented on May 6, 2020

    @hybrist
    Contributor

    Therefore, if I'm not mistaken, module.parent is currently the only credible
    way to determine whether or not the current module is the root node of the
    module graph.

    It's not any more or less credible than every other option. There's no reliability difference (afaik) between require.main and module.parent. Both can be influenced by code running before the module because CommonJS is highly hackable. module.parent is very easy to change by just using Module._load:

    $ cat /tmp/my.cjs
    console.log(module.parent ? "has parent" : "no parent")
    $ node /tmp/my.cjs
    no parent
    $ node -e "require('/tmp/my.cjs')"
    has parent
    $ node -e "require('module')._load('/tmp/my.cjs', null, true)"
    no parent

    You can even modify require.cache['/realpath/to/my.cjs'].parent after the fact and make the parent appear and disappear at runtime.

    it seems to me like it would come at the expense of module graph observability

    I don't agree since require.cache never modeled a full or reliable module graph. And if we ever bring a module graph to ESM in general, it would likely not be in the CommonJS cache. So require.cache will become less and less complete one way or another as people start using ESM features.

  18. aduh95 commented on May 6, 2020

    @aduh95
    Author

    Therefore, if I'm not mistaken, module.parent is currently the only credible
    way to determine whether or not the current module is the root node of the
    module graph.

    I believe process.mainModule achieves the same purpose as well. It's been doc deprecated on v14.0.0 but it's still an alternative.

    Because require can be “hijacked” (and often is), none of its behavior can
    be trusted

    That doesn't seem like a problem we would want to fix, I guess people that do "hijack" require do it for their own reasons. If you want certainty, you shouldn't use code that "hijacks" it, wouldn't you agree?

    Anyway, module.parent has other issues that have been discussed in this thread already, I'm not sure further discussion can be constructive at this point.

  19. added a commit that references this issue on May 24, 2020
  20. added a commit that references this issue on Jun 9, 2020
  21. added a commit that references this issue on Aug 18, 2020
  22. added a commit that references this issue on Aug 18, 2020
  23. added a commit that references this issue on Dec 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions