Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
145 lines
4.4 KiB
JavaScript
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);
|