Skip to content

Commit ba5bd92

Browse files
authored
Merge pull request #220 from webpack/dev-1
Merge exports field development into master
2 parents 2fb9a07 + 6b6766f commit ba5bd92

File tree

34 files changed

+2224
-36
lines changed

34 files changed

+2224
-36
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,11 @@ myResolver.resolve({}, lookupStartPath, request, resolveContext, (
8181
| alias | [] | A list of module alias configurations or an object which maps key to value |
8282
| aliasFields | [] | A list of alias fields in description files |
8383
| cacheWithContext | true | If unsafe cache is enabled, includes `request.context` in the cache key |
84+
| conditionNames | ["node"] | A list of exports field condition names |
8485
| descriptionFiles | ["package.json"] | A list of description files to read from |
8586
| enforceExtension | false | Enforce that a extension from extensions must be used |
8687
| extensions | [".js", ".json", ".node"] | A list of extensions which should be tried for files |
88+
| exportsFields | ["exports"] | A list of exports fields in description files |
8789
| mainFields | ["main"] | A list of main fields in description files |
8890
| mainFiles | ["index"] | A list of main files in directories |
8991
| modules | ["node_modules"] | A list of directories to resolve modules from, can be absolute path or folder name |

lib/DescriptionFileUtils.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,9 @@ function getField(content, field) {
146146
}
147147
current = current[field[j]];
148148
}
149-
if (typeof current === "object") {
150-
return current;
151-
}
149+
return current;
152150
} else {
153-
if (typeof content[field] === "object") {
154-
return content[field];
155-
}
151+
return content[field];
156152
}
157153
}
158154

lib/ExportsFieldPlugin.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
MIT License http://www.opensource.org/licenses/mit-license.php
3+
Author Ivan Kopeykin @vankop
4+
*/
5+
6+
"use strict";
7+
8+
const path = require("path");
9+
const DescriptionFileUtils = require("./DescriptionFileUtils");
10+
const forEachBail = require("./forEachBail");
11+
const { checkExportsFieldTarget } = require("./pathUtils");
12+
const processExportsField = require("./processExportsField");
13+
14+
/** @typedef {import("./Resolver")} Resolver */
15+
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
16+
/** @typedef {import("./processExportsField").ExportsField} ExportsField */
17+
/** @typedef {import("./processExportsField").ExportsFieldProcessor} ExportsFieldProcessor */
18+
19+
module.exports = class ExportsFieldPlugin {
20+
/**
21+
* @param {string | ResolveStepHook} source source
22+
* @param {Set<string>} conditionNames condition names
23+
* @param {string | string[]} fieldNamePath name path
24+
* @param {string | ResolveStepHook} target target
25+
*/
26+
constructor(source, conditionNames, fieldNamePath, target) {
27+
this.source = source;
28+
this.target = target;
29+
this.conditionNames = conditionNames;
30+
this.fieldName = fieldNamePath;
31+
/** @type {WeakMap<any, ExportsFieldProcessor>} */
32+
this.exportsFieldProcessorCache = new WeakMap();
33+
}
34+
35+
/**
36+
* @param {Resolver} resolver the resolver
37+
* @returns {void}
38+
*/
39+
apply(resolver) {
40+
const target = resolver.ensureHook(this.target);
41+
resolver
42+
.getHook(this.source)
43+
.tapAsync("ExportsFieldPlugin", (request, resolveContext, callback) => {
44+
// When there is no description file, abort
45+
if (!request.descriptionFilePath) return callback();
46+
// When the description file is inherited from parent, abort
47+
// (There is no description file inside of this package)
48+
if (request.relativePath !== ".") return callback();
49+
50+
const remainingRequest = request.request;
51+
if (remainingRequest === undefined) return callback();
52+
53+
/** @type {ExportsField|null} */
54+
const exportsField = DescriptionFileUtils.getField(
55+
request.descriptionFileData,
56+
this.fieldName
57+
);
58+
if (!exportsField) return callback();
59+
60+
if (request.directory) {
61+
return callback(
62+
new Error(
63+
`Resolving to directories is not possible with the exports field (request was ${remainingRequest}/)`
64+
)
65+
);
66+
}
67+
68+
let paths;
69+
70+
try {
71+
// We attach the cache to the description file instead of the exportsField value
72+
// because we use a WeakMap and the exportsField could be a string too.
73+
// Description file is always an object when exports field can be accessed.
74+
let exportFieldProcessor = this.exportsFieldProcessorCache.get(
75+
request.descriptionFileData
76+
);
77+
if (exportFieldProcessor === undefined) {
78+
exportFieldProcessor = processExportsField(exportsField);
79+
this.exportsFieldProcessorCache.set(
80+
request.descriptionFileData,
81+
exportFieldProcessor
82+
);
83+
}
84+
paths = exportFieldProcessor(remainingRequest, this.conditionNames);
85+
} catch (err) {
86+
if (resolveContext.log) {
87+
resolveContext.log(
88+
`Exports field in ${request.descriptionFilePath} can't be processed: ${err}`
89+
);
90+
}
91+
return callback(err);
92+
}
93+
94+
if (paths.length === 0) {
95+
return callback(
96+
new Error(
97+
`Package path ${remainingRequest} is not exported from package ${request.descriptionFileRoot} (see exports field in ${request.descriptionFilePath})`
98+
)
99+
);
100+
}
101+
102+
forEachBail(
103+
paths,
104+
(p, callback) => {
105+
const error = checkExportsFieldTarget(p);
106+
107+
if (error) {
108+
return callback(error);
109+
}
110+
111+
const obj = {
112+
...request,
113+
request: undefined,
114+
path: path.join(
115+
/** @type {string} */ (request.descriptionFileRoot),
116+
p
117+
),
118+
relativePath: p
119+
};
120+
121+
resolver.doResolve(
122+
target,
123+
obj,
124+
"using exports field: " + p,
125+
resolveContext,
126+
callback
127+
);
128+
},
129+
(err, result) => callback(err, result || null)
130+
);
131+
});
132+
}
133+
};

