Files
waoowaoo/scripts/guards/task-loading-guard.mjs

133 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import process from 'process'
const workspaceRoot = process.cwd()
const baselinePath = path.join(workspaceRoot, 'scripts/guards/task-loading-baseline.json')
function walkFiles(dir, out = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next') continue
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
walkFiles(fullPath, out)
} else {
out.push(fullPath)
}
}
return out
}
function toPosixRelative(filePath) {
return path.relative(workspaceRoot, filePath).split(path.sep).join('/')
}
function collectMatches(files, pattern) {
const matches = []
for (const fullPath of files) {
if (!fullPath.endsWith('.ts') && !fullPath.endsWith('.tsx')) continue
const relPath = toPosixRelative(fullPath)
const content = fs.readFileSync(fullPath, 'utf8')
const lines = content.split('\n')
for (let i = 0; i < lines.length; i += 1) {
if (lines[i].includes(pattern)) {
matches.push(`${relPath}:${i + 1}`)
}
}
}
return matches
}
function fail(title, lines) {
console.error(`\n[task-loading-guard] ${title}`)
for (const line of lines) {
console.error(` - ${line}`)
}
process.exit(1)
}
if (!fs.existsSync(baselinePath)) {
fail('Missing baseline file', [toPosixRelative(baselinePath)])
}
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'))
const allowedFiles = new Set(baseline.allowedDirectTaskStateUsageFiles || [])
const allowedLegacyGeneratingFiles = new Set(baseline.allowedLegacyGeneratingUsageFiles || [])
const allFiles = walkFiles(path.join(workspaceRoot, 'src'))
const directTaskStateUsage = collectMatches(allFiles, 'useTaskTargetStates(')
const directUsageOutOfAllowlist = directTaskStateUsage
.map((entry) => entry.split(':')[0])
.filter((file) => !allowedFiles.has(file))
if (directUsageOutOfAllowlist.length > 0) {
fail(
'Found component-level direct useTaskTargetStates outside baseline allowlist',
Array.from(new Set(directUsageOutOfAllowlist)),
)
}
const crossDomainLabels = collectMatches(allFiles, 'video.panelCard.generating')
if (crossDomainLabels.length > 0) {
fail('Found cross-domain loading label reuse (video.panelCard.generating)', crossDomainLabels)
}
const uiFiles = allFiles.filter((file) => {
const relPath = toPosixRelative(file)
return relPath.startsWith('src/app/') || relPath.startsWith('src/components/')
})
const legacyGeneratingPatterns = [
'appearance.generating',
'panel.generatingImage',
'shot.generatingImage',
'line.generating',
]
const legacyGeneratingMatches = legacyGeneratingPatterns.flatMap((pattern) =>
collectMatches(uiFiles, pattern),
)
const legacyGeneratingOutOfAllowlist = legacyGeneratingMatches
.map((entry) => entry.split(':')[0])
.filter((file) => !allowedLegacyGeneratingFiles.has(file))
if (legacyGeneratingOutOfAllowlist.length > 0) {
fail(
'Found legacy generating truth usage in UI components',
Array.from(new Set(legacyGeneratingOutOfAllowlist)),
)
}
const hooksIndexPath = path.join(workspaceRoot, 'src/lib/query/hooks/index.ts')
if (fs.existsSync(hooksIndexPath)) {
const hooksIndex = fs.readFileSync(hooksIndexPath, 'utf8')
const bannedReexports = [
{
pattern: /export\s*\{[^}]*useGenerateCharacterImage[^}]*\}\s*from\s*['"]\.\/useGlobalAssets['"]/m,
message: 'hooks/index.ts must not export useGenerateCharacterImage from useGlobalAssets',
},
{
pattern: /export\s*\{[^}]*useGenerateLocationImage[^}]*\}\s*from\s*['"]\.\/useGlobalAssets['"]/m,
message: 'hooks/index.ts must not export useGenerateLocationImage from useGlobalAssets',
},
{
pattern: /export\s*\{[^}]*useGenerateProjectCharacterImage[^}]*\}\s*from\s*['"]\.\/useProjectAssets['"]/m,
message: 'hooks/index.ts must not export useGenerateProjectCharacterImage from useProjectAssets',
},
{
pattern: /export\s*\{[^}]*useGenerateProjectLocationImage[^}]*\}\s*from\s*['"]\.\/useProjectAssets['"]/m,
message: 'hooks/index.ts must not export useGenerateProjectLocationImage from useProjectAssets',
},
]
const violations = bannedReexports
.filter((item) => item.pattern.test(hooksIndex))
.map((item) => item.message)
if (violations.length > 0) {
fail('Found non-canonical mutation re-exports', violations)
}
}
console.log('[task-loading-guard] OK')