| 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 DescriptionFileUtils = require("./DescriptionFileUtils");
|
|---|
| 9 | const forEachBail = require("./forEachBail");
|
|---|
| 10 | const { processExportsField } = require("./util/entrypoints");
|
|---|
| 11 | const { parseIdentifier } = require("./util/identifier");
|
|---|
| 12 | const {
|
|---|
| 13 | deprecatedInvalidSegmentRegEx,
|
|---|
| 14 | invalidSegmentRegEx,
|
|---|
| 15 | } = require("./util/path");
|
|---|
| 16 |
|
|---|
| 17 | /** @typedef {import("./Resolver")} Resolver */
|
|---|
| 18 | /** @typedef {import("./Resolver").JsonObject} JsonObject */
|
|---|
| 19 | /** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
|
|---|
| 20 | /** @typedef {import("./Resolver").ResolveStepHook} ResolveStepHook */
|
|---|
| 21 | /** @typedef {import("./util/entrypoints").ExportsField} ExportsField */
|
|---|
| 22 | /** @typedef {import("./util/entrypoints").FieldProcessor} FieldProcessor */
|
|---|
| 23 |
|
|---|
| 24 | module.exports = class ExportsFieldPlugin {
|
|---|
| 25 | /**
|
|---|
| 26 | * @param {string | ResolveStepHook} source source
|
|---|
| 27 | * @param {Set<string>} conditionNames condition names
|
|---|
| 28 | * @param {string | string[]} fieldNamePath name path
|
|---|
| 29 | * @param {string | ResolveStepHook} target target
|
|---|
| 30 | */
|
|---|
| 31 | constructor(source, conditionNames, fieldNamePath, target) {
|
|---|
| 32 | this.source = source;
|
|---|
| 33 | this.target = target;
|
|---|
| 34 | this.conditionNames = conditionNames;
|
|---|
| 35 | this.fieldName = fieldNamePath;
|
|---|
| 36 | // `null` is cached for description files that have no exports field,
|
|---|
| 37 | // so subsequent resolves against the same package.json skip the
|
|---|
| 38 | // `DescriptionFileUtils.getField` walk entirely.
|
|---|
| 39 | /** @type {WeakMap<JsonObject, FieldProcessor | null>} */
|
|---|
| 40 | this._fieldProcessorCache = new WeakMap();
|
|---|
| 41 | }
|
|---|
| 42 |
|
|---|
| 43 | /**
|
|---|
| 44 | * @param {Resolver} resolver the resolver
|
|---|
| 45 | * @returns {void}
|
|---|
| 46 | */
|
|---|
| 47 | apply(resolver) {
|
|---|
| 48 | const target = resolver.ensureHook(this.target);
|
|---|
| 49 | resolver
|
|---|
| 50 | .getHook(this.source)
|
|---|
| 51 | .tapAsync("ExportsFieldPlugin", (request, resolveContext, callback) => {
|
|---|
| 52 | // When there is no description file, abort
|
|---|
| 53 | if (!request.descriptionFileData) return callback();
|
|---|
| 54 | if (
|
|---|
| 55 | // When the description file is inherited from parent, abort
|
|---|
| 56 | // (There is no description file inside of this package)
|
|---|
| 57 | request.relativePath !== "." ||
|
|---|
| 58 | request.request === undefined
|
|---|
| 59 | ) {
|
|---|
| 60 | return callback();
|
|---|
| 61 | }
|
|---|
| 62 |
|
|---|
| 63 | const { descriptionFileData } = request;
|
|---|
| 64 | const remainingRequest =
|
|---|
| 65 | request.query || request.fragment
|
|---|
| 66 | ? (request.request === "." ? "./" : request.request) +
|
|---|
| 67 | request.query +
|
|---|
| 68 | request.fragment
|
|---|
| 69 | : request.request;
|
|---|
| 70 |
|
|---|
| 71 | /** @type {string[]} */
|
|---|
| 72 | let paths;
|
|---|
| 73 | /** @type {string | null} */
|
|---|
| 74 | let usedField;
|
|---|
| 75 |
|
|---|
| 76 | try {
|
|---|
| 77 | // Look up the cached processor first. On a cache hit we
|
|---|
| 78 | // avoid re-walking the description file for the exports
|
|---|
| 79 | // field — and `null` is cached for description files that
|
|---|
| 80 | // have no exports field at all, so those skip the read
|
|---|
| 81 | // entirely. `processExportsField` can throw on a malformed
|
|---|
| 82 | // `exports` map (e.g. a key without a leading `.`), so
|
|---|
| 83 | // building the processor must stay inside this try/catch.
|
|---|
| 84 | let fieldProcessor =
|
|---|
| 85 | this._fieldProcessorCache.get(descriptionFileData);
|
|---|
| 86 | if (
|
|---|
| 87 | fieldProcessor === undefined &&
|
|---|
| 88 | !this._fieldProcessorCache.has(descriptionFileData)
|
|---|
| 89 | ) {
|
|---|
| 90 | const exportsField =
|
|---|
| 91 | /** @type {ExportsField | null | undefined} */
|
|---|
| 92 | (
|
|---|
| 93 | DescriptionFileUtils.getField(
|
|---|
| 94 | descriptionFileData,
|
|---|
| 95 | this.fieldName,
|
|---|
| 96 | )
|
|---|
| 97 | );
|
|---|
| 98 | fieldProcessor = exportsField
|
|---|
| 99 | ? processExportsField(exportsField)
|
|---|
| 100 | : null;
|
|---|
| 101 | this._fieldProcessorCache.set(descriptionFileData, fieldProcessor);
|
|---|
| 102 | }
|
|---|
| 103 | if (!fieldProcessor) return callback();
|
|---|
| 104 |
|
|---|
| 105 | if (request.directory) {
|
|---|
| 106 | return callback(
|
|---|
| 107 | new Error(
|
|---|
| 108 | `Resolving to directories is not possible with the exports field (request was ${remainingRequest}/)`,
|
|---|
| 109 | ),
|
|---|
| 110 | );
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | [paths, usedField] = fieldProcessor(
|
|---|
| 114 | remainingRequest,
|
|---|
| 115 | this.conditionNames,
|
|---|
| 116 | );
|
|---|
| 117 | } catch (/** @type {unknown} */ err) {
|
|---|
| 118 | if (resolveContext.log) {
|
|---|
| 119 | resolveContext.log(
|
|---|
| 120 | `Exports field in ${request.descriptionFilePath} can't be processed: ${err}`,
|
|---|
| 121 | );
|
|---|
| 122 | }
|
|---|
| 123 | return callback(/** @type {Error} */ (err));
|
|---|
| 124 | }
|
|---|
| 125 |
|
|---|
| 126 | if (paths.length === 0) {
|
|---|
| 127 | const conditions = [...this.conditionNames];
|
|---|
| 128 | const conditionsStr =
|
|---|
| 129 | conditions.length === 1
|
|---|
| 130 | ? `the condition "${conditions[0]}"`
|
|---|
| 131 | : `the conditions ${JSON.stringify(conditions)}`;
|
|---|
| 132 | return callback(
|
|---|
| 133 | new Error(
|
|---|
| 134 | `"${remainingRequest}" is not exported under ${conditionsStr} from package ${request.descriptionFileRoot} (see exports field in ${request.descriptionFilePath})`,
|
|---|
| 135 | ),
|
|---|
| 136 | );
|
|---|
| 137 | }
|
|---|
| 138 |
|
|---|
| 139 | forEachBail(
|
|---|
| 140 | paths,
|
|---|
| 141 | /**
|
|---|
| 142 | * @param {string} path path
|
|---|
| 143 | * @param {(err?: null | Error, result?: null | ResolveRequest) => void} callback callback
|
|---|
| 144 | * @param {number} i index
|
|---|
| 145 | * @returns {void}
|
|---|
| 146 | */
|
|---|
| 147 | (path, callback, i) => {
|
|---|
| 148 | const parsedIdentifier = parseIdentifier(path);
|
|---|
| 149 |
|
|---|
| 150 | if (!parsedIdentifier) return callback();
|
|---|
| 151 |
|
|---|
| 152 | const [relativePath, query, fragment] = parsedIdentifier;
|
|---|
| 153 |
|
|---|
| 154 | if (!relativePath.startsWith("./")) {
|
|---|
| 155 | if (paths.length === i) {
|
|---|
| 156 | return callback(
|
|---|
| 157 | new Error(
|
|---|
| 158 | `Invalid "exports" target "${path}" defined for "${usedField}" in the package config ${request.descriptionFilePath}, targets must start with "./"`,
|
|---|
| 159 | ),
|
|---|
| 160 | );
|
|---|
| 161 | }
|
|---|
| 162 |
|
|---|
| 163 | return callback();
|
|---|
| 164 | }
|
|---|
| 165 |
|
|---|
| 166 | const withoutDotSlash = relativePath.slice(2);
|
|---|
| 167 | if (
|
|---|
| 168 | invalidSegmentRegEx.test(withoutDotSlash) &&
|
|---|
| 169 | deprecatedInvalidSegmentRegEx.test(withoutDotSlash)
|
|---|
| 170 | ) {
|
|---|
| 171 | if (paths.length === i) {
|
|---|
| 172 | return callback(
|
|---|
| 173 | new Error(
|
|---|
| 174 | `Invalid "exports" target "${path}" defined for "${usedField}" in the package config ${request.descriptionFilePath}, targets must start with "./"`,
|
|---|
| 175 | ),
|
|---|
| 176 | );
|
|---|
| 177 | }
|
|---|
| 178 |
|
|---|
| 179 | return callback();
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | /** @type {ResolveRequest} */
|
|---|
| 183 | const obj = {
|
|---|
| 184 | ...request,
|
|---|
| 185 | request: undefined,
|
|---|
| 186 | path: resolver.join(
|
|---|
| 187 | /** @type {string} */ (request.descriptionFileRoot),
|
|---|
| 188 | relativePath,
|
|---|
| 189 | ),
|
|---|
| 190 | relativePath,
|
|---|
| 191 | query,
|
|---|
| 192 | fragment,
|
|---|
| 193 | };
|
|---|
| 194 |
|
|---|
| 195 | resolver.doResolve(
|
|---|
| 196 | target,
|
|---|
| 197 | obj,
|
|---|
| 198 | `using exports field: ${path}`,
|
|---|
| 199 | resolveContext,
|
|---|
| 200 | (err, result) => {
|
|---|
| 201 | if (err) return callback(err);
|
|---|
| 202 | // Don't allow to continue - https://github.com/webpack/enhanced-resolve/issues/400
|
|---|
| 203 | if (result === undefined) return callback(null, null);
|
|---|
| 204 | callback(null, result);
|
|---|
| 205 | },
|
|---|
| 206 | );
|
|---|
| 207 | },
|
|---|
| 208 | /**
|
|---|
| 209 | * @param {(null | Error)=} err error
|
|---|
| 210 | * @param {(null | ResolveRequest)=} result result
|
|---|
| 211 | * @returns {void}
|
|---|
| 212 | */
|
|---|
| 213 | (err, result) => {
|
|---|
| 214 | if (err) return callback(err);
|
|---|
| 215 | // When an exports field match was found but the target file doesn't exist,
|
|---|
| 216 | // return an error to prevent fallback to parent node_modules directories.
|
|---|
| 217 | // Per the Node.js ESM spec, a matched exports entry that fails to resolve
|
|---|
| 218 | // is a hard error, not a signal to continue searching up the directory tree.
|
|---|
| 219 | // See: https://github.com/webpack/enhanced-resolve/issues/399
|
|---|
| 220 | if (!result) {
|
|---|
| 221 | return callback(
|
|---|
| 222 | new Error(
|
|---|
| 223 | `Package path ${remainingRequest} is exported from package ${request.descriptionFileRoot}, but no valid target file was found (see exports field in ${request.descriptionFilePath})`,
|
|---|
| 224 | ),
|
|---|
| 225 | );
|
|---|
| 226 | }
|
|---|
| 227 | callback(null, result);
|
|---|
| 228 | },
|
|---|
| 229 | );
|
|---|
| 230 | });
|
|---|
| 231 | }
|
|---|
| 232 | };
|
|---|