Skip to content
Open
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"verify": "npm run build && node scripts/check-charter.mjs && node scripts/verify-four-layer.mjs && node scripts/generate-sweep.mjs && node scripts/verify-sweep.mjs && node scripts/verify-cross-family.mjs && node scripts/verify-ua-styles.mjs && node scripts/verify-rect-contract.mjs && node scripts/verify-stress.mjs",
"verify:all": "npm run build && node scripts/check-charter.mjs && node scripts/verify-four-layer.mjs && node scripts/generate-sweep.mjs && node scripts/verify-sweep.mjs && node scripts/verify-cross-family.mjs && node scripts/verify-ua-styles.mjs && node scripts/verify-text-measure.mjs && node scripts/verify-calc.mjs && node scripts/verify-shadow.mjs && node scripts/verify-opacity.mjs && node scripts/verify-layers.mjs && node scripts/verify-paint-text.mjs && node scripts/verify-border-radius.mjs && node scripts/verify-layout-flexbox.mjs && node scripts/verify-layout-floats.mjs && node scripts/verify-layout-grid.mjs && node scripts/verify-layout-inline-block.mjs && node scripts/verify-layout-positioning.mjs && node scripts/verify-lists.mjs && node scripts/verify-media-queries.mjs && node scripts/verify-pseudo-elements.mjs && node scripts/verify-report.mjs && node scripts/verify-segmenter.mjs && node scripts/verify-text-align.mjs && node scripts/verify-white-space.mjs && node scripts/verify-firefox.mjs && node scripts/verify-font-registration.mjs && node scripts/verify-overflow.mjs && node scripts/verify-breaker.mjs && node scripts/verify-comments.mjs && node scripts/verify-rect-contract.mjs && node scripts/verify-stress.mjs",
"verify:all": "npm run build && tsc -p scripts/tsconfig.json && tsc -p scripts/tsconfig.all.json && node scripts/check-charter.mjs && node scripts/verify-four-layer.mjs && node scripts/generate-sweep.mjs && node scripts/verify-sweep.mjs && node scripts/verify-cross-family.mjs && node scripts/verify-ua-styles.mjs && node scripts/verify-text-measure.mjs && node scripts/verify-calc.mjs && node scripts/verify-shadow.mjs && node scripts/verify-opacity.mjs && node scripts/verify-layers.mjs && node scripts/verify-paint-text.mjs && node scripts/verify-border-radius.mjs && node scripts/verify-layout-flexbox.mjs && node scripts/verify-layout-floats.mjs && node scripts/verify-layout-grid.mjs && node scripts/verify-layout-inline-block.mjs && node scripts/verify-layout-positioning.mjs && node scripts/verify-lists.mjs && node scripts/verify-media-queries.mjs && node scripts/verify-pseudo-elements.mjs && node scripts/verify-report.mjs && node scripts/verify-segmenter.mjs && node scripts/verify-text-align.mjs && node scripts/verify-white-space.mjs && node scripts/verify-firefox.mjs && node scripts/verify-font-registration.mjs && node scripts/verify-overflow.mjs && node scripts/verify-breaker.mjs && node scripts/verify-comments.mjs && node scripts/verify-rect-contract.mjs && node scripts/verify-stress.mjs && node scripts/verify-float-content.mjs && node scripts/verify-atomic-perf.mjs",
"verify:four-layer": "npm run build && node scripts/verify-four-layer.mjs",
"verify:border-radius": "npm run build && node scripts/verify-border-radius.mjs",
"verify:overflow": "npm run build && node scripts/verify-overflow.mjs",
Expand Down Expand Up @@ -51,6 +51,9 @@
"verify:property-coverage": "npm run build && node scripts/verify-property-coverage.mjs",
"verify:breaker": "npm run build && node scripts/verify-breaker.mjs",
"verify:comments": "npm run build && node scripts/verify-comments.mjs",
"typecheck:scripts": "npm run build && tsc -p scripts/tsconfig.json && tsc -p scripts/tsconfig.all.json",
"verify:float-content": "npm run build && node scripts/verify-float-content.mjs",
"verify:atomic-perf": "npm run build && node scripts/verify-atomic-perf.mjs",
"probe:browser-gap": "npm run build && node probes/probe-browser-gap.mjs",
"bench:engine-vs-oracle": "npm run build && node scripts/bench-engine-vs-oracle.mjs",
"test:probe": "npm run build && node --test probes/probe-gap-lib.test.mjs",
Expand Down
8 changes: 4 additions & 4 deletions scripts/bench-engine-vs-oracle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,19 @@ async function measureChromeRender(browser, raw) {
const h = raw.harvest;
const page = await browser.newPage({ viewport: { width: h.viewport.width, height: h.viewport.height } });
await page.addInitScript(() => {
window.__paints = [];
(/** @type {any} */ (window)).__paints = [];
try {
new PerformanceObserver((list) => {
for (const e of list.getEntries()) window.__paints.push({ name: e.name, startTime: e.startTime });
for (const e of list.getEntries()) (/** @type {any} */ (window)).__paints.push({ name: e.name, startTime: e.startTime });
}).observe({ type: 'paint' });
} catch (e) {
window.__paints.push({ name: 'observer-error', startTime: 0 });
(/** @type {any} */ (window)).__paints.push({ name: 'observer-error', startTime: 0 });
}
});
await page.goto('data:text/html;charset=utf-8,' + encodeURIComponent(h.html));
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(50);
const paints = await page.evaluate(() => window.__paints);
const paints = await page.evaluate(() => (/** @type {any} */ (window)).__paints);
const fcp = paints.find((e) => e.name === 'first-contentful-paint') ?? paints.find((e) => e.name === 'first-paint');
await page.close();
if (!fcp) throw new Error(`fixture ${raw.name}: no paint timing entry observed`);
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-charter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const requires = [
];

for (const [label, re] of requires) {
if (!re.test(charter)) fail(`charter.md missing: ${label}`);
if (!(/** @type {RegExp} */ (re)).test(charter)) fail(`charter.md missing: ${label}`);
}

// --- corpus gap-fixture schema (improvement-plan §4) ---
Expand Down
8 changes: 4 additions & 4 deletions scripts/generate-sweep.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ try {
const viewport = combo.viewport;
const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } });
await page.setContent(combo.html);
const referenceRects = {};
/** @type {Record<string, any>} */ const referenceRects = {};
for (const id of combo.rects) {
referenceRects[id] = await page.$eval(`#${id}`, (el) => {
const r = el.getBoundingClientRect();
Expand Down Expand Up @@ -100,9 +100,9 @@ try {
width: refImg.width,
height: refImg.height,
};
const harness = evaluateFixture(fixture);
const rectLayer = harness.layers.rect;
const shotLayer = harness.layers.screenshot;
const harness = evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture));
const rectLayer = /** @type {import('../dist/harness/evaluate.js').RectLayerResult} */ (harness.layers.rect);
const shotLayer = /** @type {import('../dist/harness/evaluate.js').ScreenshotLayerResult} */ (harness.layers.screenshot);

const expected = { measureText: 'pass', computedStyle: 'pass' };
expected.rect = rectLayer.pass ? 'pass' : gapFor(combo.label);
Expand Down
46 changes: 46 additions & 0 deletions scripts/lib/render.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Shared render harness for the engine-only verify gates (no browser oracle):
* the FONT_FILE / FONT_FAMILY fallbacks, the document wrapper, and a typed
* `render(body)` over `renderHtml`. Centralizing this keeps each gate to just
* its assertions and gives the gates a single typed surface — `renderHtml`
* options are checked against the engine's own `RenderOptions` type (see
* scripts/tsconfig.json).
*
* The repo default font is the bundled Hack face, so these gates need no system
* fonts; FONT_FILE / FONT_FAMILY still override it (matching the oracle gates).
*/
import { resolve } from 'node:path';
import { renderHtml } from '../../dist/index.js';

