133 lines
4.4 KiB
JavaScript
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')
|