lib/JoinRequestPartPlugin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ module.exports = class JoinRequestPartPlugin {
4141
let moduleName, remainingRequest;
4242
if (i < 0) {
4343
moduleName = req;
44-
remainingRequest = "";
44+
remainingRequest = ".";
4545
} else {
4646
moduleName = req.slice(0, i);
4747
remainingRequest = "." + req.slice(i);

lib/MainFieldPlugin.js

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"use strict";
77

88
const path = require("path");
9+
const DescriptionFileUtils = require("./DescriptionFileUtils");
910

1011
/** @typedef {import("./Resolver")} Resolver */
1112
/** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
@@ -40,28 +41,18 @@ module.exports = class MainFieldPlugin {
4041
!request.descriptionFilePath
4142
)
4243
return callback();
43-
const content = request.descriptionFileData;
4444
const filename = path.basename(request.descriptionFilePath);
45-
let mainModule;
46-
const field = this.options.name;
47-
if (Array.isArray(field)) {
48-
let current = content;
49-
for (let j = 0; j < field.length; j++) {
50-
if (current === null || typeof current !== "object") {
51-
current = null;
52-
break;
53-
}
54-
current = current[field[j]];
55-
}
56-
if (typeof current === "string") {
57-
mainModule = current;
58-
}
59-
} else {
60-
if (typeof content[field] === "string") {
61-
mainModule = content[field];
62-
}
63-
}
64-
if (!mainModule || mainModule === "." || mainModule === "./") {
45+
let mainModule = DescriptionFileUtils.getField(
46+
request.descriptionFileData,
47+
this.options.name
48+
);
49+
50+
if (
51+
!mainModule ||
52+
typeof mainModule !== "string" ||
53+
mainModule === "." ||
54+
mainModule === "./"
55+
) {
6556
return callback();
6657
}
6758
if (this.options.forceRelative && !/^\.\.?\//.test(mainModule))

lib/ResolverFactory.js

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const AliasPlugin = require("./AliasPlugin");
1515
const AppendPlugin = require("./AppendPlugin");
1616
const DescriptionFilePlugin = require("./DescriptionFilePlugin");
1717
const DirectoryExistsPlugin = require("./DirectoryExistsPlugin");
18+
const ExportsFieldPlugin = require("./ExportsFieldPlugin");
1819
const FileExistsPlugin = require("./FileExistsPlugin");
1920
const FileKindPlugin = require("./FileKindPlugin");
2021
const JoinRequestPartPlugin = require("./JoinRequestPartPlugin");
@@ -27,6 +28,7 @@ const NextPlugin = require("./NextPlugin");
2728
const ParsePlugin = require("./ParsePlugin");
2829
const PnpPlugin = require("./PnpPlugin");
2930
const ResultPlugin = require("./ResultPlugin");
31+
const SelfReferencePlugin = require("./SelfReferencePlugin");
3032
const SymlinkPlugin = require("./SymlinkPlugin");
3133
const TryNextPlugin = require("./TryNextPlugin");
3234
const UnsafeCachePlugin = require("./UnsafeCachePlugin");
@@ -48,7 +50,9 @@ const UseFilePlugin = require("./UseFilePlugin");
4850
* @property {(function(ResolveRequest): boolean)=} cachePredicate A function which decides whether a request should be cached or not. An object is passed with at least `path` and `request` properties.
4951
* @property {boolean=} cacheWithContext Whether or not the unsafeCache should include request context as part of the cache key.
5052
* @property {string[]=} descriptionFiles A list of description files to read from
53+
* @property {string[]=} conditionNames A list of exports field condition names.
5154
* @property {boolean=} enforceExtension Enforce that a extension from extensions must be used
55+
* @property {(string | string[])[]=} exportsFields A list of exports fields in description files
5256
* @property {string[]=} extensions A list of extensions which should be tried for files
5357
* @property {FileSystem} fileSystem The file system which should be used
5458
* @property {(Object | boolean)=} unsafeCache Use this cache object to unsafely cache the successful requests
@@ -69,8 +73,10 @@ const UseFilePlugin = require("./UseFilePlugin");
6973
* @property {Set<string | string[]>} aliasFields
7074
* @property {(function(ResolveRequest): boolean)} cachePredicate
7175
* @property {boolean} cacheWithContext
76+
* @property {Set<string>} conditionNames A list of exports field condition names.
7277
* @property {string[]} descriptionFiles
7378
* @property {boolean} enforceExtension
79+
* @property {Set<string | string[]>} exportsFields
7480
* @property {Set<string>} extensions
7581
* @property {FileSystem} fileSystem
7682
* @property {Object | false} unsafeCache
@@ -144,6 +150,8 @@ function createOptions(options) {
144150
typeof options.cacheWithContext !== "undefined"
145151
? options.cacheWithContext
146152
: true,
153+
exportsFields: new Set(options.exportsFields || ["exports"]),
154+
conditionNames: new Set(options.conditionNames),
147155
descriptionFiles: Array.from(
148156
new Set(options.descriptionFiles || ["package.json"])
149157
),
@@ -189,8 +197,10 @@ exports.createResolver = function(options) {
189197
aliasFields,
190198
cachePredicate,
191199
cacheWithContext,
200+
conditionNames,
192201
descriptionFiles,
193202
enforceExtension,
203+
exportsFields,
194204
extensions,
195205
fileSystem,
196206
mainFields,
@@ -217,7 +227,9 @@ exports.createResolver = function(options) {
217227
resolver.ensureHook("describedResolve");
218228
resolver.ensureHook("rawModule");
219229
resolver.ensureHook("module");
220-
resolver.ensureHook("resolveInDirectory");
230+
resolver.ensureHook("resolveAsModule");
231+
resolver.ensureHook("undescribedResolveInPackage");
232+
resolver.ensureHook("resolveInPackage");
221233
resolver.ensureHook("resolveInExistingDirectory");
222234
resolver.ensureHook("relative");
223235
resolver.ensureHook("describedRelative");
@@ -227,6 +239,7 @@ exports.createResolver = function(options) {
227239
resolver.ensureHook("undescribedRawFile");
228240
resolver.ensureHook("rawFile");
229241
resolver.ensureHook("file");
242+
resolver.ensureHook("finalFile");
230243
resolver.ensureHook("existingFile");
231244
resolver.ensureHook("resolved");
232245

@@ -266,7 +279,12 @@ exports.createResolver = function(options) {
266279
plugins.push(new ModuleKindPlugin("after-described-resolve", "raw-module"));
267280
plugins.push(new JoinRequestPlugin("after-described-resolve", "relative"));
268281

269-
// module
282+
// raw-module
283+
exportsFields.forEach(exportsField => {
284+
plugins.push(
285+
new SelfReferencePlugin("raw-module", exportsField, "resolve-as-module")
286+
);
287+
});
270288
if (pnpApi) {
271289
plugins.push(new PnpPlugin("raw-module", pnpApi, "relative"));
272290
}
@@ -279,25 +297,53 @@ exports.createResolver = function(options) {
279297
});
280298

281299
// module
282-
plugins.push(new JoinRequestPartPlugin("module", "resolve-in-directory"));
300+
plugins.push(new JoinRequestPartPlugin("module", "resolve-as-module"));
283301

284-
// resolve-in-directory
302+
// resolve-as-module
285303
if (!resolveToContext) {
286304
plugins.push(
287305
new FileKindPlugin(
288-
"resolve-in-directory",
306+
"resolve-as-module",
289307
"single file module",
290308
"undescribed-raw-file"
291309
)
292310
);
293311
}
294312
plugins.push(
295313
new DirectoryExistsPlugin(
296-
"resolve-in-directory",
297-
"resolve-in-existing-directory"
314+
"resolve-as-module",
315+
"undescribed-resolve-in-package"
298316
)
299317
);
300318

319+
// undescribed-resolve-in-package
320+
plugins.push(
321+
new DescriptionFilePlugin(
322+
"undescribed-resolve-in-package",
323+
descriptionFiles,
324+
false,
325+
"resolve-in-package"
326+
)
327+
);
328+
plugins.push(
329+
new NextPlugin("after-undescribed-resolve-in-package", "resolve-in-package")
330+
);
331+
332+
// resolve-in-package
333+
exportsFields.forEach(exportsField => {
334+
plugins.push(
335+
new ExportsFieldPlugin(
336+
"resolve-in-package",
337+
conditionNames,
338+
exportsField,
339+
"final-file"
340+
)
341+
);
342+
});
343+
plugins.push(
344+
new NextPlugin("resolve-in-package", "resolve-in-existing-directory")
345+
);
346+
301347
// resolve-in-existing-directory
302348
plugins.push(
303349
new JoinRequestPlugin("resolve-in-existing-directory", "relative")
@@ -391,7 +437,10 @@ exports.createResolver = function(options) {
391437
aliasFields.forEach(item => {
392438
plugins.push(new AliasFieldPlugin("file", item, "resolve"));
393439
});
394-
plugins.push(new FileExistsPlugin("file", "existing-file"));
440+
plugins.push(new NextPlugin("file", "final-file"));
441+
442+
// final-file
443+
plugins.push(new FileExistsPlugin("final-file", "existing-file"));
395444

396445
// existing-file
397446
if (symlinks)

0 commit comments

Comments
 (0)