Skip to content

Commit

Permalink
Fix unchanged revision hashes when watching
Browse files Browse the repository at this point in the history
  • Loading branch information
kentaroi committed Aug 14, 2024
1 parent 6e7e5af commit 0c9bf4f
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 3 deletions.
23 changes: 20 additions & 3 deletions lib/compile.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
const path = require("path");
const { posix: path } = require("path");
const upath = require("upath");
const url = require("url");

const sass = require("sass");

const depMap = require("./dependency-map");
const sourceMapStore = require("./source-map-store");
const getLoadPathsFixedSassOptions = require("./get-load-paths-fixed-sass-options");

const debugDev = require("debug")("Dev:EleventySass:Compile");

const workingDir = upath.normalize(process.cwd());

const compile = async function(inputContent, inputPath, sassOptions, config, postcss) {
let parsed = path.posix.parse(inputPath);
let parsed = path.parse(inputPath);
if (parsed.name.startsWith("_")) {
debugDev(`Actually, didn't compile ${ inputPath }, because the filename starts with "_"`);
return;
}

let inputURL = url.pathToFileURL(path.posix.resolve(inputPath)).href;
let inputURL = url.pathToFileURL(path.resolve(inputPath)).href;

let stringOptions = getLoadPathsFixedSassOptions(sassOptions, parsed.dir, config);
stringOptions.url = inputURL;
Expand Down Expand Up @@ -46,6 +50,19 @@ const compile = async function(inputContent, inputPath, sassOptions, config, pos

this.addDependencies(inputPath, loadedUrls);

// `depMap` is used to determine which revision hashes should be invalidated when
// a Sass/SCSS file is updated.
// In the following code, `loadedPaths` includes the dependent Sass/SCSS file path
// (`dependant`) during the compilation of `inputContent`.
// By calling `depMap.update()` with `dependant` and `loadedPaths`, dependencies
// become self-referential. This means that `dependantsOf()` for an updated file
// returns not only files that depend on it but also the updated file itself.
let dependant = path.normalize(inputPath);
let loadedPaths = loadedUrls.map(loadedUrl => {
return path.relative(workingDir, upath.normalize(url.fileURLToPath(loadedUrl)));
});
depMap.update(dependant, loadedPaths);

return css;
};

Expand Down
18 changes: 18 additions & 0 deletions lib/eleventy-sass.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ try {
} catch { }

const parseOptions = require("./parse-options");
const depMap = require("./dependency-map");
const compile = require("./compile");
const sourceMapStore = require("./source-map-store");
const hasPlugin = require("./has-plugin");
Expand Down Expand Up @@ -273,6 +274,23 @@ const eleventySass = function(eleventyConfig, userOptions = {}) {
eleventyConfig.on("eleventy.before", () => {
compileCache.clear();
});

if (rev) {
eleventyConfig.on("eleventy.beforeWatch", (queue) => {
for (let inputPath of queue) {
let last5Chars = inputPath.substring(inputPath.length - 5);
if (last5Chars !== ".sass" && last5Chars !== ".scss")
continue;
// `dependants` have `normalizedPath` as an element in the following code.
// See the comment for `depMap.update()` in `compile.js`.
let normalizedPath = path.normalize(inputPath);
let dependants = depMap.dependantsOf(normalizedPath);
dependants.forEach(dependant => pluginRev.deleteRevHash(dependant));
debugDev("Invalidated the revision hashes for %O because of the update of %O",
dependants, normalizedPath);
}
});
}
}

module.exports = eleventySass;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const sass = require("../../../..");
const pluginRev = require("eleventy-plugin-rev");

console.log(`Eleventy PID: ${ process.pid }`);

module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginRev);
eleventyConfig.addPlugin(sass, {
rev: true
});
};
114 changes: 114 additions & 0 deletions test/integration/watcher-with-rev-dependency-file-update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
if (parseInt(process.version.match(/^v(\d+)/)[1]) < 16) {
const test = require("ava");
test("tests don't support node version < 16", async t => {
t.pass();
});
return;
}