export const FONT_FILE = process.env.FONT_FILE ?? resolve('fonts/HackNerdFont-Regular.ttf');
export const FONT_FAMILY = process.env.FONT_FAMILY ?? 'Hack Nerd Font';

/**
* Wrap body markup in a minimal document with a zero-margin body at the shared
* font. `style` is extra CSS appended to the body rule's stylesheet.
* @param {string} body
* @param {string} [style]
* @returns {string}
*/
export function doc(body, style = '') {
return `<!doctype html><html><head><style>body{margin:0;font:16px '${FONT_FAMILY}'}${style}</style></head><body>${body}</body></html>`;
}

/**
* Render body markup through the engine at the given viewport.
* @param {string} body
* @param {{ width?: number, height?: number, style?: string }} [opts]
*/
export function render(body, opts = {}) {
const { width = 800, height = 600, style = '' } = opts;
return renderHtml(doc(body, style), { width, height, fontFamily: FONT_FAMILY, fontFile: FONT_FILE });
}

/**
* Normalize a caught value (typed `unknown` under strict) to a message string.
* @param {unknown} e
* @returns {string}
*/
export function errorMessage(e) {
return e instanceof Error ? e.message : String(e);
}
21 changes: 21 additions & 0 deletions scripts/tsconfig.all.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"//": "Broad type-check for the remaining scripts (npm run typecheck:scripts runs this after the strict gate config). checkJs against dist/*.d.ts catches the errors that matter for test code — a wrong renderHtml/BrowserConfig option, a wrong harness field or property — across the oracle/bench/generator scripts. noImplicitAny and strictNullChecks are relaxed here (the strict tier in tsconfig.json covers the engine-only gates): the oracle scripts run untyped browser-context code inside page.evaluate and build result objects dynamically, where per-parameter annotations and DOM null-guards are noise rather than signal. As scripts are hardened they can graduate to the strict tier.",
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"noEmit": true,
"allowJs": true,
"checkJs": true,
"declaration": false,
"sourceMap": false,
"noImplicitAny": false,
"strictNullChecks": false
},
"include": ["**/*.mjs"],
"exclude": [
"lib/render.mjs",
"verify-comments.mjs",
"verify-float-content.mjs",
"verify-atomic-perf.mjs"
]
}
19 changes: 19 additions & 0 deletions scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"//": "Type-checks the engine-only verify gates and their shared harness (npm run typecheck:scripts). checkJs brings the src/ strict safety net to the .mjs gates that pin the engine's behavior, without converting all of scripts/ at once. renderHtml options and RenderOutput fields are checked against dist/*.d.ts, so a mistyped option or a wrong rects field is caught before runtime. Add new engine-only gates and lib helpers to `include` as they land.",
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"noEmit": true,
"allowJs": true,
"checkJs": true,
"declaration": false,
"sourceMap": false
},
"include": [
"lib/render.mjs",
"verify-comments.mjs",
"verify-float-content.mjs",
"verify-atomic-perf.mjs"
],
"exclude": []
}
54 changes: 54 additions & 0 deletions scripts/verify-atomic-perf.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/**
* `npm run verify:atomic-perf`
*
* Intrinsic sizing / measurement of inline-block ("atomic") boxes used to be
* unmemoized: sizing an auto-width inline-block measures its whole subtree, and
* every atomic was measured once for line-breaking and again for placement — so
* nested inline-blocks re-measured every level below them, twice, giving
* O(2^depth) work. Deeply nested inline-blocks (a real WPT flex reference, or
* the progressive nesting a stray self-closing `<div/>` produces in the HTML
* parser) hung the engine for minutes / OOM-killed the process.
*
* atomicBoxSize/measureAtomic are now memoized per (element, width), which is
* linear. This gate renders pathological nesting and asserts it finishes fast;
* the pre-fix engine could not complete depth 14 in two minutes.
*/
import assert from 'node:assert/strict';
import { render, errorMessage } from './lib/render.mjs';

