Files
teknav-bak/app/scripts/production-readiness.mjs
arsam a202e253fd Import source: app/scripts/production-readiness.mjs
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:23:42 +03:30

145 lines
4.4 KiB
JavaScript

import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const checks = [];
function record(level, label, detail = '') {
checks.push({ level, label, detail });
}
function pass(label, detail = '') {
record('PASS', label, detail);
}
function warn(label, detail = '') {
record('WARN', label, detail);
}
function fail(label, detail = '') {
record('FAIL', label, detail);
}
function file(rel) {
return path.join(root, rel);
}
function read(rel) {
return readFileSync(file(rel), 'utf8');
}
function requireFile(rel, label = rel) {
if (existsSync(file(rel))) pass(label);
else fail(label, `${rel} is missing`);
}
function requireText(rel, patterns, label = rel) {
if (!existsSync(file(rel))) {
fail(label, `${rel} is missing`);
return;
}
const text = read(rel);
for (const pattern of patterns) {
if (typeof pattern === 'string' ? text.includes(pattern) : pattern.test(text)) {
pass(`${label}: ${pattern.toString()}`);
} else {
fail(`${label}: ${pattern.toString()}`, 'not found');
}
}
}
function walk(rel, predicate, hits = []) {
const abs = file(rel);
if (!existsSync(abs)) return hits;
for (const name of readdirSync(abs)) {
if (['node_modules', 'dist', '.npm-cache'].includes(name)) continue;
const childRel = path.join(rel, name);
const childAbs = file(childRel);
const stat = statSync(childAbs);
if (stat.isDirectory()) walk(childRel, predicate, hits);
else if (predicate(childRel)) hits.push(childRel);
}
return hits;
}
requireFile('package.json');
requireFile('backend/package.json');
requireFile('backend/prisma/schema.prisma');
requireFile('.env.prod.example');
requireFile('backend/.env.example');
requireFile('nginx/nginx.conf.example');
requireFile('DEPLOY.md');
requireFile('SECURITY.md');
requireText('package.json', [
'"check:no-cdn"',
'"check:responsive"',
'"check:seo"',
'"preflight:prod"',
'generate-article-pages',
'compress-dist',
], 'frontend scripts');
requireText('backend/package.json', [
'"build": "tsc"',
'"test": "node --test tests/*.test.mjs"',
'"prisma:generate"',
'"prisma:apply"',
'"seed"',
], 'backend scripts');
requireText('.env.prod.example', [
'DATABASE_URL=',
'REDIS_URL=',
'SESSION_SECRET=',
'COOKIE_DOMAIN=".teknav.ir"',
'CORS_ORIGINS="https://www.teknav.ir,https://teknav.ir"',
'UPLOAD_DIR="/var/lib/teknav/uploads"',
'VAPID_PUBLIC_KEY=',
'VAPID_PRIVATE_KEY=',
'ZARINPAL_MERCHANT_ID=',
'STORAGE_BACKEND="local"',
], 'production env template');
requireText('backend/.env.example', [
'SMTP_HOST=',
'SMS_PROVIDER=',
'VAPID_PUBLIC_KEY=',
'ZARINPAL_MERCHANT_ID=',
'STORAGE_BACKEND=local',
], 'backend env example');
requireText('nginx/nginx.conf.example', [
'location = /ACCOUNTS.md',
/location ~ \^\/api\/\(admin\/stream\|auth\/notifications\/stream\)\$/,
/proxy_buffering\s+off;/,
'gzip_static on',
], 'nginx production template');
const accountLeaks = ['public/ACCOUNTS.md', 'dist/ACCOUNTS.md'].filter((rel) => existsSync(file(rel)));
if (accountLeaks.length) fail('operator credentials not public', accountLeaks.join(', '));
else pass('operator credentials not public');
const migrationsDir = file('backend/prisma/migrations');
if (existsSync(migrationsDir)) pass('Prisma migrations directory present');
else warn('Prisma migrations directory absent', 'production currently relies on prisma:apply/db push fallback');
const engineArtifacts = walk('backend', (rel) =>
/(^|\/)(query_engine.*\.node(\.gz)?|schema-engine.*(\.gz)?)$/.test(rel),
);
if (engineArtifacts.length) fail('no generated Prisma engine artifacts committed', engineArtifacts.join(', '));
else pass('no generated Prisma engine artifacts committed');
const backendTests = walk('backend/tests', (rel) => rel.endsWith('.test.mjs'));
if (backendTests.length >= 2) pass('backend test coverage files', `${backendTests.length} test files`);
else warn('backend test coverage files', `${backendTests.length} test file(s); add more domain tests`);
const output = checks.map(({ level, label, detail }) => `${level} ${label}${detail ? ` - ${detail}` : ''}`);
console.log(output.join('\n'));
const failures = checks.filter((item) => item.level === 'FAIL');
const warnings = checks.filter((item) => item.level === 'WARN');
console.log(`\nProduction readiness: ${failures.length} failure(s), ${warnings.length} warning(s).`);
if (failures.length) process.exit(1);