const { spawn } = require("child_process");
const path = require("path");
const { promises: fs } = require("fs");
const { setTimeout } = require("timers/promises");
const { createHash } = require("crypto");

const test = require("ava");
const Semaphore = require("@debonet/es6semaphore");

const createProject = require("./_create-project-default");
let dir;
let proc;
let pid;

const headerCssContent = "header{background-color:pink}";
const headerRevHash = createHash("sha256").update(headerCssContent).digest("hex").slice(0, 8);

const updatedHeaderCssContent = "header{background-color:red}";
const updatedHeaderRevHash = createHash("sha256").update(updatedHeaderCssContent).digest("hex").slice(0, 8);

const styleCssContent = "header{background-color:pink}body{background-color:red}";
const styleRevHash = createHash("sha256").update(styleCssContent).digest("hex").slice(0, 8);

const updatedStyleCssContent = "header{background-color:red}body{background-color:red}";
const updatedStyleRevHash = createHash("sha256").update(updatedStyleCssContent).digest("hex").slice(0, 8);

test.after.always("cleanup child process", t => {
if (proc && proc.exitCode === null) {
pid ? process.kill(pid, "SIGINT") : proc.kill();
}
});

test.before(async t => {
let sem = new Semaphore(1);
await sem.wait();
dir = createProject("watcher-with-rev-dependency-file-update");
proc = spawn("npx", ["@11ty/eleventy", "--config=config-for-watcher-with-rev.js", "--watch"], { cwd: dir, shell: true, timeout: 20000 });
proc.on("exit", (code, signal) => {
if (process.platform === "darwin")
pid = undefined;
sem.signal();
});
proc.stdout.on("data", function(data) {
let str = data.toString();

let match = str.match(/^Eleventy PID: (\d+)/);
if (match) {
pid = parseInt(match[1]);
}

if (str.trim() === "[11ty] Watching…") {
sem.signal();
}
});
await sem.wait();
await setTimeout(300);


let stylesheetsDir = path.join(dir, "_site", "stylesheets");
let csses = await fs.readdir(stylesheetsDir);
t.deepEqual(csses, [`header-${ headerRevHash }.css`, `style-${ styleRevHash }.css`]);


let headerSCSS = path.join(dir, "stylesheets", "header.scss");
fs.writeFile(headerSCSS, `header {
background-color: red;
}`);

await sem.wait();
await setTimeout(300);

if (pid) {
process.kill(pid, "SIGINT");
pid = undefined;
await sem.wait();
}
});

test("write CSS files with correct revision hashes", async t => {
let stylesheetsDir = path.join(dir, "_site", "stylesheets");
let csses = await fs.readdir(stylesheetsDir);
t.is(csses.length, 4);
t.true(csses.includes(`header-${ headerRevHash }.css`));
t.true(csses.includes(`header-${ updatedHeaderRevHash }.css`));
t.true(csses.includes(`style-${ styleRevHash }.css`));
t.true(csses.includes(`style-${ updatedStyleRevHash }.css`));
});


test("watcher works", async t => {
let headerPath = path.join(dir, "_site", "stylesheets", `header-${ headerRevHash }.css`);
let headerCss = await fs.readFile(headerPath, { encoding: "utf8" });
t.is(headerCss, headerCssContent);

let updatedHeaderPath = path.join(dir, "_site", "stylesheets", `header-${ updatedHeaderRevHash }.css`);
let updatedHeaderCss = await fs.readFile(updatedHeaderPath, { encoding: "utf8" });
t.is(updatedHeaderCss, updatedHeaderCssContent);

let stylePath = path.join(dir, "_site", "stylesheets", `style-${ styleRevHash }.css`);
let styleCss = await fs.readFile(stylePath, { encoding: "utf8" });
t.is(styleCss, styleCssContent);

let updatedStylePath = path.join(dir, "_site", "stylesheets", `style-${ updatedStyleRevHash }.css`);
let updatedStyleCss = await fs.readFile(updatedStylePath, { encoding: "utf8" });
t.is(updatedStyleCss, updatedStyleCssContent);
});
105 changes: 105 additions & 0 deletions test/integration/watcher-with-rev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
if (parseInt(process.version.match(/^v(\d+)/)[1]) < 16) {
const test = require("ava");
test("tests don't support node version < 16", async t => {
t.pass();
});
return;
}

