212 lines
4.6 KiB
JavaScript
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)
|