openpencil/scripts/check-package-metadata.ts
2026-05-30 19:14:12 +03:00

93 lines
2.6 KiB
TypeScript

import { readFileSync } from 'node:fs'
import { join } from 'node:path'
const publicPackages = ['packages/core', 'packages/cli', 'packages/mcp', 'packages/vue']
interface PackageJson {
name: string
main?: string
types?: string
files?: string[]
bin?: Record<string, string> | string
exports?: unknown
publishConfig?: Record<string, unknown>
}
const errors: string[] = []
function readPackageJson(packageDir: string): PackageJson {
return JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'))
}
function checkRuntimePath(packageName: string, field: string, value: string): void {
if (value.endsWith('.ts') && !value.endsWith('.d.ts')) {
errors.push(`${packageName}: ${field} must not point to runtime TypeScript (${value})`)
}
if (value.startsWith('./src/')) {
errors.push(`${packageName}: ${field} must not point to source files (${value})`)
}
}
function checkIncludedRuntimePath(
packageName: string,
field: string,
value: string,
files: string[]
): void {
checkRuntimePath(packageName, field, value)
const normalized = value.replace(/^\.\//, '')
const topLevelDir = normalized.split('/')[0]
if (topLevelDir && !files.includes(topLevelDir)) {
errors.push(
`${packageName}: ${field} points to ${value}, but files does not include ${topLevelDir}`
)
}
}
function walkExports(packageName: string, value: unknown, path: string[] = []): void {
if (typeof value === 'string') {
const key = path.at(-1)
if (key !== 'types' && key !== 'bun')
checkRuntimePath(packageName, `exports.${path.join('.')}`, value)
return
}
if (!value || typeof value !== 'object') return
for (const [key, child] of Object.entries(value)) {
walkExports(packageName, child, [...path, key])
}
}
for (const packageDir of publicPackages) {
const pkg = readPackageJson(packageDir)
if (!pkg.files?.includes('dist')) {
errors.push(`${pkg.name}: files must include dist`)
}
if (pkg.main) checkRuntimePath(pkg.name, 'main', pkg.main)
if (typeof pkg.bin === 'string') {
checkIncludedRuntimePath(pkg.name, 'bin', pkg.bin, pkg.files ?? [])
} else if (pkg.bin) {
for (const [name, target] of Object.entries(pkg.bin)) {
checkIncludedRuntimePath(pkg.name, `bin.${name}`, target, pkg.files ?? [])
}
}
walkExports(pkg.name, pkg.exports)
if (
pkg.publishConfig &&
('exports' in pkg.publishConfig || 'main' in pkg.publishConfig || 'types' in pkg.publishConfig)
) {
errors.push(`${pkg.name}: publishConfig must not rewrite runtime entrypoints`)
}
}
if (errors.length > 0) {
console.error(errors.join('\n'))
process.exit(1)
}
console.log('Package metadata is publish-safe.')