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.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user