Skip to content

Commit

Permalink
feat(shared): #200 watch for config changes
Browse files Browse the repository at this point in the history
- Watch for configuration file changes
- Watch for parts of the configuration which are imported
- Use tmpdirTest for watcher tests
- Enable watcher tests by default
- Do not watch child directories of already watched directories

Closes: #132
  • Loading branch information
sdorra authored Aug 3, 2024
1 parent 53efa50 commit 207a3de
Show file tree
Hide file tree
Showing 13 changed files with 600 additions and 172 deletions.
6 changes: 6 additions & 0 deletions .changeset/cyan-paws-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@content-collections/integrations": minor
"@content-collections/core": minor
---

Rebuild on configuration changes
8 changes: 5 additions & 3 deletions packages/core/src/__tests__/tmpdir.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test } from "vitest";
import os from "node:os";
import fs from "node:fs/promises";
import path from "node:path";
import path, { resolve } from "node:path";

interface TmpDirFixture {
tmpdir: string;
Expand All @@ -12,8 +12,10 @@ export const tmpdirTest = test.extend<TmpDirFixture>({
const ostmpdir = os.tmpdir();

const directory = await fs.mkdtemp(path.join(ostmpdir, "vitest-"));

await use(directory);
// we need to call realpath, because mktemp returns /var/folders/... on macOS
// but the paths which are returned by the watcher are /private/var/folders/...
const directoryPath = await fs.realpath(directory);
await use(directoryPath);

await fs.rm(directory, { recursive: true });
},
Expand Down
184 changes: 166 additions & 18 deletions packages/core/src/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,7 @@ import { createBuilder as origCreateBuilder } from "./builder";
import path from "node:path";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { Emitter } from "./events";

// we mock the watcher module, because it causes problems in some situations
// we test the watcher module separately
vi.mock("./watcher", async () => {
return {
createWatcher: async (_: Emitter, paths: Array<string>) => {
return {
paths,
unsubscribe: async () => {},
};
},
};
});
import { createEmitter, type Emitter } from "./events";

describe("builder", () => {
afterEach(async () => {
Expand All @@ -43,6 +30,36 @@ describe("builder", () => {
};
}

it("should create builder with the default output directory", async () => {
const configPath = path.join(__dirname, "__tests__", "config.002.ts");
const emitter: Emitter = createEmitter();

const events: Array<{
outputDirectory: string;
}> = [];
emitter.on("builder:created", (event) => {
events.push(event);
});

await origCreateBuilder(
configPath,
{
configName: "default-config.mjs",
},
emitter
);

await vi.waitUntil(() => events.length > 0);

const event = events[0]
if (!event) {
throw new Error("Event is undefined");
}
expect(event.outputDirectory).toBe(
path.join(__dirname, "__tests__", ".content-collections", "generated")
);
});

describe("build", () => {
it("should build", async () => {
const { builder, outputDir } = await createBuilder("config.002");
Expand Down Expand Up @@ -93,6 +110,8 @@ describe("builder", () => {

expect(events).toEqual(["builder:start", "builder:end"]);
});

it("should resolve to default output directory", async () => {});
});

describe("sync", () => {
Expand Down Expand Up @@ -157,9 +176,50 @@ describe("builder", () => {

expect(allPosts.length).toBe(0);
});

it("should return false on sync without previous build", async () => {
const builder = await origCreateBuilder(path.join("tmp", "config.ts"), {
configName: "sync.no.build.ts",
cacheDir: path.join("tmp", "cache"),
outputDir: path.join("tmp", "output-no-build"),
});

const newFile = path.join("tmp", "sources", "posts", "second.md");
await fs.writeFile(newFile, "---\ntitle: Second\n---\nSecond post");

expect(await builder.sync("create", newFile)).toBe(false);
});
});

describe("watch", () => {
const watcherOptions = vi.hoisted(() => ({
paths: [] as Array<string>,
configPaths: [] as Array<string>,
}));

// we mock the watcher module, because it causes problems in some situations
// we test the watcher module separately
vi.mock("./watcher", async () => {
return {
createWatcher: async (
_: Emitter,
configPaths: Array<string>,
paths: Array<string>
) => {
watcherOptions.paths = paths;
watcherOptions.configPaths = configPaths;
return {
unsubscribe: vi.fn(),
};
},
};
});

beforeEach(() => {
watcherOptions.paths = [];
watcherOptions.configPaths = [];
});

it("should return watcher", async () => {
const { builder } = await createBuilder("config.002");

Expand All @@ -170,11 +230,99 @@ describe("builder", () => {
it("should pass the collection directories to the watcher", async () => {
const { builder } = await createBuilder("config.002");

const watcher = await builder.watch();
// @ts-ignore our mock returns a watcher with a paths property
expect(watcher.paths).toEqual([
await builder.watch();

expect(watcherOptions.paths).toEqual([
path.join(__dirname, "__tests__", "sources", "posts"),
]);
})
});

it("should pass the configuration paths to the watcher", async () => {
const { builder } = await createBuilder("config.002");

const watcher = await builder.watch();
await watcher.unsubscribe();

expect(watcherOptions.configPaths).toEqual([
path.join("src", "__tests__", "config.002.ts"),
]);
});

it("should recompile the configuration after the configuration has changed", async () => {
const configPath = path.join(__dirname, "__tests__", "config.002.ts");
const outputDir = path.join(
__dirname,
"__tests__",
".content-collections",
"generated-config-rebuild-on-config-change"
);

const emitter = createEmitter();

const builder = await origCreateBuilder(
configPath,
{
configName: "rebuild-on-change.mjs",
outputDir,
},
emitter
);

await builder.build();

const compiledConfiguration = path.join(
__dirname,
"__tests__",
".content-collections",
"cache",
"rebuild-on-change.mjs"
);

const { mtimeMs } = await fs.lstat(compiledConfiguration);

emitter.emit("watcher:config-changed", {
filePath: configPath,
modification: "create",
});

await builder.build();

const { mtimeMs: newMtimeMs } = await fs.lstat(compiledConfiguration);

expect(newMtimeMs).toBeGreaterThan(mtimeMs);
});

it("should reconnect watcher after configuration has changed", async () => {
const configPath = path.join(__dirname, "__tests__", "config.002.ts");
const outputDir = path.join(
__dirname,
"__tests__",
".content-collections",
"reconnect-watcher-on-config-change"
);

const emitter = createEmitter();

const builder = await origCreateBuilder(
configPath,
{
configName: "reconnect-watcher-on-config-change.mjs",
outputDir,
},
emitter
);

await builder.build();
const watcher = await builder.watch();

emitter.emit("watcher:config-changed", {
filePath: configPath,
modification: "create",
});

await builder.build();

await watcher.unsubscribe();
});
});
});
Loading

0 comments on commit 207a3de

Please sign in to comment.