Skip to content

Commit c6b3399

Browse files
sokraarcanis
andcommitted
add support for Yarn PnP
fixes #168 Co-authored-by: Maël Nison <[email protected]>
1 parent b0e5282 commit c6b3399

File tree

11 files changed

+216
-1
lines changed

11 files changed

+216
-1
lines changed

lib/PnpPlugin.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Maël Nison @arcanis
4+
*/
5+
6+
"use strict";
7+
8+
module.exports = class PnpPlugin {
9+
constructor(source, pnpApi, target) {
10+
this.source = source;
11+
this.pnpApi = pnpApi;
12+
this.target = target;
13+
}
14+
15+
apply(resolver) {
16+
const target = resolver.ensureHook(this.target);
17+
resolver
18+
.getHook(this.source)
19+
.tapAsync("PnpPlugin", (request, resolveContext, callback) => {
20+
const req = request.request;
21+
22+
// The trailing slash indicates to PnP that this value is a folder rather than a file
23+
const issuer = `${request.path}/`;
24+
25+
let resolution;
26+
try {
27+
resolution = this.pnpApi.resolveToUnqualified(req, issuer, {
28+
considerBuiltins: false
29+
});
30+
} catch (error) {
31+
return callback(error);
32+
}
33+
34+
if (resolution === req) return callback();
35+
36+
const obj = {
37+
...request,
38+
path: resolution,
39+
request: undefined,
40+
ignoreSymlinks: true
41+
};
42+
resolver.doResolve(
43+
target,
44+
obj,
45+
`resolved by pnp to ${resolution}`,
46+
resolveContext,
47+
(err, result) => {
48+
if (err) return callback(err);
49+
if (result) return callback(null, result);
50+
// Skip alternatives
51+
return callback(null, null);
52+
}
53+
);
54+
});
55+
}
56+
};

lib/ResolverFactory.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const UseFilePlugin = require("./UseFilePlugin");
2828
const AppendPlugin = require("./AppendPlugin");
2929
const ResultPlugin = require("./ResultPlugin");
3030
const UnsafeCachePlugin = require("./UnsafeCachePlugin");
31+
const PnpPlugin = require("./PnpPlugin");
3132

