diff --git a/index.d.ts b/index.d.ts index 6002f86..497dfd3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -50,6 +50,11 @@ declare interface Options { Loader?: typeof Loader; resolve?: (file: string) => string | Promise; + + fileResolve?: ( + file: string, + importer: string + ) => string | null | Promise; } declare interface PostcssModulesPlugin { diff --git a/src/css-loader-core/loader.js b/src/css-loader-core/loader.js index 85a7d9f..983ee61 100644 --- a/src/css-loader-core/loader.js +++ b/src/css-loader-core/loader.js @@ -43,7 +43,7 @@ const traceKeySorter = (a, b) => { }; export default class FileSystemLoader { - constructor(root, plugins) { + constructor(root, plugins, fileResolve) { if (root === "/" && process.platform === "win32") { const cwdDrive = process.cwd().slice(0, 3); if (!/^[A-Z]:\\$/.test(cwdDrive)) { @@ -55,6 +55,7 @@ export default class FileSystemLoader { } this.root = root; + this.fileResolve = fileResolve; this.sources = {}; this.traces = {}; this.importNr = 0; @@ -65,43 +66,61 @@ export default class FileSystemLoader { fetch(_newPath, relativeTo, _trace) { let newPath = _newPath.replace(/^["']|["']$/g, ""), trace = _trace || String.fromCharCode(this.importNr++); + const useFileResolve = typeof this.fileResolve === "function"; return new Promise((resolve, reject) => { - let relativeDir = path.dirname(relativeTo), - rootRelativePath = path.resolve(relativeDir, newPath), - fileRelativePath = path.resolve( - path.resolve(this.root, relativeDir), - newPath - ); + (useFileResolve + ? this.fileResolve(newPath, relativeTo) + : Promise.resolve() + ).then((fileResolvedPath) => { + if (fileResolvedPath && !path.isAbsolute(fileResolvedPath)) { + reject( + 'The returned path from the "fileResolve" option must be absolute.' + ); + } + let relativeDir = path.dirname(relativeTo), + rootRelativePath = + fileResolvedPath || path.resolve(relativeDir, newPath), + fileRelativePath = + fileResolvedPath || + path.resolve( + path.resolve(this.root, relativeDir), + newPath + ); - // if the path is not relative or absolute, try to resolve it in node_modules - if (newPath[0] !== "." && !path.isAbsolute(newPath)) { - try { - fileRelativePath = require.resolve(newPath); - } catch (e) { - // noop + // if the path is not relative or absolute, try to resolve it in node_modules + if ( + !useFileResolve && + newPath[0] !== "." && + !path.isAbsolute(newPath) + ) { + try { + fileRelativePath = require.resolve(newPath); + } catch (e) { + // noop + } } - } - const tokens = this.tokensByFile[fileRelativePath]; - if (tokens) { - return resolve(tokens); - } + const tokens = this.tokensByFile[fileRelativePath]; + if (tokens) { + return resolve(tokens); + } - fs.readFile(fileRelativePath, "utf-8", (err, source) => { - if (err) reject(err); - this.core - .load( - source, - rootRelativePath, - trace, - this.fetch.bind(this) - ) - .then(({ injectableSource, exportTokens }) => { - this.sources[fileRelativePath] = injectableSource; - this.traces[trace] = fileRelativePath; - this.tokensByFile[fileRelativePath] = exportTokens; - resolve(exportTokens); - }, reject); + fs.readFile(fileRelativePath, "utf-8", (err, source) => { + if (err) reject(err); + this.core + .load( + source, + rootRelativePath, + trace, + this.fetch.bind(this) + ) + .then(({ injectableSource, exportTokens }) => { + this.sources[fileRelativePath] = injectableSource; + this.traces[trace] = fileRelativePath; + this.tokensByFile[fileRelativePath] = exportTokens; + resolve(exportTokens); + }, reject); + }); }); }); } diff --git a/src/index.js b/src/index.js index e010616..fc9c65f 100644 --- a/src/index.js +++ b/src/index.js @@ -33,8 +33,8 @@ function getScopedNameGenerator(opts) { function getLoader(opts, plugins) { const root = typeof opts.root === "undefined" ? "/" : opts.root; return typeof opts.Loader === "function" - ? new opts.Loader(root, plugins) - : new FileSystemLoader(root, plugins); + ? new opts.Loader(root, plugins, opts.fileResolve) + : new FileSystemLoader(root, plugins, opts.fileResolve); } function isGlobalModule(globalModules, inputFile) { @@ -85,6 +85,15 @@ module.exports = (opts = {}) => { if (resultPluginIndex === -1) { throw new Error("Plugin missing from options."); } + // resolve and fileResolve can't be used together + if ( + typeof opts.resolve === "function" && + typeof opts.fileResolve == "function" + ) { + throw new Error( + 'Please use either the "resolve" or the "fileResolve" option.' + ); + } const earlierPlugins = result.processor.plugins.slice( 0, resultPluginIndex diff --git a/test/__snapshots__/test.js.snap b/test/__snapshots__/test.js.snap index 46e1992..f1fe87b 100644 --- a/test/__snapshots__/test.js.snap +++ b/test/__snapshots__/test.js.snap @@ -328,6 +328,31 @@ Object { } `; +exports[`processes fileResolve option: processes fileResolve option 1`] = ` +"._composes_a_another-mixin { + display: flex; + height: 100px; + width: 200px; +}._composes_a_hello { + foo: bar; +}._composes_mixins_title { + color: black; + font-size: 40px; +}._composes_mixins_title:hover { + color: red; +}._composes_mixins_figure { + text-align: center +}._composes_mixins_title:focus, ._composes_mixins_figure:focus { + outline: none; + border: 1px solid red; +}._deepDeepCompose_deepDeepCompose { +}._deepDeepCompose_dotSlashRelativePath { +}._deepCompose_deepCompose { + content: \\"deepCompose\\"; +} +" +`; + exports[`processes globalModulePaths option: processes globalModulePaths option 1`] = ` ".page { padding: 20px; diff --git a/test/fixtures/in/deepCompose.css b/test/fixtures/in/deepCompose.css new file mode 100644 index 0000000..aa5c345 --- /dev/null +++ b/test/fixtures/in/deepCompose.css @@ -0,0 +1,4 @@ +.deepCompose { + composes: deepDeepCompose from "test-fixture-in/deepDeepCompose.css"; + content: "deepCompose"; +} diff --git a/test/fixtures/in/deepDeepCompose.css b/test/fixtures/in/deepDeepCompose.css new file mode 100644 index 0000000..878e42d --- /dev/null +++ b/test/fixtures/in/deepDeepCompose.css @@ -0,0 +1,7 @@ +.deepDeepCompose { + composes: title from "test-fixture-in/composes.mixins.css"; +} + +.dotSlashRelativePath { + composes: title from "./composes.mixins.css"; +} diff --git a/test/test.js b/test/test.js index 8ed6d8b..22143f0 100644 --- a/test/test.js +++ b/test/test.js @@ -415,3 +415,50 @@ it("processes resolve option", async () => { "_compose_resolve_figure-single-quote _composes_a_hello", }); }); + +it("processes fileResolve option", async () => { + const sourceFile = path.join(fixturesPath, "in", "deepCompose.css"); + const source = fs.readFileSync(sourceFile).toString(); + let json; + const result = await postcss([ + plugin({ + generateScopedName, + fileResolve: async (file, importer) => { + return path.resolve( + path.dirname(importer), + file.replace(/^test-fixture-in/, path.dirname(sourceFile)) + ); + }, + getJSON: (_, result) => { + json = result; + }, + }), + ]).process(source, { from: sourceFile }); + + expect(result.css).toMatchSnapshot("processes fileResolve option"); + expect(json).toStrictEqual({ + deepCompose: + "_deepCompose_deepCompose _deepDeepCompose_deepDeepCompose _composes_mixins_title", + }); +}); + +it("processes fileResolve and resolve option", async () => { + const sourceFile = path.join(fixturesPath, "in", "deepCompose.css"); + const source = fs.readFileSync(sourceFile).toString(); + const result = await postcss([ + plugin({ + generateScopedName, + resolve: (file) => file, + fileResolve: async (file, importer) => { + return path.resolve( + path.dirname(importer), + file.replace(/^test-fixture-in/, path.dirname(sourceFile)) + ); + }, + }), + ]) + .process(source, { from: sourceFile }) + .catch((error) => error); + + expect(result instanceof Error).toBe(true); +});