diff --git a/lib/PnpPlugin.js b/lib/PnpPlugin.js new file mode 100644 index 00000000..f4eb8bf8 --- /dev/null +++ b/lib/PnpPlugin.js @@ -0,0 +1,40 @@ +"use strict"; + +module.exports = class PnpPlugin { + constructor(source, pnpApi, target) { + this.source = source; + this.pnpApi = pnpApi; + this.target = target; + } + + apply(resolver) { + const target = resolver.ensureHook(this.target); + resolver + .getHook(this.source) + .tapAsync("PnpPlugin", (requestContext, resolveContext, callback) => { + const request = requestContext.request; + + // The trailing slash indicates to PnP that this value is a folder rather than a file + const issuer = `${requestContext.path}/`; + + let resolution; + try { + resolution = this.pnpApi.resolveToUnqualified(request, issuer, { + considerBuiltins: false + }); + } catch (error) { + return callback(error); + } + + resolver.doResolve( + target, + Object.assign({}, requestContext, { + request: resolution + }), + null, + resolveContext, + callback + ); + }); + } +}; diff --git a/lib/ResolverFactory.js b/lib/ResolverFactory.js index d148413c..6ca8cd34 100644 --- a/lib/ResolverFactory.js +++ b/lib/ResolverFactory.js @@ -28,6 +28,7 @@ const AppendPlugin = require("./AppendPlugin"); const ResultPlugin = require("./ResultPlugin"); const ModuleAppendPlugin = require("./ModuleAppendPlugin"); const UnsafeCachePlugin = require("./UnsafeCachePlugin"); +const PnpPlugin = require("./PnpPlugin"); exports.createResolver = function(options) { //// OPTIONS //// @@ -67,9 +68,23 @@ exports.createResolver = function(options) { // A list of module alias configurations or an object which maps key to value let alias = options.alias || []; + // A PnP API that should be used - null is "never", undefined is "auto" + const pnpApi = + typeof options.pnpApi === "undefined" + ? process.versions.pnp + ? require("pnpapi") // eslint-disable-line node/no-missing-require + : null + : options.pnpApi; + // Resolve symlinks to their symlinked location const symlinks = - typeof options.symlinks !== "undefined" ? options.symlinks : true; + typeof options.symlinks !== "undefined" + ? options.symlinks + : // PnP currently needs symlinks to be preserved to resolve peer dependencies + // Ref: https://github.com/webpack/enhanced-resolve/pull/168/files#r268819172 + pnpApi + ? false + : true; // Resolve to a context instead of a file const resolveToContext = options.resolveToContext || false; @@ -215,6 +230,7 @@ exports.createResolver = function(options) { plugins.push(new TryNextPlugin("raw-module", null, "module")); // module + if (pnpApi) plugins.push(new PnpPlugin("before-module", pnpApi, "resolve")); modules.forEach(item => { if (Array.isArray(item)) plugins.push( diff --git a/test/fixtures/pnp/pkg/dir/index.js b/test/fixtures/pnp/pkg/dir/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/pnp/pkg/index.js b/test/fixtures/pnp/pkg/index.js new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/pnp/pkg/symlink b/test/fixtures/pnp/pkg/symlink new file mode 120000 index 00000000..87245193 --- /dev/null +++ b/test/fixtures/pnp/pkg/symlink @@ -0,0 +1 @@ +dir \ No newline at end of file diff --git a/test/fixtures/pnp/pkg/typescript/index.ts b/test/fixtures/pnp/pkg/typescript/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/pnp.js b/test/pnp.js new file mode 100644 index 00000000..cdc08305 --- /dev/null +++ b/test/pnp.js @@ -0,0 +1,67 @@ +var path = require("path"); +require("should"); +var ResolverFactory = require("../lib/ResolverFactory"); +var NodeJsInputFileSystem = require("../lib/NodeJsInputFileSystem"); +var CachedInputFileSystem = require("../lib/CachedInputFileSystem"); + +var pnpApi = { + mocks: new Map(), + resolveToUnqualified(request, issuer) { + if (pnpApi.mocks.has(request)) { + return pnpApi.mocks.get(request); + } else { + throw new Error(`No way`); + } + } +}; + +var nodeFileSystem = new CachedInputFileSystem( + new NodeJsInputFileSystem(), + 4000 +); + +var resolver = ResolverFactory.createResolver({ + extensions: [".ts", ".js"], + fileSystem: nodeFileSystem, + pnpApi +}); + +var fixture = path.resolve(__dirname, "fixtures", "pnp"); + +describe("extensions", function() { + it("should resolve by going through the pnp api", function(done) { + pnpApi.mocks.set( + "pkg/dir/index.js", + path.resolve(fixture, "pkg/dir/index.js") + ); + resolver.resolve({}, fixture, "pkg/dir/index.js", {}, function( + err, + result + ) { + console.log(err); + result.should.equal(path.resolve(fixture, "pkg/dir/index.js")); + done(); + }); + }); + it("should resolve module names", function(done) { + pnpApi.mocks.set("pkg", path.resolve(fixture, "pkg")); + resolver.resolve({}, fixture, "pkg", {}, function(err, result) { + result.should.equal(path.resolve(fixture, "pkg/index.js")); + done(); + }); + }); + it("should not resolve symlinks", function(done) { + pnpApi.mocks.set("pkg/symlink", path.resolve(fixture, "pkg/symlink")); + resolver.resolve({}, fixture, "pkg/symlink", {}, function(err, result) { + result.should.equal(path.resolve(fixture, "pkg/symlink/index.js")); + done(); + }); + }); + it("should properly deal with other extensions", function(done) { + pnpApi.mocks.set("pkg/typescript", path.resolve(fixture, "pkg/typescript")); + resolver.resolve({}, fixture, "pkg/typescript", {}, function(err, result) { + result.should.equal(path.resolve(fixture, "pkg/typescript/index.ts")); + done(); + }); + }); +});