Skip to content

Commit

Permalink
Watch for file changes 👁️
Browse files Browse the repository at this point in the history
  • Loading branch information
01taylop committed Oct 28, 2024
1 parent 06f4c02 commit c226e8a
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 77 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { readFile } from 'fs'
import { readFile } from 'node:fs'

import chokidar from 'chokidar'

import { Events } from '@Types'
import { EVENTS, fileChangeEvent, watchFiles } from '../watch-files'

import { fileChangeEvent, watchFiles } from '../watchFiles'

jest.mock('fs')
jest.mock('node:fs')
jest.mock('chokidar')

describe('watchFiles', () => {
let mockWatcher: chokidar.FSWatcher

const getEventPromise = (eventHandler: jest.Mock): Promise<void> => new Promise<void>((resolve) => {
fileChangeEvent.on(EVENTS.FILE_CHANGED, (params: Record<string, unknown>) => {
eventHandler(params)
resolve()
})
})

const saveFile = (path: string, content: string, event: 'add' | 'change' | 'unlink') => {
jest.mocked(readFile).mockImplementationOnce((_path: any, _encoding: any, callback?: (error: any, data: any) => void): void => {
if (callback) {
Expand Down Expand Up @@ -40,7 +45,7 @@ describe('watchFiles', () => {
fileChangeEvent.removeAllListeners()
})

it('initialises chokidar with the correct file and ignore patterns', () => {
it('initialises chokidar with the file patterns and ignore patterns', () => {
const filePatterns = ['**/*.ts']
const ignorePatterns = ['node_modules']

Expand All @@ -53,103 +58,106 @@ describe('watchFiles', () => {
})
})

it('emits a "FILE_CHANGED" event when saving an existing file (because there is no hash map yet)', done => {
expect.assertions(1)
it('emits a "FILE_CHANGED" event when saving an existing file (because there is no hash map yet)', async () => {
expect.assertions(2)

const eventHandler = jest.fn()
const eventPromise = getEventPromise(eventHandler)
const mockPath = 'mock/existing-file.ts'

fileChangeEvent.on(Events.FILE_CHANGED, params => {
expect(params).toStrictEqual({
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
done()
})

watchFiles({ filePatterns: [mockPath], ignorePatterns: [] })

saveFile(mockPath, 'hello-world', 'change')

await eventPromise

expect(eventHandler).toHaveBeenCalledTimes(1)
expect(eventHandler).toHaveBeenNthCalledWith(1, {
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
})

it('emits a "FILE_CHANGED" event when saving a file if the file content changes', done => {
expect.assertions(2)
it('emits a "FILE_CHANGED" event when saving a file if the file content changes', async () => {
expect.assertions(3)

const eventHandler = jest.fn()
const eventPromise = getEventPromise(eventHandler)
const mockPath = 'mock/update-file.ts'

fileChangeEvent.on(Events.FILE_CHANGED, params => {
expect(params).toStrictEqual({
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
})

watchFiles({ filePatterns: [mockPath], ignorePatterns: [] })

saveFile(mockPath, 'old-content', 'change') // First save - no hash map yet
saveFile(mockPath, 'new-content', 'change') // Second save - content hash is different

setTimeout(() => {
done()
}, 100)
await eventPromise

expect(eventHandler).toHaveBeenCalledTimes(2)
expect(eventHandler).toHaveBeenNthCalledWith(1, {
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
expect(eventHandler).toHaveBeenNthCalledWith(2, {
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
})

it('does not emit a "FILE_CHANGED" event when saving a file if the file content did not change', done => {
expect.assertions(1)
it('does not emit a "FILE_CHANGED" event when saving a file if the file content did not change', async () => {
expect.assertions(2)

const eventHandler = jest.fn()
const eventPromise = getEventPromise(eventHandler)
const mockPath = 'mock/unchanged-file.ts'

fileChangeEvent.on(Events.FILE_CHANGED, params => {
expect(params).toStrictEqual({
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
})

watchFiles({ filePatterns: [mockPath], ignorePatterns: [] })

saveFile(mockPath, 'old-content', 'change') // First save - no hash map yet
saveFile(mockPath, 'old-content', 'change') // Second save - content hash is the same
saveFile(mockPath, 'old-content', 'change') // Third save - content hash is still the same

setTimeout(() => {
done()
}, 100)
await eventPromise

expect(eventHandler).toHaveBeenCalledTimes(1)
expect(eventHandler).toHaveBeenNthCalledWith(1, {
message: `File \`${mockPath}\` has been changed.`,
path: mockPath,
})
})

it('emits a "FILE_CHANGED" event when a new file is added', done => {
expect.assertions(1)
it('emits a "FILE_CHANGED" event when a new file is added', async () => {
expect.assertions(2)

const eventHandler = jest.fn()
const eventPromise = getEventPromise(eventHandler)
const mockPath = 'mock/new-file.ts'

fileChangeEvent.on(Events.FILE_CHANGED, params => {
expect(params).toStrictEqual({
message: `File \`${mockPath}\` has been added.`,
path: mockPath,
})
done()
})

watchFiles({ filePatterns: [mockPath], ignorePatterns: [] })

saveFile(mockPath, 'new-content', 'add')

await eventPromise

expect(eventHandler).toHaveBeenCalledTimes(1)
expect(eventHandler).toHaveBeenNthCalledWith(1, {
message: `File \`${mockPath}\` has been added.`,
path: mockPath,
})
})

it('emits a "FILE_CHANGED" event when a file is removed', done => {
expect.assertions(1)
it('emits a "FILE_CHANGED" event when a file is removed', async () => {
expect.assertions(2)

const eventHandler = jest.fn()
const eventPromise = getEventPromise(eventHandler)
const mockPath = 'mock/legacy-file.ts'

fileChangeEvent.on(Events.FILE_CHANGED, params => {
expect(params).toStrictEqual({
message: `File \`${mockPath}\` has been removed.`,
path: mockPath,
})
done()
})

watchFiles({ filePatterns: [mockPath], ignorePatterns: [] })

saveFile(mockPath, 'legacy-content', 'unlink')

await eventPromise

expect(eventHandler).toHaveBeenCalledTimes(1)
expect(eventHandler).toHaveBeenNthCalledWith(1, {
message: `File \`${mockPath}\` has been removed.`,
path: mockPath,
})
})

})
27 changes: 25 additions & 2 deletions src/commands/lint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import colourLog from '@Utils/colour-log'
import { clearTerminal } from '@Utils/terminal'

import { getFilePatterns } from './file-patterns'
import { EVENTS, fileChangeEvent, watchFiles } from './watch-files'

const lint = ({ clearCache, debug, emoji, eslintInclude, ignoreDirs, ignorePatterns, title, ...options }: LintOptions) => {
const lint = ({ cache, clearCache, debug, emoji, eslintInclude, eslintUseLegacyConfig, fix, ignoreDirs, ignorePatterns, title, watch }: LintOptions) => {
global.debug = debug

clearTerminal()
Expand All @@ -21,7 +22,29 @@ const lint = ({ clearCache, debug, emoji, eslintInclude, ignoreDirs, ignorePatte
ignorePatterns,
})

console.log('Run Lint', options, filePatterns)
const lintOptions = {
cache,
eslintUseLegacyConfig,
filePatterns,
fix,
title,
watch,
}

console.log('Run Lint', lintOptions) // TODO: Run lint

if (watch) {
watchFiles({
filePatterns: Object.values(filePatterns.includePatterns).flat(),
ignorePatterns: filePatterns.ignorePatterns,
})

fileChangeEvent.on(EVENTS.FILE_CHANGED, ({ message }) => {
clearTerminal()
colourLog.info(`${message}\n`)
console.log('Run Lint', lintOptions) // TODO: Run lint
})
}
}

export default lint
21 changes: 11 additions & 10 deletions src/watchFiles.ts → src/commands/lint/watch-files.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createHash } from 'crypto'
import { EventEmitter } from 'events'
import { readFile } from 'fs'
import { createHash } from 'node:crypto'
import { EventEmitter } from 'node:events'
import { readFile } from 'node:fs'

import chokidar from 'chokidar'

import { Events } from '@Types'
enum EVENTS {
FILE_CHANGED = 'FILE_CHANGED',
}

interface WatchFiles {
filePatterns: Array<string>
Expand All @@ -15,8 +17,6 @@ const fileChangeEvent = new EventEmitter()

const fileHashes = new Map<string, string>()

const getHash = (data: string) => createHash('md5').update(data).digest('hex')

const watchFiles = ({ filePatterns, ignorePatterns }: WatchFiles) => {
const watcher = chokidar.watch(filePatterns, {
ignored: ignorePatterns,
Expand All @@ -25,18 +25,18 @@ const watchFiles = ({ filePatterns, ignorePatterns }: WatchFiles) => {
})

watcher.on('add', (path, _stats) => {
fileChangeEvent.emit(Events.FILE_CHANGED, {
fileChangeEvent.emit(EVENTS.FILE_CHANGED, {
message: `File \`${path}\` has been added.`,
path,
})
})

watcher.on('change', (path, _stats) => {
readFile(path, 'utf8', (_error, data) => {
const newHash = getHash(data)
const newHash = createHash('md5').update(data).digest('hex')
if (fileHashes.get(path) !== newHash) {
fileHashes.set(path, newHash)
fileChangeEvent.emit(Events.FILE_CHANGED, {
fileChangeEvent.emit(EVENTS.FILE_CHANGED, {
message: `File \`${path}\` has been changed.`,
path,
})
Expand All @@ -45,14 +45,15 @@ const watchFiles = ({ filePatterns, ignorePatterns }: WatchFiles) => {
})

watcher.on('unlink', path => {
fileChangeEvent.emit(Events.FILE_CHANGED, {
fileChangeEvent.emit(EVENTS.FILE_CHANGED, {
message: `File \`${path}\` has been removed.`,
path,
})
})
}

export {
EVENTS,
fileChangeEvent,
watchFiles,
}

0 comments on commit c226e8a

Please sign in to comment.