diff --git a/src/FileSystemLoader.js b/src/FileSystemLoader.js index 09091a1..d319ae1 100644 --- a/src/FileSystemLoader.js +++ b/src/FileSystemLoader.js @@ -116,10 +116,10 @@ export default class FileSystemLoader { }); } - get finalSource() { + get finalSources() { const traces = this.traces; const sources = this.sources; - let written = new Set(); + const written = new Set(); return Object.keys(traces) .sort(traceKeySorter) @@ -130,8 +130,12 @@ export default class FileSystemLoader { } written.add(filename); - return sources[filename]; + return { file: filename, source: sources[filename] }; }) - .join(""); + .filter(Boolean); + } + + get finalSource() { + return this.finalSources.map((entry) => entry.source).join(""); } } diff --git a/src/pluginFactory.js b/src/pluginFactory.js index 376f69c..d815f1b 100644 --- a/src/pluginFactory.js +++ b/src/pluginFactory.js @@ -84,8 +84,55 @@ export function makePlugin(opts) { if (rule.selector.trim() === "") rule.remove(); }); - const out = loader.finalSource; - if (out) css.prepend(out); + // Prepend the traced dependencies exactly as before, then re-parse + // each dependency with its own `from` and graft those sources onto + // the prepended nodes, pairing them by position. The pairing holds + // because each dependency is complete CSS, so parsing the + // concatenation yields the same node sequence as parsing each part. + // If malformed CSS merges nodes across a file boundary the counts + // differ and the graft is skipped, leaving the nodes unattributed + // as before. Plugins that run after this one (e.g. url rewriters) + // need `source.input.file` to resolve relative paths against the + // file the CSS actually came from — see #149. + const sources = Array.isArray(loader.finalSources) ? loader.finalSources : null; + if (sources) { + const joined = sources.map((entry) => entry.source).join(""); + if (joined) { + const countBefore = css.nodes.length; + css.prepend(joined); + const prepended = css.nodes.slice(0, css.nodes.length - countBefore); + const sourceSeq = []; + for (const { file, source } of sources) { + postcss.parse(source, { from: file }).walk((node) => { + sourceSeq.push(node.source); + }); + } + let count = 0; + for (const node of prepended) { + count += 1; + if (typeof node.walk === "function") { + node.walk(() => { + count += 1; + }); + } + } + if (count === sourceSeq.length) { + let i = 0; + for (const node of prepended) { + node.source = sourceSeq[i++]; + if (typeof node.walk === "function") { + node.walk((child) => { + child.source = sourceSeq[i++]; + }); + } + } + } + } + } else { + // Custom loaders may not implement `finalSources`. + const out = loader.finalSource; + if (out) css.prepend(out); + } if (opts.localsConvention) { const reducer = makeLocalsConventionReducer(opts.localsConvention, inputFile); diff --git a/test/test.js b/test/test.js index 545e3e9..aa19c09 100644 --- a/test/test.js +++ b/test/test.js @@ -129,6 +129,24 @@ p { expect(result.css).toEqual(source.replace("green", "blue")); }); +it("keeps the source file on CSS prepended from composed dependencies", async () => { + const sourceFile = path.join(fixturesPath, "in", "composes.css"); + const source = fs.readFileSync(sourceFile).toString(); + const mixinsFile = path.join(fixturesPath, "in", "composes.mixins.css"); + + const result = await postcss([plugin({ generateScopedName, getJSON: () => {} })]).process( + source, + { from: sourceFile }, + ); + + const fileByDecl = {}; + result.root.walkDecls((decl) => { + fileByDecl[`${decl.prop}: ${decl.value}`] = decl.source.input.file; + }); + expect(fileByDecl["font-size: 40px"]).toEqual(mixinsFile); + expect(fileByDecl["color: green"]).toEqual(sourceFile); +}); + it("saves JSON next to CSS by default", async () => { const sourceFile = path.join(fixturesPath, "in", "saveJSON.css"); const source = fs.readFileSync(sourceFile).toString();