From 8be3fc5f2ff1199e94ce07beda64a9f2b0a87ee2 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 8 Sep 2026 10:42:29 +0000 Subject: [PATCH 1/4] feat: allow `file://` protocol prefix in absolute paths Paths coming from `import.meta.resolve()` are `file://` URLs, so the `absolutePath` keyword accepts an optional `file://` prefix now. Fixes #209 Recreated from #210, rebased on `main`. Co-Authored-By: Sage Abdullah Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013wqLmVHkAGBsWQgXknEQCK --- src/keywords/absolutePath.js | 3 ++- test/index.test.js | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/keywords/absolutePath.js b/src/keywords/absolutePath.js index 6a09696..322222d 100644 --- a/src/keywords/absolutePath.js +++ b/src/keywords/absolutePath.js @@ -70,11 +70,12 @@ function addAbsolutePathKeyword(ajv) { passes = false; } + // (?:file:\/\/)? - optional file:// protocol prefix // ?:[A-Za-z]:\\ - Windows absolute path // \\\\ - Windows network absolute path // \/ - Unix-like OS absolute path const isCorrectAbsolutePath = - schema === /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(data); + schema === /^(?:file:\/\/)?(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(data); if (!isCorrectAbsolutePath) { callback.errors = [getErrorFor(schema, parentSchema, data)]; diff --git a/test/index.test.js b/test/index.test.js index 91aa944..a46a657 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -325,6 +325,14 @@ describe("validation", () => { testAbsolutePath: "//server/directory/deep/tree", }); + createSuccessTestCase("absolutePath #6", { + testAbsolutePath: "file:///Users/username/directory/deep/tree", + }); + + createSuccessTestCase("absolutePath #7", { + testAbsolutePath: "file:///C:/directory/deep/tree", + }); + createSuccessTestCase("$data", { dollarData: { smaller: 5, From 39d33f6d6b35eb3d090d7077722d25753394de82 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 8 Sep 2026 10:45:12 +0000 Subject: [PATCH 2/4] test: cover every webpack option that takes an absolute path Walks webpack's own schema for every option reachable from the root that uses `absolutePath: true` - 34 of them, from `output.path` and `context` to the `module.rules` conditions and the `snapshot` path lists - and checks a `file://` URL validates for each, so the support is verified against the real consumer rather than the fixture schema alone. The options that require a relative path are covered too: the prefix must not make one of them accept an absolute path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013wqLmVHkAGBsWQgXknEQCK --- test/index.test.js | 121 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index a46a657..4c0c682 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -3061,6 +3061,127 @@ describe("validation", () => { webpackSchema, ); + // `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"; + + /** @type {Record>} */ + const webpackAbsolutePathOptions = { + "cache.cacheDirectory": { + cache: { type: "filesystem", cacheDirectory: WEBPACK_FILE_URL }, + }, + "cache.cacheLocation": { + cache: { type: "filesystem", cacheLocation: WEBPACK_FILE_URL }, + }, + "cache.immutablePaths": { + cache: { type: "filesystem", immutablePaths: [WEBPACK_FILE_URL] }, + }, + "cache.managedPaths": { + cache: { type: "filesystem", managedPaths: [WEBPACK_FILE_URL] }, + }, + context: { context: WEBPACK_FILE_URL }, + "dotenv.dir": { dotenv: { dir: WEBPACK_FILE_URL } }, + "experiments.buildHttp.cacheLocation": { + experiments: { + buildHttp: { allowedUris: [], cacheLocation: WEBPACK_FILE_URL }, + }, + }, + "experiments.buildHttp.lockfileLocation": { + experiments: { + buildHttp: { allowedUris: [], lockfileLocation: WEBPACK_FILE_URL }, + }, + }, + "module.defaultRules.exclude": { + module: { defaultRules: [{ exclude: WEBPACK_FILE_URL }] }, + }, + "module.defaultRules.include": { + module: { defaultRules: [{ include: WEBPACK_FILE_URL }] }, + }, + "module.defaultRules.issuer": { + module: { defaultRules: [{ issuer: WEBPACK_FILE_URL }] }, + }, + "module.defaultRules.realResource": { + module: { defaultRules: [{ realResource: WEBPACK_FILE_URL }] }, + }, + "module.defaultRules.resolve.restrictions": { + module: { + defaultRules: [{ resolve: { restrictions: [WEBPACK_FILE_URL] } }], + }, + }, + "module.defaultRules.resource": { + module: { defaultRules: [{ resource: WEBPACK_FILE_URL }] }, + }, + "module.defaultRules.test": { + module: { defaultRules: [{ test: WEBPACK_FILE_URL }] }, + }, + "module.noParse": { module: { noParse: WEBPACK_FILE_URL } }, + "module.noParse[]": { module: { noParse: [WEBPACK_FILE_URL] } }, + "module.rules.exclude": { + module: { rules: [{ exclude: WEBPACK_FILE_URL }] }, + }, + "module.rules.include": { + module: { rules: [{ include: WEBPACK_FILE_URL }] }, + }, + "module.rules.issuer": { + module: { rules: [{ issuer: WEBPACK_FILE_URL }] }, + }, + "module.rules.realResource": { + module: { rules: [{ realResource: WEBPACK_FILE_URL }] }, + }, + "module.rules.resolve.restrictions": { + module: { rules: [{ resolve: { restrictions: [WEBPACK_FILE_URL] } }] }, + }, + "module.rules.resource": { + module: { rules: [{ resource: WEBPACK_FILE_URL }] }, + }, + "module.rules.test": { module: { rules: [{ test: WEBPACK_FILE_URL }] } }, + "output.path": { output: { path: WEBPACK_FILE_URL } }, + recordsInputPath: { recordsInputPath: WEBPACK_FILE_URL }, + recordsOutputPath: { recordsOutputPath: WEBPACK_FILE_URL }, + recordsPath: { recordsPath: WEBPACK_FILE_URL }, + "resolve.restrictions": { resolve: { restrictions: [WEBPACK_FILE_URL] } }, + "resolveLoader.restrictions": { + resolveLoader: { restrictions: [WEBPACK_FILE_URL] }, + }, + "snapshot.immutablePaths": { + snapshot: { immutablePaths: [WEBPACK_FILE_URL] }, + }, + "snapshot.managedPaths": { snapshot: { managedPaths: [WEBPACK_FILE_URL] } }, + "snapshot.unmanagedPaths": { + snapshot: { unmanagedPaths: [WEBPACK_FILE_URL] }, + }, + "stats.context": { stats: { context: WEBPACK_FILE_URL } }, + }; + + for (const [option, config] of Object.entries(webpackAbsolutePathOptions)) { + createSuccessTestCase( + `\`file://\` for the webpack option \`${option}\``, + config, + {}, + webpackSchema, + ); + } + + // The prefix must not turn an option that wants a relative path into an absolute one + it.each([ + ["output.filename", { output: { filename: WEBPACK_FILE_URL } }], + [ + "output.sourceMapFilename", + { output: { sourceMapFilename: WEBPACK_FILE_URL } }, + ], + [ + "output.assetModuleFilename", + { output: { assetModuleFilename: WEBPACK_FILE_URL } }, + ], + ])( + "should fail validation for `file://` for the relative webpack option `%s`", + (option, config) => { + expect(() => validate(webpackSchema, config)).toThrow( + /is an absolute path!/, + ); + }, + ); + createFailedTestCase( "formatExclusiveMaximum #1", { From e0b9e4ad2e4f0dea3850a531396c2001cb02807a Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 8 Sep 2026 10:45:37 +0000 Subject: [PATCH 3/4] chore: add a changeset for the `file://` prefix Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013wqLmVHkAGBsWQgXknEQCK --- .changeset/olive-files-resolve.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/olive-files-resolve.md diff --git a/.changeset/olive-files-resolve.md b/.changeset/olive-files-resolve.md new file mode 100644 index 0000000..688b6a5 --- /dev/null +++ b/.changeset/olive-files-resolve.md @@ -0,0 +1,8 @@ +--- +"schema-utils": minor +--- + +author: laymonage +author: alexander-akait + +The `absolutePath` keyword accepts an optional `file://` prefix now, so a path from `import.meta.resolve()` can be passed to an option that takes an absolute path. Options that take a relative path reject such a value instead, they used to accept it. From 863de9e44839bada942cefdf9dc0e4d6b4c4aa62 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Tue, 8 Sep 2026 10:46:31 +0000 Subject: [PATCH 4/4] chore: link the changeset to the pull request Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013wqLmVHkAGBsWQgXknEQCK --- .changeset/olive-files-resolve.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/olive-files-resolve.md b/.changeset/olive-files-resolve.md index 688b6a5..ad36d3e 100644 --- a/.changeset/olive-files-resolve.md +++ b/.changeset/olive-files-resolve.md @@ -2,6 +2,7 @@ "schema-utils": minor --- +pr: 222 author: laymonage author: alexander-akait