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