Skip to content

Commit

Permalink
add support for Yarn PnP
Browse files Browse the repository at this point in the history
fixes #168

Co-authored-by: Maël Nison <nison.mael@gmail.com>
  • Loading branch information
sokra and arcanis committed Jul 4, 2019
1 parent b0e5282 commit c6b3399
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 1 deletion.
56 changes: 56 additions & 0 deletions lib/PnpPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Maël Nison @arcanis
*/

"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", (request, resolveContext, callback) => {
const req = request.request;

// The trailing slash indicates to PnP that this value is a folder rather than a file
const issuer = `${request.path}/`;

let resolution;
try {
resolution = this.pnpApi.resolveToUnqualified(req, issuer, {
considerBuiltins: false
});
} catch (error) {
return callback(error);
}

if (resolution === req) return callback();

const obj = {
...request,
path: resolution,
request: undefined,
ignoreSymlinks: true
};
resolver.doResolve(
target,
obj,
`resolved by pnp to ${resolution}`,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
// Skip alternatives
return callback(null, null);
}
);
});
}
};
16 changes: 15 additions & 1 deletion lib/ResolverFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const UseFilePlugin = require("./UseFilePlugin");
const AppendPlugin = require("./AppendPlugin");
const ResultPlugin = require("./ResultPlugin");
const UnsafeCachePlugin = require("./UnsafeCachePlugin");
const PnpPlugin = require("./PnpPlugin");

exports.createResolver = function(options) {
//// OPTIONS ////
Expand Down Expand Up @@ -61,6 +62,15 @@ 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 =
options.pnpApi === undefined
? process.versions.pnp
? // eslint-disable-next-line node/no-missing-require
require("pnpapi")
: null
: options.pnpApi;

// Resolve symlinks to their symlinked location
const symlinks =
typeof options.symlinks !== "undefined" ? options.symlinks : true;
Expand Down Expand Up @@ -203,6 +213,9 @@ exports.createResolver = function(options) {
plugins.push(new JoinRequestPlugin("after-described-resolve", "relative"));

// module
if (pnpApi) {
plugins.push(new PnpPlugin("raw-module", pnpApi, "relative"));
}
modules.forEach(item => {
if (Array.isArray(item))
plugins.push(
Expand Down Expand Up @@ -328,7 +341,8 @@ exports.createResolver = function(options) {
plugins.push(new FileExistsPlugin("file", "existing-file"));

// existing-file
if (symlinks) plugins.push(new SymlinkPlugin("existing-file", "relative"));
if (symlinks)
plugins.push(new SymlinkPlugin("existing-file", "existing-file"));
plugins.push(new NextPlugin("existing-file", "resolved"));
}

Expand Down
1 change: 1 addition & 0 deletions lib/SymlinkPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = class SymlinkPlugin {
resolver
.getHook(this.source)
.tapAsync("SymlinkPlugin", (request, resolveContext, callback) => {
if (request.ignoreSymlinks) return callback();
const pathsResult = getPaths(request.path);
const pathSeqments = pathsResult.seqments;
const paths = pathsResult.paths;
Expand Down
Empty file.
Empty file added test/fixtures/pnp/pkg/index.js
Empty file.
Empty file added test/fixtures/pnp/pkg/main.js
Empty file.
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions test/fixtures/pnp/pkg/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"main": "main.js",
"browser": {
"./package-alias/index.js": "./package-alias/browser.js",
"module": "pkg/dir/index"
}
}
Empty file.
137 changes: 137 additions & 0 deletions test/pnp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const path = require("path");
const fs = require("fs");
require("should");
const ResolverFactory = require("../lib/ResolverFactory");
const CachedInputFileSystem = require("../lib/CachedInputFileSystem");

const nodeFileSystem = new CachedInputFileSystem(fs, 4000);

const fixture = path.resolve(__dirname, "fixtures", "pnp");

let isAdmin = false;
try {
fs.symlinkSync("dir", path.resolve(fixture, "pkg/symlink"), "dir");
isAdmin = true;
} catch (e) {
// ignore
}
try {
fs.unlinkSync(path.resolve(fixture, "pkg/symlink"));
} catch (e) {
isAdmin = false;
// ignore
}

describe("pnp", () => {
let pnpApi;
let resolver;
if (isAdmin) {
before(() => {
fs.symlinkSync("dir", path.resolve(fixture, "pkg/symlink"), "dir");
});
after(() => {
fs.unlinkSync(path.resolve(fixture, "pkg/symlink"));
});
}
beforeEach(() => {
pnpApi = {
mocks: new Map(),
resolveToUnqualified(request, issuer) {
if (pnpApi.mocks.has(request)) {
return pnpApi.mocks.get(request);
} else {
throw new Error(`No way`);
}
}
};
resolver = ResolverFactory.createResolver({
extensions: [".ts", ".js"],
aliasFields: ["browser"],
fileSystem: nodeFileSystem,
pnpApi
});
});
it("should resolve by going through the pnp api", done => {
pnpApi.mocks.set(
"pkg/dir/index.js",
path.resolve(fixture, "pkg/dir/index.js")
);
resolver.resolve({}, __dirname, "pkg/dir/index.js", {}, (err, result) => {
if (err) return done(err);
result.should.equal(path.resolve(fixture, "pkg/dir/index.js"));
done();
});
});
it("should resolve module names with package.json", done => {
pnpApi.mocks.set("pkg", path.resolve(fixture, "pkg"));
resolver.resolve({}, __dirname, "pkg", {}, (err, result) => {
if (err) return done(err);
result.should.equal(path.resolve(fixture, "pkg/main.js"));
done();
});
});
it("should resolve namespaced module names", done => {
pnpApi.mocks.set("@user/pkg", path.resolve(fixture, "pkg"));
resolver.resolve({}, __dirname, "@user/pkg", {}, (err, result) => {
if (err) return done(err);
result.should.equal(path.resolve(fixture, "pkg/main.js"));
done();
});
});
it(
"should not resolve symlinks",
isAdmin
? done => {
pnpApi.mocks.set("pkg/symlink", path.resolve(fixture, "pkg/symlink"));
resolver.resolve({}, __dirname, "pkg/symlink", {}, (err, result) => {
if (err) return done(err);
result.should.equal(path.resolve(fixture, "pkg/symlink/index.js"));
done();
});
}
: undefined
);
it("should properly deal with other extensions", done => {
pnpApi.mocks.set(
"@user/pkg/typescript",
path.resolve(fixture, "pkg/typescript")
);
resolver.resolve(
{},
__dirname,
"@user/pkg/typescript",
{},
(err, result) => {
if (err) return done(err);
result.should.equal(path.resolve(fixture, "pkg/typescript/index.ts"));
done();
}
);
});
it("should properly deal package.json alias", done => {
pnpApi.mocks.set(
"pkg/package-alias",
path.resolve(fixture, "pkg/package-alias")
);
resolver.resolve({}, __dirname, "pkg/package-alias", {}, (err, result) => {
if (err) return done(err);
result.should.equal(
path.resolve(fixture, "pkg/package-alias/browser.js")
);
done();
});
});
it("should skip normal modules when pnp resolves", done => {
pnpApi.mocks.set("m1/a.js", path.resolve(fixture, "pkg/a.js"));
resolver.resolve(
{},
path.resolve(__dirname, "fixtures"),
"m1/a.js",
{},
(err, result) => {
if (!err) return done(new Error("Resolving should fail"));
done();
}
);
});
});

0 comments on commit c6b3399

Please sign in to comment.