3233
exports.createResolver = function(options) {
3334
//// OPTIONS ////
@@ -61,6 +62,15 @@ exports.createResolver = function(options) {
6162
// A list of module alias configurations or an object which maps key to value
6263
let alias = options.alias || [];
6364

65+
// A PnP API that should be used - null is "never", undefined is "auto"
66+
const pnpApi =
67+
options.pnpApi === undefined
68+
? process.versions.pnp
69+
? // eslint-disable-next-line node/no-missing-require
70+
require("pnpapi")
71+
: null
72+
: options.pnpApi;
73+
6474
// Resolve symlinks to their symlinked location
6575
const symlinks =
6676
typeof options.symlinks !== "undefined" ? options.symlinks : true;
@@ -203,6 +213,9 @@ exports.createResolver = function(options) {
203213
plugins.push(new JoinRequestPlugin("after-described-resolve", "relative"));
204214

205215
// module
216+
if (pnpApi) {
217+
plugins.push(new PnpPlugin("raw-module", pnpApi, "relative"));
218+
}
206219
modules.forEach(item => {
207220
if (Array.isArray(item))
208221
plugins.push(
@@ -328,7 +341,8 @@ exports.createResolver = function(options) {
328341
plugins.push(new FileExistsPlugin("file", "existing-file"));
329342

330343
// existing-file
331-
if (symlinks) plugins.push(new SymlinkPlugin("existing-file", "relative"));
344+
if (symlinks)
345+
plugins.push(new SymlinkPlugin("existing-file", "existing-file"));
332346
plugins.push(new NextPlugin("existing-file", "resolved"));
333347
}
334348

lib/SymlinkPlugin.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = class SymlinkPlugin {
2020
resolver
2121
.getHook(this.source)
2222
.tapAsync("SymlinkPlugin", (request, resolveContext, callback) => {
23+
if (request.ignoreSymlinks) return callback();
2324
const pathsResult = getPaths(request.path);
2425
const pathSeqments = pathsResult.seqments;
2526
const paths = pathsResult.paths;

test/fixtures/pnp/pkg/dir/index.js

Whitespace-only changes.

test/fixtures/pnp/pkg/index.js

Whitespace-only changes.

test/fixtures/pnp/pkg/main.js

Whitespace-only changes.

test/fixtures/pnp/pkg/package-alias/browser.js

Whitespace-only changes.

test/fixtures/pnp/pkg/package-alias/index.js

Whitespace-only changes.

test/fixtures/pnp/pkg/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"main": "main.js",
3+
"browser": {
4+
"./package-alias/index.js": "./package-alias/browser.js",
5+
"module": "pkg/dir/index"
6+
}
7+
}

test/fixtures/pnp/pkg/typescript/index.ts

Whitespace-only changes.

test/pnp.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
const path = require("path");
2+
const fs = require("fs");
3+
require("should");
4+
const ResolverFactory = require("../lib/ResolverFactory");
5+
const CachedInputFileSystem = require("../lib/CachedInputFileSystem");
6+
7+
const nodeFileSystem = new CachedInputFileSystem(fs, 4000);
8+
9+
const fixture = path.resolve(__dirname, "fixtures", "pnp");
10+
11+
let isAdmin = false;
12+
try {
13+
fs.symlinkSync("dir", path.resolve(fixture, "pkg/symlink"), "dir");
14+
isAdmin = true;
15+
} catch (e) {
16+
// ignore
17+
}
18+
try {
19+
fs.unlinkSync(path.resolve(fixture, "pkg/symlink"));
20+
} catch (e) {
21+
isAdmin = false;
22+
// ignore
23+
}
24+
25+
describe("pnp", () => {
26+
let pnpApi;
27+
let resolver;
28+
if (isAdmin) {
29+
before(() => {
30+
fs.symlinkSync("dir", path.resolve(fixture, "pkg/symlink"), "dir");
31+
});
32+
after(() => {
33+
fs.unlinkSync(path.resolve(fixture, "pkg/symlink"));
34+
});
35+
}
36+
beforeEach(() => {
37+
pnpApi = {
38+
mocks: new Map(),
39+
resolveToUnqualified(request, issuer) {
40+
if (pnpApi.mocks.has(request)) {
41+
return pnpApi.mocks.get(request);
42+
} else {
43+
throw new Error(`No way`);
44+
}
45+
}
46+
};
47+
resolver = ResolverFactory.createResolver({
48+
extensions: [".ts", ".js"],
49+
aliasFields: ["browser"],
50+
fileSystem: nodeFileSystem,
51+
pnpApi
52+
});
53+
});
54+
it("should resolve by going through the pnp api", done => {
55+
pnpApi.mocks.set(
56+
"pkg/dir/index.js",
57+
path.resolve(fixture, "pkg/dir/index.js")
58+
);
59+
resolver.resolve({}, __dirname, "pkg/dir/index.js", {}, (err, result) => {
60+
if (err) return done(err);
61+
result.should.equal(path.resolve(fixture, "pkg/dir/index.js"));
62+
done();
63+
});
64+
});
65+
it("should resolve module names with package.json", done => {
66+
pnpApi.mocks.set("pkg", path.resolve(fixture, "pkg"));
67+
resolver.resolve({}, __dirname, "pkg", {}, (err, result) => {
68+
if (err) return done(err);
69+
result.should.equal(path.resolve(fixture, "pkg/main.js"));
70+
done();
71+
});
72+
});
73+
it("should resolve namespaced module names", done => {
74+
pnpApi.mocks.set("@user/pkg", path.resolve(fixture, "pkg"));
75+
resolver.resolve({}, __dirname, "@user/pkg", {}, (err, result) => {
76+
if (err) return done(err);
77+
result.should.equal(path.resolve(fixture, "pkg/main.js"));
78+
done();
79+
});
80+
});
81+
it(
82+
"should not resolve symlinks",
83+
isAdmin
84+
? done => {
85+
pnpApi.mocks.set("pkg/symlink", path.resolve(fixture, "pkg/symlink"));
86+
resolver.resolve({}, __dirname, "pkg/symlink", {}, (err, result) => {
87+
if (err) return done(err);
88+
result.should.equal(path.resolve(fixture, "pkg/symlink/index.js"));
89+
done();
90+
});
91+
}
92+
: undefined
93+
);
94+
it("should properly deal with other extensions", done => {
95+
pnpApi.mocks.set(
96+
"@user/pkg/typescript",
97+
path.resolve(fixture, "pkg/typescript")
98+
);
99+
resolver.resolve(
100+
{},
101+
__dirname,
102+
"@user/pkg/typescript",
103+
{},
104+
(err, result) => {
105+
if (err) return done(err);
106+
result.should.equal(path.resolve(fixture, "pkg/typescript/index.ts"));
107+
done();
108+
}
109+
);
110+
});
111+
it("should properly deal package.json alias", done => {
112+
pnpApi.mocks.set(
113+
"pkg/package-alias",
114+
path.resolve(fixture, "pkg/package-alias")
115+
);
116+
resolver.resolve({}, __dirname, "pkg/package-alias", {}, (err, result) => {
117+
if (err) return done(err);
118+
result.should.equal(
119+
path.resolve(fixture, "pkg/package-alias/browser.js")
120+
);
121+
done();
122+
});
123+
});
124+
it("should skip normal modules when pnp resolves", done => {
125+
pnpApi.mocks.set("m1/a.js", path.resolve(fixture, "pkg/a.js"));
126+
resolver.resolve(
127+
{},
128+
path.resolve(__dirname, "fixtures"),
129+
"m1/a.js",
130+
{},
131+
(err, result) => {
132+
if (!err) return done(new Error("Resolving should fail"));
133+
done();
134+
}
135+
);
136+
});
137+
});

0 commit comments

Comments
 (0)