Files
ghost.sori.studio/scripts/dev-watch.js

212 lines
4.6 KiB
JavaScript

const fs = require('fs')
const path = require('path')
const { spawn } = require('child_process')
const rootDir = path.resolve(__dirname, '..')
const ignoredDirectories = new Set([
'.git',
'.cursor',
'.vscode',
'.docker',
'docs',
'local-ghost',
'node_modules',
'seed',
'theme-export'
])
const ignoredFiles = new Set([
'.DS_Store'
])
const syncExtensions = new Set([
'.css',
'.hbs',
'.js',
'.json',
'.svg',
'.yaml',
'.yml'
])
const watchedRoots = [rootDir]
let syncTimer = null
let syncProcess = null
let scanTimer = null
let previousSnapshot = new Map()
/**
* @param {string} relativePath
*/
function shouldIgnorePath(relativePath) {
if (!relativePath || relativePath === '.') {
return false
}
const segments = relativePath.split(path.sep)
return segments.some((segment) => ignoredDirectories.has(segment) || ignoredFiles.has(segment))
}
/**
* @param {string} relativePath
*/
function shouldSyncFile(relativePath) {
if (shouldIgnorePath(relativePath)) {
return false
}
if (relativePath === path.join('assets', 'styles', 'tailwind.css')) {
return false
}
const extension = path.extname(relativePath)
return syncExtensions.has(extension)
}
/**
* @param {string} command
* @param {string[]} args
* @param {(code: number | null) => void} [onExit]
*/
function runCommand(command, args, onExit) {
const child = spawn(command, args, {
cwd: rootDir,
stdio: 'inherit',
shell: false
})
child.on('exit', (code) => {
if (onExit) {
onExit(code)
}
})
return child
}
function queueSync() {
if (syncTimer) {
clearTimeout(syncTimer)
}
syncTimer = setTimeout(() => {
if (syncProcess) {
queueSync()
return
}
console.log('[dev:watch] theme sync')
syncProcess = runCommand('npm', ['run', 'dev:sync'], (code) => {
syncProcess = null
if (code !== 0) {
console.error(`[dev:watch] sync failed with code ${code}`)
}
})
}, 180)
}
/**
* @param {string} directoryPath
* @param {Map<string, number>} snapshot
*/
function collectSnapshot(directoryPath, snapshot) {
if (!fs.existsSync(directoryPath)) {
return
}
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
const entryPath = path.join(directoryPath, entry.name)
const relativePath = path.relative(rootDir, entryPath)
if (shouldIgnorePath(relativePath)) {
continue
}
if (entry.isDirectory()) {
collectSnapshot(entryPath, snapshot)
continue
}
if (shouldSyncFile(relativePath)) {
snapshot.set(relativePath, fs.statSync(entryPath).mtimeMs)
}
}
}
function buildSnapshot() {
const snapshot = new Map()
for (const watchedRoot of watchedRoots) {
collectSnapshot(watchedRoot, snapshot)
}
return snapshot
}
function scanForChanges() {
const nextSnapshot = buildSnapshot()
for (const [relativePath, modifiedTime] of nextSnapshot.entries()) {
if (previousSnapshot.get(relativePath) !== modifiedTime) {
console.log(`[dev:watch] change detected: ${relativePath}`)
queueSync()
previousSnapshot = nextSnapshot
return
}
}
for (const relativePath of previousSnapshot.keys()) {
if (!nextSnapshot.has(relativePath)) {
console.log(`[dev:watch] change detected: ${relativePath}`)
queueSync()
previousSnapshot = nextSnapshot
return
}
}
previousSnapshot = nextSnapshot
}
function startPolling() {
previousSnapshot = buildSnapshot()
scanTimer = setInterval(scanForChanges, 800)
}
function shutdown() {
if (syncTimer) {
clearTimeout(syncTimer)
}
if (tailwindProcess && !tailwindProcess.killed) {
tailwindProcess.kill('SIGINT')
}
if (syncProcess && !syncProcess.killed) {
syncProcess.kill('SIGINT')
}
if (scanTimer) {
clearInterval(scanTimer)
}
process.exit(0)
}
console.log('[dev:watch] initial prepare')
runCommand('npm', ['run', 'dev:prepare'], (prepareCode) => {
if (prepareCode !== 0) {
process.exit(prepareCode || 1)
}
console.log('[dev:watch] tailwind watch start')
tailwindProcess = runCommand('npm', ['run', 'build:tailwind', '--', '--watch'])
console.log('[dev:watch] filesystem watch start')
startPolling()
})
let tailwindProcess = null
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)