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

Support bare internal mappings #30

Open
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
4 participants
@guybedford
Copy link
Contributor

guybedford commented Mar 12, 2019

This PR includes an example and description of bare internal mappings as an extension to the proposal.

This has been discussed before, and opens up opportunities for extending the behaviours to internal requires allowing full control over both internal and external resolutions.

@GeoffreyBooth

This comment has been minimized.

Copy link
Collaborator

GeoffreyBooth commented Mar 12, 2019

Two thoughts, not necessarily deal breakers:

  • How would this interact with the require algorithm? Supercede it?
  • How would this work for deep imports? We would check the package.json for every deep imported file? (I guess we're doing that anyway.)
@jkrems

This comment has been minimized.

Copy link
Owner

jkrems commented Mar 12, 2019

One question: A common pattern in universal packages is to remap an implementation detail using the browser field in package.json. Would the expectation be that we'd encourage them to use a bare specifier for these? Or will we extend the relative path mappings to also apply to $packageBaseURL/$path?

@ljharb

This comment has been minimized.

Copy link

ljharb commented Mar 12, 2019

I think in general, every path should be resolved/normalized at every opportunity, so that you can always use any path anywhere, and they map as one might intuitively expect.

}
}
```

Within the `"exports"` object, the key string after the `'.'` is concatenated on the end of the name field, e.g. `import utc from '@momentjs/moment/timezones/utc'` is formed from `'@momentjs/moment'` + `'/timezones/utc'`. Note that this is string manipulation, not a file path: `"./timezones/utc"` is allowed, but just `"timezones/utc"` is not. The `.` is a placeholder representing the package name. The main entrypoint is therefore the dot string, `".": "./src/moment.mjs"`.

For keys not beginning in `.`, these are mappings which apply internally to any imports made from within the package itself. So `src/moment.mjs` could contain for example `import dep from 'localdep'` which would load from a package-relative location. For example one could define `"moment": "./src/moment.mjs"` allowing the package to refer to itself by name as well in this way. These mappings cannot be used at all from outside of the package.

This comment has been minimized.

@ljharb

ljharb Mar 12, 2019

This seems to preclude being able to remap a file to a node_module. Also what about paths beginning in /?

This comment has been minimized.

@guybedford

guybedford Mar 12, 2019

Author Contributor

Because this mapping operation happens early, it could even be a node_modules package lookup actually.

We could allow arbitrary paths or not. Thoughts?

This comment has been minimized.

@ljharb

ljharb Mar 12, 2019

I actually think this should forbid paths that don't start with ..

npm 6.9 has an alias feature that can be used to remap bare imports - I don't think there's any benefit in us duplicating it.

@guybedford

This comment has been minimized.

Copy link
Contributor Author

guybedford commented Mar 12, 2019

@GeoffreyBooth

How would this interact with the require algorithm? Supercede it?

Yes, it happens as an early step before running the rest of the algorithm normally.

How would this work for deep imports? We would check the package.json for every deep imported file? (I guess we're doing that anyway.)

I'm not sure I follow this question? These apply to imports from within the package to bare names.

@jkrems can you clarify your question? I'm not sure I follow.

@GeoffreyBooth

This comment has been minimized.

Copy link
Collaborator

GeoffreyBooth commented Mar 13, 2019

I’m not sure I follow this question? These apply to imports from within the package to bare names.

If my project has import 'some-package/some-file.js' and inside some-file.js it has import 'foo', you would need to look up some-package/package.json to find the mapping to foo. Which is fine, just pointing out that we need to look up the package.json relative to the file for this to work, rather than the one in the root project. In other words this builds on the package scope concept we’re using for "type".

@jkrems

This comment has been minimized.

Copy link
Owner

jkrems commented Mar 13, 2019

can you clarify your question? I'm not sure I follow.

I think a high level concern is that these entries have different "hoisting" rules. We're hoisting import map entries that start with ./ to the parent scope but keep entries that don't at the package scope.

/project-root
  /package.json - { dependencies: "x" }
/deps
  /x
    /package.json - { exports: /* see example this PR modifies */  }
  /localdep
    /package.json

If we'd generate an import map (ignoring protocol etc. and pretending that this is all at the root), it might look like this:

{
  "scopes": {
    "/project-root": {
        "x": "/deps/x/src/moment.mjs",
        "x/": "/deps/x/src/util/",
        "x/timezones/": "/deps/x/data/timezones/",
        "x/timezones/utc": "/deps/x/data/timezones/utc/index.mjs"
    },
    "/deps/x": {
      "localdep": "/deps/x/third-party/localdep.mjs"
    }
  }
}

To actually come back to the question above ("will we extend the relative path mappings to also apply to $packageBaseURL/$path"), expressed as an import map, it would be the following:

{
  "scopes": {
    "/project-root": {
        "x": "/deps/x/src/moment.mjs",
        "x/": "/deps/x/src/util/",
        "x/timezones/": "/deps/x/data/timezones/",
        "x/timezones/utc": "/deps/x/data/timezones/utc/index.mjs"
    },
    "/deps/x": {
      "localdep": "/deps/x/third-party/localdep.mjs",
      "/deps/x/": "/deps/x/src/util/",
      "/deps/x/timezones/": "/deps/x/data/timezones/",
      "/deps/x/timezones/utc": "/deps/x/data/timezones/utc/index.mjs"
    }
  }
}

I guess I answered by own question, this would likely become super confusing, especially with path remappings.

@guybedford

This comment has been minimized.

Copy link
Contributor Author

guybedford commented Mar 13, 2019

Which is fine, just pointing out that we need to look up the package.json relative to the file for this to work, rather than the one in the root project

Yes exactly this works since we now have a concept of package scope.

I think a high level concern is that these entries have different "hoisting" rules. We're hoisting import map entries that start with ./ to the parent scope but keep entries that don't at the package scope.

I'm not sure I'd consider it as hoisting, but rather just more as thinking in terms of resolution rules for "bare package entry scope resolution" and "internal package scope resolution".

Agree the generated import map would like your example though.

To actually come back to the question above ("will we extend the relative path mappings to also apply to $packageBaseURL/$path"), expressed as an import map, it would be the following:

Thanks that example clearly indicates the question now. Yes I don't think we should do this - the relative mappings should apply only for the bare specifier entry into the package.

@jkrems

This comment has been minimized.

Copy link
Owner

jkrems commented Mar 13, 2019

Trying to apply this to a library that I maintain: https://github.com/groupon/gofer/blob/66dd001669091b9c1ba74e9dc08e08000761e80b/package.json#L8-L9. This is switching out a (non-public) implementation detail based on platform support. This proposal would suggest I'd do this for the standard module system:

{
  "exports": {
    "fetch-binding": ["./lib/fetch.browser.js", "./lib/fetch.js"]
  }
}

With the assumption that I can guard the first entry somehow (e.g. #29). It feels a bit weird to use a bare specifier to "abstract away" an internal override but it may be okay..?

@guybedford

This comment has been minimized.

Copy link
Contributor Author

guybedford commented Mar 13, 2019

Right, so how do we guard the fetch.browser.js? Could we even do ["browser:./lib/fetch.browser.js", "./lib/fetch.js"]?

Note that one of the benefits of the indirection through a bare specifier is exactly ensuring a well-defined non-recursive resolver, as if we had relative paths become LHS entries in the map we would be introducing recursion :)

@jkrems

This comment has been minimized.

Copy link
Owner

jkrems commented Mar 13, 2019

I think people will (rightfully?) complain about the string-based meta data. But I might also just be biased by NIH and lean towards the earlier [{ "browser": true, "from": "./lib/fetch.browser.js" }, "./lib/fetch.js"] strawman. But I think that no matter what we found multiple possible syntax solutions there so I think it's a problem of agreeing on one, not finding a workable solutions. I could live with "pseudo-protocols" even though they may interfere with real protocols.

@guybedford

This comment has been minimized.

Copy link
Contributor Author

guybedford commented Mar 13, 2019

My hope with conditionals is that we can not get too caught up in trying to craft the perfect generalization of environment detection (which is also a very difficult optimization IMO), but can rather focus on solving the use cases of having different entry points between modules browsers, legacy browsers, modules Node, legacy Node, and production and development environments can be done which I still see primarily as a combinatorial problem of the "main" / "module" / "browser" mains problem applied to arbitrary entry points.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.