nodejs / node

Node.js JavaScript runtime βœ¨πŸ’πŸš€βœ¨
https://nodejs.org
Other
106.46k stars 29.01k forks source link

createRequire should have a createImport equivalent #30645

Closed jkrems closed 4 years ago

jkrems commented 4 years ago

Is your feature request related to a problem? Please describe.

Right now we can easily require files relative to a given location. The same isn't possible for import. One possible use case is "import third party tool X relative to the working directory from within a globally installed CLI tool".

Example: https://github.com/zeit/now/blob/7e75d8c1a3485b7121f919e5035dc174ae9d629a/packages/now-next/src/dev-server.ts#L14

Describe the solution you'd like

import {createImport} from 'module';
import {pathToFileURL} from 'url';
import {resolve as resolvePath} from 'path';

const $import = createImport(pathToFileURL(resolvePath('.app'));
const {default as next} = await $import('next');
$import.meta.url; // file:///path/to/cwd/.app

Note: $import.meta exposes the same API as the usual import.meta but would get a clean copy. It would not be the same object as the meta object for file:///path/to/cwd/.app if a module with that URL actually exists.

Describe alternatives you've considered

One alternative would be to use require for resolution but that has the downside of not matching what import resolution would do. It would also not give access to the import.meta APIs.

import {createRequire} from 'module';
import {fileURLToPath, pathToFileURL} from 'url';
import {resolve as resolvePath} from 'path';

function createImport(base) {
  const req = createRequire(fileURLToPath(base));
  return function __import(specifier) {
    return import(pathToFileURL(req.resolve(specifier)));
  };
}

const $import = createImport(pathToFileURL(resolvePath('.app'));
const {default as next} = await $import('next');
jkrems commented 4 years ago

/cc @nodejs/modules-active-members FYI

ljharb commented 4 years ago

with require, you can use path functions to alter the specifier you pass in, so you don't necessarily need createRequire; is this an issue because there's no similar URL navigation functions for use in import()?

devsnek commented 4 years ago

createRequire exists because it's the only way to get access to a require without creating a cjs module, but import() exists everywhere. It seems to me like what we actually need is an exposed loader.resolve.

jkrems commented 4 years ago

It seems to me like what we actually need is an exposed loader.resolve.

Sure, we could have a loader.resolve instead but it would be a bit more verbose in what I expect to be the more common case (load relative to). It would also imply exposing a loader. Maybe {url,module}.urlFromSpecifier(specifier, base) would be easier to add?

jkrems commented 4 years ago

Also, import.meta may be expanded in the future. In which case the local import plus resolveSpecifier isn't enough to get that data.

devsnek commented 4 years ago

Yeah there are a lot of options, I just dislike curried import as a core API. More generally I'm thinking of patterns like import(z.resolve(x, y)) or z.import(x, y).

guybedford commented 4 years ago

import.meta.resolve has been long-considered a web possibility in future. The key question there is sync or async, which has ramifications for our resolver and loaders as well. As I've said before this is an area where Node.js may have to make the first move with the web to follow on compat, as opposed to the usual pattern where we can follow web specs as the source-of-truth as it were. But some collaboration may be needed on this.

bmeck commented 4 years ago

I'd be ok with an async import.meta.resolve but have concerns about it being synchronous. Additionally, whatever the API, it should be made so that it doesn't appear to have problems if https://github.com/sebmarkbage/ecmascript-asset-references moves forward.

jkrems commented 4 years ago

One thing I'd like to bring up again is that the issue covers both relative import and getting access to an equivalent import.meta object. The latter isn't super valuable right now (since we only expose .url) but may be more valuable in the future if we keep adding things to the meta object. If it would be just a resolution API, I'd vote for new URL('import:x', base), betting hard on sync/static resolution.

devsnek commented 4 years ago

if you want an import.meta object I think you should need a reified source text module. we shouldn't be creating fake ones.

SMotaal commented 4 years ago

So, I think that we want to distinguish finer details about createImport, it is not clear cut as createRequire:

  1. createImport as a direct first-class conforming helper would be with (module.scope) { eval('specifier => import(specifier)') } (theoretical).

  2. createImport as an indirect/symbolic first-class conforming helper would be referrer => specifier => import(resolveModuleURL(specifier, referrer)) where resolveModuleURL would be the wrapper for the host resolver behaviour from the referrer.

Going from the theoretical (1) to the practical (2) can outline some of the concerns about passing around things like import.meta.resolve.

Instead, I'd like to find a lower-level way to expose the theoretical one, ie you cannot import cleanly per-specs if you are doing it on behalf of a module before or without that module being one β€” at least not for first-class instrumentation/tooling code (unless it wants not to be).

So once we have the module, we want to marshal calls from all related createImport() functions through it, and that is as simple as exposing (internally at least) the evaluated wrapper inside the scope (ie after binding).

SMotaal commented 4 years ago

What we can do is consider createImportResolver and createRequireResolver.

Update β€” I am backtracking here a bit because host resolve and host import are separated out for a good reason, if you are going to import(createImportResolver(r)(s)) you are not importing r but you are also not accounting for the effects of doing so.

Update β€” It also helps to consider if this resolver would be wired to the actual resolver instance (ie affect state for subsequent import) or not.

SMotaal commented 4 years ago

I'd vote for new URL('import:x', base), betting hard on sync/static resolution

@jkrems worth noting that the way new URL(…) behaves about schemes will take some work, we want to resolve against file: or http: first and then swap the prefix: we want in place β€” I'm concerned here with a little more than Chromium's URL implementation.

Note β€” May be this becomes a little relevant β€” sorry for the hard read

weswigham commented 4 years ago

if you want an import.meta object I think you should need a reified source text module.

Can you produce a complete one with the VM library? (A runInThisContext for modules, kinda?) The APIs I've seen all seem to expect the caller to provide the import meta and dynamic import implementation... But if you can, you could always just make a module with that (at a similar path to the desired url), and then have it export a wrapper around its import/import.meta?

SMotaal commented 4 years ago

@weswigham I think the issue here is that import.meta implies an import should take place (or at least that early errors don't prevent it) and that is assuming too much about things (at least if we are considering import.meta to become spec at some point, unless I'm not looking in the right place(s), it is not).

Fair to note, it not being that means we are not really breaking the spec, but I am not sure it is favourable to be too literal here (even I would see the value of not being literal here).

devsnek commented 4 years ago

can y'all expand on why you need the meta object from a source text module? it seems like a rather random request from my perspective, so more info would help.

jkrems commented 4 years ago

It's the most likely place to put APIs that are module-relative. It's definitely speculative at this point. If we never add any interesting APIs to import.meta, it won't matter.

guybedford commented 4 years ago

I strongly disagree there is a need for a new dynamic import, and we also have the ability to achieve the same goals via import.meta.resolve now.