const BUDGET_MS = Number(process.env.ATOMIC_BUDGET_MS ?? 5000); // generous; pre-fix hung > 120s at depth 14

let pass = 0;
/** @type {string[]} */
const fails = [];
/** @param {string} name @param {string} body */
const timed = (name, body) => {
const t = Date.now();
try { render(body); } catch (e) { fails.push(`${name}: threw ${errorMessage(e)}`); return; }
const ms = Date.now() - t;
if (ms > BUDGET_MS) fails.push(`${name}: took ${ms}ms > ${BUDGET_MS}ms budget (exponential regression?)`);
else { pass++; console.log(` ${name}: ${ms}ms`); }
};

// Deep nested inline-blocks — the exponential shape.
let ib = 'x';
for (let i = 0; i < 200; i++) ib = `<span style="display:inline-block">${ib}</span>`;
timed('nested inline-block depth 200', ib);

// Progressive nesting from self-closing <div/> in HTML parsing (the WPT ref).
let sc = '';
for (let i = 0; i < 80; i++) sc += `<div style="line-height:0"><div style="display:inline-block;width:10px;height:10px"/><div style="display:inline-block;width:50px;height:10px"/></div>`;
timed('80 self-closing-div blocks', sc);

// Correctness: memoization must not change layout — deep nesting still resolves.
try {
const r = render(`<span id="o" style="display:inline-block"><span id="i" style="display:inline-block;width:30px;height:12px"></span></span>`).rects;
assert.ok(r.o && r.i && r.i.width === 30, 'nested inline-block rects wrong');
pass++;
} catch (e) { fails.push(`nested rect correctness: ${errorMessage(e)}`); }

