Skip to content

Commit d1fd390

Browse files
feat: accept file URLs where an option takes an absolute path
`import.meta.resolve()` returns a file URL, so a config built with it passed one where webpack expects an absolute path. The path options were rejected by `lib/util/fs.js` as neither posix nor windows, and a rule condition was matched against a resource path as a prefix and so never matched at all. `fileUrlToPath` converts them next to the other path helpers, and the config boundary and the rule set compiler apply it. Refs webpack/schema-utils#209
1 parent 26bce99 commit d1fd390

11 files changed

Lines changed: 121 additions & 13 deletions

File tree

‎.changeset/007-file-url-paths.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
Accept file URLs where an option takes an absolute path.

‎lib/config/normalization.js‎

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

88
const util = require("util");
9+
const { fileUrlToPath } = require("../util/identifier");
910

1011
/**
1112
* @import {
@@ -35,6 +36,28 @@ const util = require("util");
3536
* @property {WebpackOptionsNormalized["devtool"]=} devtool
3637
*/
3738

39+
/**
40+
* Converts a file URL to the path it names. Anything that is not a string is
41+
* returned unchanged, so `false` and `undefined` pass through.
42+
* @template {string | false | undefined} T
43+
* @param {T} pathOrUrl an absolute path, a file URL, `false` or `undefined`
44+
* @returns {T} an absolute path, `false` or `undefined`
45+
*/
46+
const optionalFileUrlToPath = (pathOrUrl) =>
47+
typeof pathOrUrl === "string"
48+
? /** @type {T} */ (fileUrlToPath(pathOrUrl))
49+
: pathOrUrl;
50+
51+
/**
52+
* Converts every file URL of a list to the path it names.
53+
* @param {(string | RegExp)[]} pathsOrUrls absolute paths, file URLs or regular expressions
54+
* @returns {(string | RegExp)[]} absolute paths and regular expressions
55+
*/
56+
const fileUrlsToPaths = (pathsOrUrls) =>
57+
pathsOrUrls.map((pathOrUrl) =>
58+
typeof pathOrUrl === "string" ? fileUrlToPath(pathOrUrl) : pathOrUrl
59+
);
60+
3861
const handledDeprecatedNoEmitOnErrors = util.deprecate(
3962
/**
4063
* Handles the callback logic for this hook.
@@ -226,8 +249,8 @@ const getNormalizedWebpackOptions = (config) => ({
226249
buildDependencies: normalizeBuildDependencies(
227250
cache.buildDependencies
228251
),
229-
cacheDirectory: cache.cacheDirectory,
230-
cacheLocation: cache.cacheLocation,
252+
cacheDirectory: optionalFileUrlToPath(cache.cacheDirectory),
253+
cacheLocation: optionalFileUrlToPath(cache.cacheLocation),
231254
hashAlgorithm: cache.hashAlgorithm,
232255
compression: cache.compression,
233256
idleTimeout: cache.idleTimeout,
@@ -250,7 +273,7 @@ const getNormalizedWebpackOptions = (config) => ({
250273
}
251274
})
252275
),
253-
context: config.context,
276+
context: optionalFileUrlToPath(config.context),
254277
dependencies: config.dependencies,
255278
devServer: optionalNestedConfig(config.devServer, (devServer) => {
256279
if (devServer === false) return false;
@@ -478,7 +501,7 @@ const getNormalizedWebpackOptions = (config) => ({
478501
: libraryBase.umdNamedDefine
479502
},
480503
module: output.module,
481-
path: output.path,
504+
path: optionalFileUrlToPath(output.path),
482505
pathinfo: output.pathinfo,
483506
publicPath: output.publicPath,
484507
resourceHints: normalizeResourceHints(output.resourceHints),
@@ -520,14 +543,16 @@ const getNormalizedWebpackOptions = (config) => ({
520543
nestedArray(config.plugins, (p) => p.filter(Boolean))
521544
),
522545
profile: config.profile,
523-
recordsInputPath:
546+
recordsInputPath: optionalFileUrlToPath(
524547
config.recordsInputPath !== undefined
525548
? config.recordsInputPath
526-
: config.recordsPath,
527-
recordsOutputPath:
549+
: config.recordsPath
550+
),
551+
recordsOutputPath: optionalFileUrlToPath(
528552
config.recordsOutputPath !== undefined
529553
? config.recordsOutputPath
530-
: config.recordsPath,
554+
: config.recordsPath
555+
),
531556
resolve: nestedConfig(config.resolve, (resolve) => ({
532557
...resolve,
533558
byDependency: keyedNestedConfig(resolve.byDependency, cloneObject)
@@ -563,9 +588,15 @@ const getNormalizedWebpackOptions = (config) => ({
563588
hash: contextModule.hash
564589
})
565590
),
566-
immutablePaths: optionalNestedArray(snapshot.immutablePaths, (p) => [...p]),
567-
managedPaths: optionalNestedArray(snapshot.managedPaths, (p) => [...p]),
568-
unmanagedPaths: optionalNestedArray(snapshot.unmanagedPaths, (p) => [...p])
591+
immutablePaths: optionalNestedArray(
592+
snapshot.immutablePaths,
593+
fileUrlsToPaths
594+
),
595+
managedPaths: optionalNestedArray(snapshot.managedPaths, fileUrlsToPaths),
596+
unmanagedPaths: optionalNestedArray(
597+
snapshot.unmanagedPaths,
598+
fileUrlsToPaths
599+
)
569600
})),
570601
stats: nestedConfig(config.stats, (stats) => {
571602
if (stats === false) {

‎lib/rules/RuleSetCompiler.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const { SyncHook } = require("tapable");
99
const { createPathGlobMatcher } = require("../util/globUtils");
10+
const { fileUrlToPath } = require("../util/identifier");
1011

1112
/** @import { ResolveRequest } from "enhanced-resolve" */
1213
/**
@@ -352,9 +353,10 @@ class RuleSetCompiler {
352353
);
353354
}
354355
if (typeof condition === "string") {
356+
const prefix = fileUrlToPath(condition);
355357
return {
356-
matchWhenEmpty: condition.length === 0,
357-
fn: (str) => typeof str === "string" && str.startsWith(condition)
358+
matchWhenEmpty: prefix.length === 0,
359+
fn: (str) => typeof str === "string" && str.startsWith(prefix)
358360
};
359361
}
360362
if (typeof condition === "function") {

‎lib/util/identifier.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"use strict";
66

77
const path = require("path");
8+
const { fileURLToPath } = require("url");
89

910
// Any absolute path: POSIX (/foo) and every Windows form — drive-letter (C:\,
1011
// C:/), UNC (\\, //), rooted (\, /); equals posix||win32 isAbsolute.
@@ -15,6 +16,19 @@ const WINDOWS_ABS_PATH_REGEXP = /^[a-z]:[\\/]/i;
1516
const SEGMENTS_SPLIT_REGEXP = /([|!])/;
1617
const WINDOWS_PATH_SEPARATOR_REGEXP = /\\/g;
1718
const NODE_MODULES_REGEXP = /[\\/]node_modules[\\/]/i;
19+
// A file URL, the form `import.meta.resolve()` returns a local module in.
20+
const FILE_URL_REGEXP = /^file:\/\//i;
21+
22+
/**
23+
* Converts a file URL to the path it names. Any other value is returned
24+
* unchanged, so this is safe to apply to an option that also takes a path.
25+
* @param {string} pathOrUrl an absolute path or a file URL
26+
* @returns {string} an absolute path
27+
*/
28+
const fileUrlToPath = (pathOrUrl) => {
29+
if (!FILE_URL_REGEXP.test(pathOrUrl)) return pathOrUrl;
30+
return fileURLToPath(pathOrUrl);
31+
};
1832

1933
/**
2034
* Relative path to request.
@@ -574,6 +588,7 @@ module.exports.absolutify = absolutify;
574588
module.exports.contextify = contextify;
575589
module.exports.contextifySourceUrl = contextifySourceUrl;
576590
module.exports.escapeHashInPathRequest = escapeHashInPathRequest;
591+
module.exports.fileUrlToPath = fileUrlToPath;
577592
module.exports.getUndoPath = getUndoPath;
578593
module.exports.makeCacheable = makeCacheable;
579594
module.exports.makePathsAbsolute = makeCacheableWithContext(_makePathsAbsolute);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "MARKER";
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
it("should apply a rule whose `test` is a file URL", function () {
2+
expect(require("./matched")).toBe("MARKER-test");
3+
});
4+
5+
it("should apply a rule whose `include` is a file URL", function () {
6+
expect(require("./directory/included")).toBe("MARKER-include");
7+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/** @type {import("../../../../").LoaderDefinition<{ marker: string }>} */
2+
module.exports = function (source) {
3+
return source.replace("MARKER", `MARKER-${this.getOptions().marker}`);
4+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = "MARKER";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"use strict";
2+
3+
const supportsFileUrlPaths = require("../../../helpers/supportsFileUrlPaths");
4+
5+
module.exports = () => supportsFileUrlPaths();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use strict";
2+
3+
const path = require("path");
4+
const { pathToFileURL } = require("url");
5+
6+
/**
7+
* Builds the file URL of a path below this directory.
8+
* @param {string} relativePath path relative to this directory
9+
* @returns {string} a file URL
10+
*/
11+
const fileUrl = (relativePath) =>
12+
pathToFileURL(path.resolve(__dirname, relativePath)).href;
13+
14+
/** @type {import("../../../../").Configuration} */
15+
module.exports = {
16+
module: {
17+
rules: [
18+
{
19+
test: fileUrl("matched.js"),
20+
use: { loader: "./loader", options: { marker: "test" } }
21+
},
22+
{
23+
include: fileUrl("directory"),
24+
use: { loader: "./loader", options: { marker: "include" } }
25+
}
26+
]
27+
}
28+
};

0 commit comments

Comments
 (0)