Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/FileSystemLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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("");
}
}
51 changes: 49 additions & 2 deletions src/pluginFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down