From 6737c92f68f6bc380b874cf7ad79a39dda255548 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:18:06 +0000 Subject: [PATCH] feat: read a `file:` URL the way Node does in `absolutePath` The prefix wanted exactly two slashes and a lower-case scheme, but Node's URL parser normalizes any number of them and treats the scheme case-insensitively - `import("file:/abs/x.js")` loads. The optional group now takes the scheme with a lookahead, leaving the first slash to the path itself, so a scheme with nothing after it still names no absolute path. --- .changeset/absolute-path-file-url.md | 5 ++++ src/keywords/absolutePath.js | 7 +++-- test/__snapshots__/index.test.js.snap | 10 +++++++ test/index.test.js | 41 +++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .changeset/absolute-path-file-url.md diff --git a/.changeset/absolute-path-file-url.md b/.changeset/absolute-path-file-url.md new file mode 100644 index 0000000..df9831a --- /dev/null +++ b/.changeset/absolute-path-file-url.md @@ -0,0 +1,5 @@ +--- +"schema-utils": minor +--- + +The `absolutePath` keyword reads a `file:` URL the way Node's own URL parser does: the scheme may be followed by any number of slashes, so `file:/directory` is accepted alongside `file:///directory`, and it is matched case-insensitively, so `FILE:///directory` is too. A scheme with no slash after it — `file:directory` — still names no absolute path and is rejected. diff --git a/src/keywords/absolutePath.js b/src/keywords/absolutePath.js index 322222d..e9bf2f5 100644 --- a/src/keywords/absolutePath.js +++ b/src/keywords/absolutePath.js @@ -70,12 +70,15 @@ function addAbsolutePathKeyword(ajv) { passes = false; } - // (?:file:\/\/)? - optional file:// protocol prefix + // (?:file:(?=\/))? - optional file: scheme, which Node's URL parser + // takes with any number of slashes after it, the lookahead leaving + // the first one to the path itself // ?:[A-Za-z]:\\ - Windows absolute path // \\\\ - Windows network absolute path // \/ - Unix-like OS absolute path const isCorrectAbsolutePath = - schema === /^(?:file:\/\/)?(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(data); + schema === + /^(?:file:(?=\/))?(?:[A-Za-z]:(\\|\/)|\\\\|\/)/i.test(data); if (!isCorrectAbsolutePath) { callback.errors = [getErrorFor(schema, parentSchema, data)]; diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap index 2bd8d2c..baed50e 100644 --- a/test/__snapshots__/index.test.js.snap +++ b/test/__snapshots__/index.test.js.snap @@ -56,6 +56,16 @@ exports[`validation should fail validation for absolute path #4 1`] = ` - configuration.testAbsolutePath: The provided value ".." is not an absolute path!" `; +exports[`validation should fail validation for absolute path #5 1`] = ` +"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.testAbsolutePath: The provided value "file:directory/deep/tree" is not an absolute path!" +`; + +exports[`validation should fail validation for absolute path #6 1`] = ` +"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.testAbsolutePath: The provided value "file:C:/directory/deep/tree" is not an absolute path!" +`; + exports[`validation should fail validation for absolute path 1`] = ` "Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - configuration.context: The provided value "baz" is not an absolute path! diff --git a/test/index.test.js b/test/index.test.js index a9eb304..60d83ae 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -333,6 +333,18 @@ describe("validation", () => { testAbsolutePath: "file:///C:/directory/deep/tree", }); + createSuccessTestCase("absolutePath #8", { + testAbsolutePath: "file:/Users/username/directory/deep/tree", + }); + + createSuccessTestCase("absolutePath #9", { + testAbsolutePath: "file:/C:/directory/deep/tree", + }); + + createSuccessTestCase("absolutePath #10", { + testAbsolutePath: "FILE:///Users/username/directory/deep/tree", + }); + createSuccessTestCase("$data", { dollarData: { smaller: 5, @@ -2982,6 +2994,22 @@ describe("validation", () => { (msg) => expect(msg).toMatchSnapshot(), ); + createFailedTestCase( + "absolute path #5", + { + testAbsolutePath: "file:directory/deep/tree", + }, + (msg) => expect(msg).toMatchSnapshot(), + ); + + createFailedTestCase( + "absolute path #6", + { + testAbsolutePath: "file:C:/directory/deep/tree", + }, + (msg) => expect(msg).toMatchSnapshot(), + ); + createFailedTestCase( "not empty string #1", { @@ -3109,6 +3137,8 @@ describe("validation", () => { // `import.meta.resolve()` returns a `file://` URL, so every option of webpack's own schema that // takes an absolute path has to accept one - these are all of them const WEBPACK_FILE_URL = "file:///directory/deep/tree"; + // The same URL with the single slash Node's URL parser normalizes to three + const WEBPACK_SHORT_FILE_URL = "file:/directory/deep/tree"; /** @type {Record>} */ const webpackAbsolutePathOptions = { @@ -3205,6 +3235,17 @@ describe("validation", () => { {}, webpackSchema, ); + + createSuccessTestCase( + `\`file:/\` for the webpack option \`${option}\``, + JSON.parse( + JSON.stringify(config) + .split(WEBPACK_FILE_URL) + .join(WEBPACK_SHORT_FILE_URL), + ), + {}, + webpackSchema, + ); } // The prefix must not turn an option that wants a relative path into an absolute one