for (const f of fails) console.log(`FAIL ${f}`);
console.log(`${pass}/${pass + fails.length} atomic-perf checks passed`);
if (fails.length) { console.error('verify:atomic-perf: FAIL'); process.exit(1); }
console.log('verify:atomic-perf: PASS');
8 changes: 4 additions & 4 deletions scripts/verify-border-radius.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ try {
await page.setContent(h.html);
await page.evaluate(() => document.fonts.ready);

const referenceRects = {};
/** @type {Record<string, any>} */ const referenceRects = {};
for (const id of h.rects ?? []) {
referenceRects[id] = await page.$eval(`#${id}`, (el) => {
const r = el.getBoundingClientRect();
return { x: r.x, y: r.y, width: r.width, height: r.height };
});
}

const referenceMeasure = {};
/** @type {Record<string, any>} */ const referenceMeasure = {};
if (h.measureText) {
for (const { text, font } of h.measureText) {
referenceMeasure[`${font} | ${text}`] = await page.evaluate(
Expand All @@ -108,7 +108,7 @@ try {
}
}

const referenceComputed = {};
/** @type {Record<string, any>} */ const referenceComputed = {};
if (h.computedStyle) {
for (const { id, props } of h.computedStyle) {
referenceComputed[id] = await page.evaluate(
Expand Down Expand Up @@ -156,7 +156,7 @@ try {
const candImg = decodePng(out.rgba);

const candidateRects = out.rects;
const candidateMeasure = {};
/** @type {Record<string, any>} */ const candidateMeasure = {};
if (h.measureText) {
const { measureTextWidth } = await import('../dist/layout/measure.js');
for (const { text, font } of h.measureText) {
Expand Down
1 change: 1 addition & 0 deletions scripts/verify-breaker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ if (fontsByFamily.size === 0) {
console.error(`verify:breaker: no fixtures found under ${corpus}`);
process.exit(1);
}
/** @type {import('../dist/index.js').BrowserConfig} */
const config = {
browser: 'chrome',
fonts: [...fontsByFamily.values()],
Expand Down
4 changes: 2 additions & 2 deletions scripts/verify-calc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ try {
await page.setContent(h.html);
await page.evaluate(() => document.fonts.ready);

const referenceRects = {};
/** @type {Record<string, any>} */ const referenceRects = {};
for (const id of h.rects ?? []) {
referenceRects[id] = await page.$eval(`#${id}`, (el) => {
const r = el.getBoundingClientRect();
Expand Down Expand Up @@ -97,7 +97,7 @@ try {
height,
};

results.push(evaluateFixture(fixture));
results.push(evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture)));
console.log(`verified ${name}: ${width}x${height}`);
}
} finally {
Expand Down
18 changes: 6 additions & 12 deletions scripts/verify-comments.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,10 @@
*/

import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { renderHtml } from '../dist/index.js';
import { render as renderAt, errorMessage } from './lib/render.mjs';

const FONT_FILE = process.env.FONT_FILE ?? resolve('fonts/HackNerdFont-Regular.ttf');
const FONT_FAMILY = process.env.FONT_FAMILY ?? 'Hack Nerd Font';

const doc = (body, style = '') =>
`<!doctype html><html><head><style>body{margin:0;font:16px '${FONT_FAMILY}'}${style}</style></head><body>${body}</body></html>`;

const render = (body, style) =>
renderHtml(doc(body, style), { width: 400, height: 300, fontFamily: FONT_FAMILY, fontFile: FONT_FILE });
/** @param {string} body @param {string} [style] */
const render = (body, style = '') => renderAt(body, { width: 400, height: 300, style });

// Each case: [name, withoutComments, withComments, style]. The two markups must
// lay out identically — the only difference is interleaved comment nodes.
Expand Down Expand Up @@ -61,6 +54,7 @@ const cases = [
];

let pass = 0;
/** @type {string[]} */
const fails = [];

// Property 1 + 2 over the matrix.
Expand All @@ -70,7 +64,7 @@ for (const [name, plain, commented, style] of cases) {
base = render(plain, style);
withc = render(commented, style);
} catch (e) {
fails.push(`${name}: threw ${e.message}`);
fails.push(`${name}: threw ${errorMessage(e)}`);
continue;
}
try {
Expand All @@ -86,7 +80,7 @@ try {
render(`<!-- a comment --><p id="a">hi</p>`);
pass++;
} catch (e) {
fails.push(`original repro still throws: ${e.message}`);
fails.push(`original repro still throws: ${errorMessage(e)}`);
}

const total = cases.length + 1;
Expand Down
10 changes: 5 additions & 5 deletions scripts/verify-cross-family.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,15 @@ try {
await page.setContent(h.html);
await page.evaluate(() => document.fonts.ready);

const referenceRects = {};
/** @type {Record<string, any>} */ const referenceRects = {};
for (const id of h.rects ?? []) {
referenceRects[id] = await page.$eval(`#${id}`, (el) => {
const r = el.getBoundingClientRect();
return { x: r.x, y: r.y, width: r.width, height: r.height };
});
}

const referenceMeasure = {};
/** @type {Record<string, any>} */ const referenceMeasure = {};
if (h.measureText) {
for (const { text, font } of h.measureText) {
referenceMeasure[`${font} | ${text}`] = await page.evaluate(
Expand All @@ -123,7 +123,7 @@ try {
}
}

const referenceComputed = {};
/** @type {Record<string, any>} */ const referenceComputed = {};
if (h.computedStyle) {
for (const { id, props } of h.computedStyle) {
referenceComputed[id] = await page.evaluate(
Expand Down Expand Up @@ -173,7 +173,7 @@ try {
const candImg = decodePng(out.rgba);

const candidateRects = out.rects;
const candidateMeasure = {};
/** @type {Record<string, any>} */ const candidateMeasure = {};
if (h.measureText) {
const { measureTextWidth } = await import('../dist/layout/measure.js');
for (const { text, font } of h.measureText) {
Expand Down Expand Up @@ -238,7 +238,7 @@ try {
height,
};

results.push(evaluateFixture(fixture));
results.push(evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture)));
console.log(`verified ${name} (${target}): ${width}x${height}, ${fragments.length} text fragments, ${mask.some((b) => b === 1) ? 'masked' : 'no mask'}`);
}
} finally {
Expand Down
Loading