webpack / enhanced-resolve

Offers an async require.resolve function. It's highly configurable.
MIT License
928 stars 186 forks source link

resolving packages entrypoints with a non-standard node_modules #382

Closed Galcarmi closed 6 months ago

Galcarmi commented 1 year ago

Hey guys,

I have a special case where we install npm packages on an external "hard-drive", it has a unique path and every package is stored also in a unique path inside the drive. let's say lodash is installed on this drive, the path of it will be externalHardDriveUrI/lodash/4.0.0/lodash/package-contents.

I'm using Webpack to bundle my project. It cant be bundled properly because of the obvious reason of the no-existing node_modules folder. I cant change this fact of the special external node_modules folder and it has many reasons why it exists in that way. I have a function that given an npm name it locates the exact path of the npm package. (i.e. lodash -> externalHardDriveUrI/lodash/4.0.0/lodash/package-contents)

I need somehow to resolve the npm packages entrypoints by webpack target (i also need that it will support the exports field property in package.json)

(given externalHardDriveUrI/lodash/4.0.0/lodash/package.json and webpack target = worker -> externalHardDriveUrI/lodash/4.0.0/lodash/index-worker.js

I've tried to create a custom webpack resolver (which is enhanced-resolve instance AFAIU) but i can't make it work, I've tried to resolve the package entrypoint in many ways but non of them worked.

Can enhanced-resolve support my use case?

This is an example of what i've tried (i tried many more things like changing the path etc..):

class CustomResolverPlugin {
  _isAbsoluteOrRelativePath(request) {
    return request.startsWith("/") || request.startsWith("./") || request.startsWith("..");
  }

  apply(resolver) {
    const hookToFireOnceFinished = resolver.ensureHook("resolve");

    resolver
      .getHook("resolve")
      .tapAsync("NpmResolverPlugin", (resolveRequest, resolveContext, callback) => {
        const { request, pathAlreadyResolved } = resolveRequest;
        debugger

        if (this._isAbsoluteOrRelativePath(request) || pathAlreadyResolved) {
          callback();

          return;
        }

        try {
            // new request is an absolute path to package root
            const newRequest = getPackageRoot(request);
            resolver.doResolve(
                    hookToFireOnceFinished,
                    {...resolveRequest, path, request:newRequest, pathAlreadyResolved: true} ,
                    null,
                    resolveContext,
                    callback
                  );
        } catch {
          callback();
        }
      });

am I missing something? can enhance-resolve handle non-standard node_modules dir? are you aware of other ways of solving my use case?

alexander-akait commented 1 year ago

Can you create small github repo and provide structure, I need to undestand strcuture and how you are importing this in code, thank you

Galcarmi commented 1 year ago

Hey @alexander-akait thanks for responding 🙏🏽, Here's a minimal reproducible example: https://github.com/Galcarmi/WebpackResolvingIssue I added a detailed readme file

edit: oops, forgot to add the dist folders, fixed it now in the example

alexander-akait commented 1 year ago

@Galcarmi After looking at your code I want to say - your structure is not consistent, top level packages have one logic, inner packages have another, anyway there is an example how you can solve it:

class CustomResolverPlugin {
  _isAbsoluteOrRelativePath(request) {
    return request.startsWith("/") || request.startsWith("./") || request.startsWith("..");
  }

  apply(resolver) {
    const source = resolver.ensureHook("raw-module");
    const target = resolver.ensureHook("undescribed-resolve-in-package");

    resolver
      .getHook(source)
      .tapAsync("NpmResolverPlugin", (resolveRequest, resolveContext, callback) => {
        const { request } = resolveRequest;

        if (this._isAbsoluteOrRelativePath(request)) {
          callback();

          return;
        }

          const packageMatch = /^(@[^/]+\/)?[^/]+/.exec(request);
          if (!packageMatch) return callback();
          const packageName = packageMatch[0];
          const innerInternalRequest = request.slice(packageName.length);

          const innerRequest = innerInternalRequest.length === 0 ? `.${innerInternalRequest}` : ".";

        try {
            const modulePath = path.join(__dirname, 'external_modules', 'current_version_start', packageName, 'current_version_end', packageName, innerInternalRequest);

            const obj = {
                ...resolveRequest,
                path: modulePath,
                fullySpecified: request.fullySpecified && innerRequest !== ".",
                request: innerRequest,
                module: false
            };

            resolver.doResolve(target, obj, null, resolveContext, callback);
        } catch {
          callback();
        }
      });
  }
}

Note - it is just an example, some things are missing - you need to use this logic https://github.com/webpack/enhanced-resolve/blob/main/lib/ModulesInHierarchicalDirectoriesPlugin.js#L49 and check is it directory, if not - add missing dependecies, otherwise in the watch mode if you add a new package, watcher will not triggered

Galcarmi commented 1 year ago

@alexander-akait thanks 🙏🏽🙏🏽🙏🏽 i'll try it