more
All checks were successful
Deploy to Pages / build (pull_request) Successful in 1m36s

This commit is contained in:
Andras Schmelczer 2026-05-25 12:52:37 +01:00
parent 84769f9ce4
commit fd4bb61b5f
30 changed files with 355 additions and 156 deletions

View file

@ -56,7 +56,7 @@ try {
}
const files = await walk(dist);
const htmlAndXml = files.filter((file) => /\.(html|xml)$/.test(file));
const checkedFiles = files.filter((file) => /\.(html|xml|css|webmanifest)$/.test(file));
function pagePathname(file) {
const rel = path.relative(dist, file).replaceAll(path.sep, '/');
@ -65,14 +65,52 @@ function pagePathname(file) {
return `/${rel}`;
}
for (const file of htmlAndXml) {
function collectUrlReferences(body, rel) {
const urls = [];
for (const match of body.matchAll(/\b(?:href|src|poster)=["']([^"']+)["']/g)) {
urls.push(match[1]);
}
for (const match of body.matchAll(/\bsrcset=["']([^"']+)["']/g)) {
for (const candidate of match[1].split(',')) {
const url = candidate.trim().split(/\s+/)[0];
if (url) urls.push(url);
}
}
if (rel.endsWith('.css')) {
for (const match of body.matchAll(/url\(\s*['"]?([^'")]+)['"]?\s*\)/g)) {
urls.push(match[1]);
}
}
if (rel.endsWith('.webmanifest')) {
try {
const manifest = JSON.parse(body);
for (const key of ['start_url', 'scope']) {
if (typeof manifest[key] === 'string') urls.push(manifest[key]);
}
for (const icon of manifest.icons ?? []) {
if (typeof icon?.src === 'string') urls.push(icon.src);
}
for (const screenshot of manifest.screenshots ?? []) {
if (typeof screenshot?.src === 'string') urls.push(screenshot.src);
}
} catch {
failures.push(`${rel}: invalid web manifest JSON`);
}
}
return urls;
}
for (const file of checkedFiles) {
const body = await readFile(file, 'utf8');
const rel = path.relative(dist, file);
const baseUrl = new URL(pagePathname(file), 'https://schmelczer.dev');
const matches = body.matchAll(/\b(?:href|src)=["']([^"'#?]+)(?:[?#][^"']*)?["']/g);
for (const match of matches) {
const raw = match[1];
for (const raw of collectUrlReferences(body, rel)) {
if (/^(mailto:|tel:|data:)/i.test(raw)) continue;
let parsed;

View file

@ -1,9 +1,10 @@
import { createServer } from 'node:http';
import { readdir, readFile, stat } from 'node:fs/promises';
import { mkdir, readdir, readFile, rm, stat } from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const dist = path.resolve('dist');
const browserTmp = path.resolve('.astro', 'playwright-overflow-tmp');
const INDEX_FILE = 'index.html';
const MAX_NAV_RETRIES = 4;
// Common device widths: iPhone SE / Galaxy S / iPhone 14 / iPad portrait /
@ -30,6 +31,7 @@ const MIME = {
'.woff2': 'font/woff2',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.vtt': 'text/vtt; charset=utf-8',
'.pdf': 'application/pdf',
};
@ -59,9 +61,6 @@ async function discoverRoutes() {
if (!file.endsWith('.html')) continue;
const rel = path.relative(dist, file).replaceAll(path.sep, '/');
if (rel === '404.html') continue;
// /writing/* are meta-refresh redirect stubs to /articles/*, not real
// pages; measuring them would just remeasure /articles/.
if (rel.startsWith('writing/')) continue;
if (rel === INDEX_FILE) {
routes.add('/');
} else if (rel.endsWith(`/${INDEX_FILE}`)) {
@ -104,6 +103,16 @@ try {
throw new Error('dist/ does not exist. Run npm run build first.');
}
// Some CI/dev containers mount /tmp as a very small tmpfs. Chromium uses the
// process temp directory for profiles and internal files; putting it under the
// already-ignored .astro/ directory keeps the overflow check reproducible even
// when the system temp mount is full.
await rm(browserTmp, { recursive: true, force: true });
await mkdir(browserTmp, { recursive: true });
process.env.TMPDIR = browserTmp;
process.env.TMP = browserTmp;
process.env.TEMP = browserTmp;
const routes = await discoverRoutes();
const server = createServer(async (req, res) => {
@ -125,6 +134,12 @@ const failures = [];
function launchBrowser() {
return chromium.launch({
headless: true,
env: {
...process.env,
TMPDIR: browserTmp,
TMP: browserTmp,
TEMP: browserTmp,
},
args: ['--disable-dev-shm-usage', '--disable-gpu', '--no-sandbox'],
});
}
@ -183,11 +198,11 @@ async function openBrowser() {
async function newMeasurementContext(browser, width) {
const context = await browser.newContext({
viewport: { width, height: 900 },
javaScriptEnabled: false,
javaScriptEnabled: true,
});
await context.route('**/*', (route) => {
const type = route.request().resourceType();
if (['font', 'image', 'media'].includes(type)) {
if (type === 'media') {
route.abort('blockedbyclient');
} else {
route.continue();
@ -281,6 +296,7 @@ try {
}
} finally {
server.close();
await rm(browserTmp, { recursive: true, force: true }).catch(() => {});
}
if (failures.length > 0) {