Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ jobs:
test -s "out/$f" || { echo "::error::out/$f is missing"; exit 1; }
done

# 🛡️ Hard limit: проверка веса JS-бандлов на каждой странице
- name: Check bundle size limit
run: npm run check:size

- uses: actions/upload-pages-artifact@v5
if: github.event_name != 'pull_request'
with:
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
"name": "shapak-apps-site",
"version": "0.1.0",
"private": true,
"scripts": {
"scripts": {
"dev": "next dev",
"build": "next build"
"build": "next build",
"check:size": "node scripts/check-size.mjs"
},
"dependencies": {
"next": "16.3.6",
Expand Down
117 changes: 117 additions & 0 deletions scripts/check-size.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// scripts/check-size.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import zlib from 'node:zlib';

// --- CONFIG ---
// Per-page gzip budget in KB. Single constant, as required by issue #6.
const LIMIT_KB = 250;
const OUT_DIR = 'out';

async function findHtmlFiles(dir) {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return [];
}

const nested = await Promise.all(
entries.map((entry) => {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) return findHtmlFiles(fullPath);
if (entry.isFile() && entry.name === 'index.html') return fullPath;
return null;
}),
);
return nested.flat().filter(Boolean);
}

// No try/catch here: a missing file must throw, never silently count as 0 KB.
async function getGzipSize(filePath) {
const buffer = await fs.readFile(filePath);
return zlib.gzipSync(buffer).length;
}

async function main() {
const htmlFiles = await findHtmlFiles(OUT_DIR);

if (htmlFiles.length === 0) {
console.error(`❌ No index.html files found in ${OUT_DIR}/. Did you run 'npm run build' first?`);
process.exit(1);
}

const results = [];
const missing = [];
let hasFailed = false;

for (const htmlFile of htmlFiles) {
const html = await fs.readFile(htmlFile, 'utf8');

const relDir = path.relative(OUT_DIR, path.dirname(htmlFile));
const route = relDir === '' ? '/' : '/' + relDir.split(path.sep).join('/') + '/';

const regex = /<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g;
const scriptPaths = new Set();
let match;
while ((match = regex.exec(html)) !== null) {
const src = match[1];
// Only local Next.js assets: a full URL must never map to a local path.
if (!src.startsWith('/_next/')) continue;
const cleanSrc = src.substring(1).split('?')[0].split('#')[0];
scriptPaths.add(path.join(OUT_DIR, cleanSrc));
}

let totalSize = 0;
for (const scriptPath of scriptPaths) {
try {
totalSize += await getGzipSize(scriptPath);
} catch (err) {
// A referenced chunk that does not exist is a FATAL error, not 0 KB.
if (err.code === 'ENOENT') {
missing.push({ route, file: scriptPath });
} else {
throw err;
}
}
}

const sizeKB = Math.round((totalSize / 1024) * 100) / 100;
const isOverLimit = sizeKB > LIMIT_KB;
if (isOverLimit) hasFailed = true;
results.push({ route, sizeKB, isOverLimit });
}

results.sort((a, b) => a.route.localeCompare(b.route));

console.log(`Page | KB gzip (Limit: ${LIMIT_KB} KB)`);
console.log('-------------- | ---------------------------');
for (const res of results) {
const icon = res.isOverLimit ? '❌' : '✅';
const missCount = missing.filter((m) => m.route === res.route).length;
const note = missCount > 0 ? ` ⚠️ ${missCount} missing` : '';
console.log(`${res.route.padEnd(14)} | ${res.sizeKB} KB ${icon}${note}`);
}

let failed = hasFailed;

if (missing.length > 0) {
console.error(`\n❌ Error: ${missing.length} referenced file(s) missing from ${OUT_DIR}/:`);
for (const m of missing) {
console.error(` page ${m.route} -> ${m.file}`);
}
failed = true;
}

if (hasFailed) {
console.error(`\n❌ Error: one or more pages exceed the limit of ${LIMIT_KB} KB gzip.`);
}

if (failed) process.exit(1);
console.log(`\n✅ Success: all pages are under the limit of ${LIMIT_KB} KB gzip.`);
}

main().catch((err) => {
console.error('Unexpected error:', err);
process.exit(1);
});
Loading