Skip to content

Exclude .remote directory from preview file watcher - #463

Open
atm-snag2 wants to merge 1 commit into
mainfrom
exclude-remote-dir-from-preview-watcher
Open

atm-snag2 wants to merge 1 commit into
mainfrom
exclude-remote-dir-from-preview-watcher

Conversation

@atm-snag2

Copy link
Copy Markdown
Contributor

What

Make the file watcher of qiita preview actually ignore the .remote directory, so that changes under it no longer send local changed to the browser.

How

  • Since chokidar 4, ignored compares a string with the whole path. So the watcher lists <watched path>/.remote itself. chokidar does not descend into an ignored directory, and a directory named .remote above the watched path is not affected. chokidar 5.0.0 handles ignored in the same way, so this keeps working after Bump chokidar from 4.0.3 to 5.0.0 #448.
  • src/server/app.test.ts stubs chokidar and checks the ignored option passed to chokidar.watch, in the same way as the existing tests stub chalk. Jest cannot load the ESM-only chokidar 5.0.0 without --experimental-vm-modules, so a test with the real watcher would break Bump chokidar from 4.0.3 to 5.0.0 #448. Whether chokidar honors the option is checked with the real watcher on 4.0.3 and 5.0.0, and the result is in a comment.

Why

ignored has "**/.remote/**", written as a glob. chokidar dropped glob support in 4.0.0 and now compares strings with the whole path, so this entry matches no path. It was added in #339, after chokidar was bumped to 4.0.3 in #234, so it has had no effect since it was added.

As a result, a write under .remote sends local changed to the browser, and the client fetches the item again. This happens when an item is published from the preview (it also writes .remote), and when another process such as npx qiita pull syncs while the preview is running. Nothing breaks, but the watcher does not do what the config says.

Refs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@atm-snag2 atm-snag2 self-assigned this Oct 23, 2026
@atm-snag2

atm-snag2 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Verification results for 2798536.

1. Lint, tests, and build

pnpm run lint, pnpm run test (twice), and pnpm run build pass on Node 24.22.1.

Test Suites: 27 passed, 27 total
Tests:       251 passed, 251 total

2. The new tests catch the old config

With ignored set back to [/node_modules|\.git/, "**/.remote/**"], all 3 tests in src/server/app.test.ts fail.

Tests:       3 failed, 3 total

3. The real watcher on chokidar 4.0.3 and 5.0.0

I built dist/ and called startLocalChangeWatcher directly with the script below, then counted the local changed messages that a WebSocket client received.
For 5.0.0, I pointed node_modules/chokidar to a copy of 5.0.0 for the run and restored it afterwards.

Operation chokidar 4.0.3 chokidar 5.0.0
Change a.md 1 1
Add b.md 1 1
Unlink b.md 1 1
Add .remote/c.md 0 0

With the old ignored in the built dist/server/app.js (4.0.3), adding .remote/c.md sent 1 message.

Script
// Usage: node verify-watcher.js <qiita-cli dir>
const path = require("node:path");
const fs = require("node:fs");
const os = require("node:os");
const { createServer } = require("node:http");

const root = path.resolve(process.argv[2]);
const { startLocalChangeWatcher } = require(path.join(root, "dist/server/app.js"));
const WebSocket = require(path.join(root, "node_modules/ws"));
console.log("chokidar", require(path.join(root, "node_modules/chokidar/package.json")).version);

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

(async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "watch-"));
  fs.mkdirSync(path.join(dir, ".remote"));
  fs.writeFileSync(path.join(dir, "a.md"), "first\n");

  const server = createServer();
  await new Promise((r) => server.listen(0, "127.0.0.1", r));
  await startLocalChangeWatcher({ server, watchPaths: dir });

  const ws = new WebSocket(`ws://127.0.0.1:${server.address().port}`);
  const received = [];
  ws.on("message", (m) => received.push(m.toString()));
  await new Promise((r) => ws.on("open", r));
  await sleep(1000); // initial scan

  const step = async (label, fn) => {
    const before = received.length;
    fn();
    await sleep(1000);
    console.log(`${label}: ${received.length - before} message(s)`, received.slice(before));
  };

  received.length = 0; // ignore events from the initial scan
  await step("change a.md", () => fs.appendFileSync(path.join(dir, "a.md"), "second\n"));
  await step("add b.md", () => fs.writeFileSync(path.join(dir, "b.md"), "new\n"));
  await step("unlink b.md", () => fs.unlinkSync(path.join(dir, "b.md")));
  await step("add .remote/c.md", () => fs.writeFileSync(path.join(dir, ".remote/c.md"), "remote\n"));

  ws.close();
  server.close();
  fs.rmSync(dir, { recursive: true, force: true });
  process.exit(0);
})();

4. Jest with chokidar 5.0.0

With node_modules/chokidar pointed to 5.0.0, src/server/app.test.ts and the tests under src/commands pass, because they stub chokidar.

Tests:       42 passed, 42 total

5. Publishing and updating an item from qiita preview

I ran qiita preview (2798536) against a local Qiita API, and called POST /api/items/:id to publish a new public item and then update it.
Next to the preview, I ran two watchers on the same root and logged their events: one with the old ignored, and one with the new one.

Operation Old ignored New ignored
Publish a new item change <name>.md ×2, add .remote/<id>.md change <name>.md ×2
Update the item change .remote/<id>.md, change <name>.md change <name>.md

I also logged the paths that the preview's own watcher notified.
In a second run, it notified only <name>.md: 2 times for publishing and 1 time for updating, and never .remote/<id>.md.

Notes:

  • Publishing writes the item file twice: once to set its id, and once when the item is synced from the response. So publishing sends 2 messages even without .remote.
  • On macOS, the item file itself sometimes emits two change events 50 to 140 ms apart, so one write can send two messages. This also happens when the file is only edited locally, and it is unrelated to .remote.

Not verified

  • How the browser reloads after the messages.

@atm-snag2
atm-snag2 marked this pull request as ready for review October 26, 2026 00:43
@atm-snag2
atm-snag2 requested review from a team and inukai-masanori and removed request for a team October 26, 2026 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant