next-intl
Version:
Internationalization (i18n) for Next.js
133 lines (126 loc) • 4.13 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { subscribe } from '@parcel/watcher';
import SourceFileFilter from './SourceFileFilter.js';
import SourceFileScanner from './SourceFileScanner.js';
class SourceFileWatcher {
subscriptions = [];
constructor(roots, onChange) {
this.roots = roots;
this.onChange = onChange;
}
async start() {
if (this.subscriptions.length > 0) {
return;
}
const ignore = SourceFileFilter.IGNORED_DIRECTORIES.map(dir => `**/${dir}/**`);
for (const root of this.roots) {
const sub = await subscribe(root, async (err, events) => {
if (err) {
console.error(err);
return;
}
const filtered = await this.normalizeEvents(events);
if (filtered.length > 0) {
void this.onChange(filtered);
}
}, {
ignore
});
this.subscriptions.push(sub);
}
}
async normalizeEvents(events) {
const directoryCreatePaths = [];
const otherEvents = [];
// We need to expand directory creates because during rename operations,
// @parcel/watcher emits a directory create event but may not emit individual
// file events for the moved files
await Promise.all(events.map(async event => {
if (event.type === 'create') {
try {
const stats = await fs.stat(event.path);
if (stats.isDirectory()) {
directoryCreatePaths.push(event.path);
return;
}
} catch {
// Path doesn't exist or is inaccessible, treat as file
}
}
otherEvents.push(event);
}));
// Expand directory create events to find source files inside
let expandedCreateEvents = [];
if (directoryCreatePaths.length > 0) {
try {
const sourceFiles = await SourceFileScanner.getSourceFiles(directoryCreatePaths);
expandedCreateEvents = Array.from(sourceFiles).map(filePath => ({
type: 'create',
path: filePath
}));
} catch {
// Directories might have been deleted or are inaccessible
}
}
// Combine original events with expanded directory creates.
// Deduplicate by path to avoid processing the same file twice
// in case @parcel/watcher also emitted individual file events.
const allEvents = [...otherEvents, ...expandedCreateEvents];
const seenPaths = new Set();
const deduplicated = [];
for (const event of allEvents) {
const key = `${event.type}:${event.path}`;
if (!seenPaths.has(key)) {
seenPaths.add(key);
deduplicated.push(event);
}
}
return deduplicated.filter(event => {
// Keep all delete events (might be deleted directories that no longer exist)
if (event.type === 'delete') {
return true;
}
// Keep source files
return SourceFileFilter.isSourceFile(event.path);
});
}
async expandDirectoryDeleteEvents(events, prevKnownFiles) {
const expanded = [];
for (const event of events) {
if (event.type === 'delete' && !SourceFileFilter.isSourceFile(event.path)) {
const dirPath = path.resolve(event.path);
const filesInDirectory = [];
for (const filePath of prevKnownFiles) {
if (SourceFileFilter.isWithinPath(filePath, dirPath)) {
filesInDirectory.push(filePath);
}
}
// If we found files within this path, it was a directory
if (filesInDirectory.length > 0) {
for (const filePath of filesInDirectory) {
expanded.push({
type: 'delete',
path: filePath
});
}
} else {
// Not a directory or no files in it, pass through as-is
expanded.push(event);
}
} else {
// Pass through as-is
expanded.push(event);
}
}
return expanded;
}
async stop() {
await Promise.all(this.subscriptions.map(sub => sub.unsubscribe()));
this.subscriptions = [];
}
[Symbol.dispose]() {
void this.stop();
}
}
export { SourceFileWatcher as default };