const { spawn } = require("child_process");
const path = require("path");
const { promises: fs } = require("fs");
const { setTimeout } = require("timers/promises");
const { createHash } = require("crypto");

const test = require("ava");
const Semaphore = require("@debonet/es6semaphore");

const createProject = require("./_create-project-default");
let dir;
let proc;
let pid;

const headerCssContent = "header{background-color:pink}";
const headerRevHash = createHash("sha256").update(headerCssContent).digest("hex").slice(0, 8);

const styleCssContent = "header{background-color:pink}body{background-color:red}";
const styleRevHash = createHash("sha256").update(styleCssContent).digest("hex").slice(0, 8);

const updatedStyleCssContent = "header{background-color:pink}body{background-color:red;color:blue}";
const updatedStyleRevHash = createHash("sha256").update(updatedStyleCssContent).digest("hex").slice(0, 8);

test.after.always("cleanup child process", t => {
if (proc && proc.exitCode === null) {
pid ? process.kill(pid, "SIGINT") : proc.kill();
}
});

test.before(async t => {
let sem = new Semaphore(1);
await sem.wait();
dir = createProject("watcher-with-rev");
proc = spawn("npx", ["@11ty/eleventy", "--config=config-for-watcher-with-rev.js", "--watch"], { cwd: dir, shell: true, timeout: 20000 });
proc.on("exit", (code, signal) => {
console.debug("exit");
if (process.platform === "darwin")
pid = undefined;
sem.signal();
});
proc.stdout.on("data", function(data) {
let str = data.toString();

let match = str.match(/^Eleventy PID: (\d+)/);
if (match) {
pid = parseInt(match[1]);
}

if (str.trim() === "[11ty] Watching…")
sem.signal();
});
await sem.wait();
await setTimeout(300);


let stylesheetsDir = path.join(dir, "_site", "stylesheets");
let csses = await fs.readdir(stylesheetsDir);
t.deepEqual(csses, [`header-${ headerRevHash }.css`, `style-${ styleRevHash }.css`]);


let styleSCSS = path.join(dir, "stylesheets", "style.scss");
fs.writeFile(styleSCSS, `@use "colors";
@use "header";
body {
background-color: colors.$background;
color: blue;
}`);

await sem.wait();
await setTimeout(300);

if (pid) {
process.kill(pid, "SIGINT");
pid = undefined;
await sem.wait();
}
});

test("write CSS files with correct revision hashes", async t => {
let stylesheetsDir = path.join(dir, "_site", "stylesheets");
let csses = await fs.readdir(stylesheetsDir);
t.is(csses.length, 3);
t.true(csses.includes(`header-${ headerRevHash }.css`));
t.true(csses.includes(`style-${ styleRevHash }.css`));
t.true(csses.includes(`style-${ updatedStyleRevHash }.css`));
});

test("watcher works", async t => {
let stylePath = path.join(dir, "_site", "stylesheets", `style-${ styleRevHash }.css`);
let styleCss = await fs.readFile(stylePath, { encoding: "utf8" });
t.is(styleCss, styleCssContent);

let updatedStylePath = path.join(dir, "_site", "stylesheets", `style-${ updatedStyleRevHash }.css`);
let updatedStyleCss = await fs.readFile(updatedStylePath, { encoding: "utf8" });
t.is(updatedStyleCss, updatedStyleCssContent);
});

0 comments on commit 0c9bf4f

Please sign in to comment.