Skip to content

Commit aa61fd1

Browse files
authored
Merge pull request #50 from happyprime/lint-cleanup
Clean up lint and adopt project JSDoc style
2 parents 4b79935 + d16e0a2 commit aa61fd1

16 files changed

Lines changed: 147 additions & 118 deletions

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,16 @@ self-ignored `.reglance/` directory — nothing to add to `.gitignore`.
5353
| `output` | no | Output directory. Defaults to `.reglance`. |
5454
| `pixelmatchOptions` | no | [pixelmatch](https://github.com/mapbox/pixelmatch) options, e.g. `{ "threshold": 0.1 }`. |
5555
| `timeouts` | no | `{ goto, settle }` in ms. `goto` bounds navigation; `settle` bounds each post-scroll wait (network idle, then image load/decode). Defaults `{ goto: 15000, settle: 8000 }`. Raise `settle` for slow, lazy-loading pages. |
56-
| `blockHosts` | no | Hostnames to block requests to during capture, e.g. `["challenges.cloudflare.com"]`. Each entry also blocks its subdomains. |
56+
| `blockHosts` | no | Hostnames to block requests to during capture, e.g. `["captcha.example.com"]`. Each entry also blocks its subdomains. |
5757

5858
`domain` is only needed by `capture`; `control` and `compare` work on the files
5959
already captured. See [`reglance.example.json`](reglance.example.json) for a full
6060
example.
6161

6262
`blockHosts` aborts every request to the listed hosts (and their subdomains)
6363
before it leaves the browser. Use it for third-party embeds that keep the
64-
network busy and stall capture — CAPTCHA widgets like Cloudflare Turnstile,
65-
ad tech, analytics — or that render differently on every load and pollute
66-
diffs. Captures wait for the network to go idle, so a widget that polls or
64+
network busy and stall capture — CAPTCHA widgets, ad tech, analytics — or
65+
that render differently on every load and pollute diffs. Captures wait for the network to go idle, so a widget that polls or
6766
retries indefinitely will otherwise time out every viewport on pages that
6867
embed it. Entries are bare hostnames; `"example.org"` blocks `example.org` and
6968
`sub.example.org` alike.

eslint.config.mjs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
11
import config from '@happyprime/eslint-config';
22

3-
export default [...config];
3+
export default [
4+
...config,
5+
{
6+
// reglance is a CLI: console.log/warn ARE its user-facing output, so
7+
// only the noisier debugging methods (debug, info, table, …) stay
8+
// flagged. Templates ship to the browser and keep the shared rule.
9+
files: ['bin/**/*.mjs', 'src/**/*.mjs'],
10+
rules: {
11+
'no-console': ['warn', { allow: ['log', 'warn', 'error'] }],
12+
},
13+
},
14+
{
15+
// Project style: no hyphen between a JSDoc param name and its
16+
// description, with types, names, and descriptions aligned in columns.
17+
rules: {
18+
'jsdoc/require-hyphen-before-param-description': ['warn', 'never'],
19+
// Align params with each other only — a long @returns type would
20+
// otherwise drag every description far to the right.
21+
'jsdoc/check-line-alignment': [
22+
'warn',
23+
'always',
24+
{ tags: ['param', 'property'] },
25+
],
26+
},
27+
},
28+
];

reglance.example.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
"goto": 15000,
2222
"settle": 8000
2323
},
24-
"blockHosts": ["challenges.cloudflare.com"]
24+
"blockHosts": ["captcha.example.com"]
2525
}

src/args.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
* instead of silently truncated, and enforces a minimum so a value like
66
* `--concurrency=0` cannot reach the capture loop and hang it.
77
*
8-
* @param {string} value - The raw flag value.
9-
* @param {string} name - The flag name, for error messages.
10-
* @param {object} [options] - Parse options.
11-
* @param {number} [options.min] - The smallest allowed value (default 1).
8+
* @param {string} value The raw flag value.
9+
* @param {string} name The flag name, for error messages.
10+
* @param {object} [options] Parse options.
11+
* @param {number} [options.min] The smallest allowed value (default 1).
1212
* @returns {number} The parsed integer.
1313
*/
1414
export function toInt(value, name, { min = 1 } = {}) {

src/capture.mjs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { filterTargets, DEFAULT_TIMEOUTS } from './config.mjs';
77
* Whether a host is a local development host, for which self-signed/invalid
88
* TLS certificates are expected and certificate errors are ignored by default.
99
*
10-
* @param {string|null} host - The hostname (no port).
10+
* @param {string|null} host The hostname (no port).
1111
* @returns {boolean} True for localhost/loopback and .test/.local(.localhost) hosts.
1212
*/
1313
export function isLocalHost(host) {
@@ -34,8 +34,8 @@ export function isLocalHost(host) {
3434
* surfacing it makes off-domain (and potentially internal-network) capture a
3535
* conscious choice rather than a silent one.
3636
*
37-
* @param {Array} targets - The targets to inspect.
38-
* @param {string|null} domain - The configured domain origin.
37+
* @param {Array} targets The targets to inspect.
38+
* @param {string|null} domain The configured domain origin.
3939
* @returns {Array} Targets pointing at a different host.
4040
*/
4141
export function offDomainTargets(targets, domain) {
@@ -66,8 +66,8 @@ export function offDomainTargets(targets, domain) {
6666
* "example.org" blocks both "example.org" and "sub.example.org". Non-network
6767
* URLs (blob:, data:, chrome-extension:) have no hostname and never match.
6868
*
69-
* @param {string} url - The request URL.
70-
* @param {string[]} blockHosts - Normalized (lowercase) hostnames.
69+
* @param {string} url The request URL.
70+
* @param {string[]} blockHosts Normalized (lowercase) hostnames.
7171
* @returns {boolean} True when the request should be blocked.
7272
*/
7373
export function isBlockedHost(url, blockHosts) {
@@ -102,7 +102,7 @@ export function isBlockedHost(url, blockHosts) {
102102
* timing race. The height is re-read every step so content that loads and
103103
* grows the page extends the walk.
104104
*
105-
* @param {import('playwright').Page} page - The page to scroll.
105+
* @param {import('playwright').Page} page The page to scroll.
106106
*/
107107
async function autoScroll(page) {
108108
await page.evaluate(async () => {
@@ -137,8 +137,8 @@ async function autoScroll(page) {
137137
* a broken image rejects, which counts as settled — a missing image is the
138138
* page's actual state, not something to keep waiting on.
139139
*
140-
* @param {import('playwright').Page} page - The page to wait on.
141-
* @param {number} timeout - Max wait in ms.
140+
* @param {import('playwright').Page} page The page to wait on.
141+
* @param {number} timeout Max wait in ms.
142142
* @returns {Promise<number>} How many images were still loading at timeout.
143143
*/
144144
async function waitForImages(page, timeout) {
@@ -171,7 +171,7 @@ async function waitForImages(page, timeout) {
171171
* per viewport while still letting setViewportSize switch sizes within a group.
172172
* A viewport without an explicit deviceScaleFactor defaults to 1.
173173
*
174-
* @param {Array} viewports - Viewport definitions.
174+
* @param {Array} viewports Viewport definitions.
175175
* @returns {Array<{ deviceScaleFactor: number, viewports: Array }>} Groups in
176176
* first-seen DPR order.
177177
*/
@@ -378,13 +378,13 @@ export function shouldFailRun(failures, failOnDegraded = false) {
378378
/**
379379
* Capture screenshots for every configured target.
380380
*
381-
* @param {object} config The normalized config.
382-
* @param {object} [options] Capture options.
383-
* @param {number} [options.concurrency] Parallel browser contexts.
384-
* @param {number} [options.staggerDelay] Delay (ms) between context starts.
385-
* @param {boolean}[options.skipReload] Reuse the page between viewports.
386-
* @param {boolean}[options.failOnDegraded] Exit non-zero if any capture is degraded.
387-
* @param {Array} [options.only] Limit to these target keys.
381+
* @param {object} config The normalized config.
382+
* @param {object} [options] Capture options.
383+
* @param {number} [options.concurrency] Parallel browser contexts.
384+
* @param {number} [options.staggerDelay] Delay (ms) between context starts.
385+
* @param {boolean} [options.skipReload] Reuse the page between viewports.
386+
* @param {boolean} [options.failOnDegraded] Exit non-zero if any capture is degraded.
387+
* @param {Array} [options.only] Limit to these target keys.
388388
* @returns {Promise<{ failures: Array }>} The degraded-slug records.
389389
*/
390390
export async function capture(config, options = {}) {

src/compare.mjs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ const HTML_DIFF_MAX_BYTES = 2 * 1024 * 1024;
2424
* Write a comparison artifact, surfacing the path and error code on failure
2525
* (e.g. a full disk or a read-only output dir) instead of an opaque stack.
2626
*
27-
* @param {string} file - The destination path.
28-
* @param {string|Buffer} data - The contents to write.
27+
* @param {string} file The destination path.
28+
* @param {string|Buffer} data The contents to write.
2929
*/
3030
function writeArtifact(file, data) {
3131
try {
@@ -43,9 +43,9 @@ function writeArtifact(file, data) {
4343
* Returns the image unchanged when it already matches the target dimensions,
4444
* avoiding a needless full-buffer copy.
4545
*
46-
* @param {PNG} img - The image to pad.
47-
* @param {number} targetWidth - The desired width.
48-
* @param {number} targetHeight - The desired height.
46+
* @param {PNG} img The image to pad.
47+
* @param {number} targetWidth The desired width.
48+
* @param {number} targetHeight The desired height.
4949
* @returns {PNG} The padded image (or the original when no padding is needed).
5050
*/
5151
export function padImage(img, targetWidth, targetHeight) {
@@ -61,9 +61,9 @@ export function padImage(img, targetWidth, targetHeight) {
6161
/**
6262
* Compare a single control/capture pair for one target and viewport.
6363
*
64-
* @param {object} config - The normalized config.
65-
* @param {object} target - The target ({ key, url }).
66-
* @param {object} viewport - The viewport definition.
64+
* @param {object} config The normalized config.
65+
* @param {object} target The target ({ key, url }).
66+
* @param {object} viewport The viewport definition.
6767
* @returns {object|null} The comparison result, or null when it can't run.
6868
*/
6969
export function compareSlug(config, target, viewport) {
@@ -172,8 +172,8 @@ export function compareSlug(config, target, viewport) {
172172
* Warn when the controls being compared were promoted across more than one
173173
* `control` run, which means the baseline mixes captures from different times.
174174
*
175-
* @param {object} config - The normalized config.
176-
* @param {Array} targets - The targets being compared.
175+
* @param {object} config The normalized config.
176+
* @param {Array} targets The targets being compared.
177177
*/
178178
function warnOnStaleControls(config, targets) {
179179
const slugs = targets.flatMap((target) =>
@@ -202,9 +202,9 @@ function warnOnStaleControls(config, targets) {
202202
* size (each in-flight comparison holds large RGBA buffers), which is why the
203203
* concurrency is capped and configurable.
204204
*
205-
* @param {object} config - The normalized config.
206-
* @param {Array} jobs - `{ target, viewport }` work items.
207-
* @param {number} concurrency - Maximum workers to run at once.
205+
* @param {object} config The normalized config.
206+
* @param {Array} jobs `{ target, viewport }` work items.
207+
* @param {number} concurrency Maximum workers to run at once.
208208
* @returns {Promise<Array>} The comparison reports (in completion order).
209209
*/
210210
async function runComparisons(config, jobs, concurrency) {
@@ -265,11 +265,11 @@ async function runComparisons(config, jobs, concurrency) {
265265
/**
266266
* Compare every captured target against its control and build the report.
267267
*
268-
* @param {object} config - The normalized config.
269-
* @param {object} [options] - Compare options.
270-
* @param {Array} [options.only] - Limit to these target keys.
271-
* @param {boolean} [options.open] - Open the report when finished.
272-
* @param {number} [options.concurrency] - Parallel diff workers.
268+
* @param {object} config The normalized config.
269+
* @param {object} [options] Compare options.
270+
* @param {Array} [options.only] Limit to these target keys.
271+
* @param {boolean} [options.open] Open the report when finished.
272+
* @param {number} [options.concurrency] Parallel diff workers.
273273
*/
274274
export async function compare(config, options = {}) {
275275
const { dirs } = config;

src/config.mjs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const DEFAULT_TIMEOUTS = { goto: 15000, settle: 8000 };
3636
* ("https://site.test"), and trailing slashes, returning a consistent
3737
* origin like "https://site.test".
3838
*
39-
* @param {string} domain - The domain to normalize.
39+
* @param {string} domain The domain to normalize.
4040
* @returns {string} The normalized origin.
4141
*/
4242
export function normalizeDomain(domain) {
@@ -77,8 +77,8 @@ const SAFE_SLUG_PART = /^[a-z0-9][a-z0-9._-]*$/i;
7777
/**
7878
* Assert a value is safe to use as part of an output filename.
7979
*
80-
* @param {string} value - The value to check.
81-
* @param {string} kind - A label for the error message (e.g. "path key").
80+
* @param {string} value The value to check.
81+
* @param {string} kind A label for the error message (e.g. "path key").
8282
*/
8383
function assertSafeSlugPart(value, kind) {
8484
if (
@@ -101,8 +101,8 @@ function assertSafeSlugPart(value, kind) {
101101
* a `.join` TypeError at the very end of a compare run, and a crafted array
102102
* would be interpolated into the report's CSS.
103103
*
104-
* @param {*} color - The configured color value.
105-
* @param {string} name - The option name, for the error message.
104+
* @param {*} color The configured color value.
105+
* @param {string} name The option name, for the error message.
106106
*/
107107
function validateColor(color, name) {
108108
const valid =
@@ -125,7 +125,7 @@ function validateColor(color, name) {
125125
* setViewportSize() and fails mid-capture with an opaque error, so it is
126126
* cheaper to catch it up front with an actionable message.
127127
*
128-
* @param {Array} viewports - The viewport definitions to validate.
128+
* @param {Array} viewports The viewport definitions to validate.
129129
*/
130130
export function validateViewports(viewports) {
131131
if (!Array.isArray(viewports) || viewports.length === 0) {
@@ -192,7 +192,7 @@ export function validateViewports(viewports) {
192192
* Anything with a scheme, path, or port is rejected up front — silently
193193
* matching nothing would read as "the block isn't working".
194194
*
195-
* @param {*} blockHosts - The raw config value.
195+
* @param {*} blockHosts The raw config value.
196196
* @returns {string[]} Lowercased hostnames with any `*.` prefix removed.
197197
*/
198198
export function normalizeBlockHosts(blockHosts) {
@@ -203,15 +203,15 @@ export function normalizeBlockHosts(blockHosts) {
203203
if (!Array.isArray(blockHosts)) {
204204
throw new Error(
205205
'❌ Invalid "blockHosts": expected an array of hostnames.\n' +
206-
'💡 Use entries like ["challenges.cloudflare.com"].'
206+
'💡 Use entries like ["captcha.example.com"].'
207207
);
208208
}
209209

210210
return blockHosts.map((entry) => {
211211
if (typeof entry !== 'string' || !entry.trim()) {
212212
throw new Error(
213213
`❌ Invalid "blockHosts" entry ${JSON.stringify(entry)}: expected a non-empty string.\n` +
214-
'💡 Use a bare hostname like "challenges.cloudflare.com".'
214+
'💡 Use a bare hostname like "captcha.example.com".'
215215
);
216216
}
217217

@@ -220,7 +220,7 @@ export function normalizeBlockHosts(blockHosts) {
220220
if (/[/:\s]/.test(host)) {
221221
throw new Error(
222222
`❌ Invalid "blockHosts" entry ${JSON.stringify(entry)}: expected a bare hostname (no scheme, port, or path).\n` +
223-
'💡 Use "challenges.cloudflare.com", not "https://challenges.cloudflare.com/".'
223+
'💡 Use "captcha.example.com", not "https://captcha.example.com/".'
224224
);
225225
}
226226

@@ -234,8 +234,8 @@ export function normalizeBlockHosts(blockHosts) {
234234
* A path that is already an absolute URL is returned untouched so that a
235235
* config can point individual entries at a different domain.
236236
*
237-
* @param {string} domain - The normalized domain origin.
238-
* @param {string} pathname - The path or absolute URL.
237+
* @param {string} domain The normalized domain origin.
238+
* @param {string} pathname The path or absolute URL.
239239
* @returns {string} The full URL.
240240
*/
241241
export function buildUrl(domain, pathname) {
@@ -253,8 +253,8 @@ export function buildUrl(domain, pathname) {
253253
* `control`/`compare` surfaces an actionable error instead of silently
254254
* doing nothing and printing a success-looking summary.
255255
*
256-
* @param {Array} targets - The configured targets.
257-
* @param {string[]} [only] - Path keys to keep. Falsy/empty keeps all.
256+
* @param {Array} targets The configured targets.
257+
* @param {string[]} [only] Path keys to keep. Falsy/empty keeps all.
258258
* @returns {Array} The filtered targets.
259259
*/
260260
export function filterTargets(targets, only) {
@@ -277,9 +277,9 @@ export function filterTargets(targets, only) {
277277
/**
278278
* Load and normalize a reglance config file.
279279
*
280-
* @param {object} [options] - Loader options.
281-
* @param {string} [options.configPath] - Path to the config file.
282-
* @param {string} [options.domain] - Domain override (e.g. from a flag).
280+
* @param {object} [options] Loader options.
281+
* @param {string} [options.configPath] Path to the config file.
282+
* @param {string} [options.domain] Domain override (e.g. from a flag).
283283
* @returns {object} The normalized config.
284284
*/
285285
export function loadConfig({ configPath = 'reglance.json', domain } = {}) {
@@ -363,7 +363,7 @@ export function loadConfig({ configPath = 'reglance.json', domain } = {}) {
363363
* Writes a self-contained .gitignore inside the output directory so that
364364
* captures, diffs, and reports never get committed to the host project.
365365
*
366-
* @param {object} config - The normalized config.
366+
* @param {object} config The normalized config.
367367
*/
368368
export function ensureOutputDir(config) {
369369
fs.mkdirSync(config.outputDir, { recursive: true });

src/control.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import { readManifest, writeManifest } from './manifest.mjs';
1313
* when a run promotes fewer captures than expected (leaving older controls in
1414
* place for the rest).
1515
*
16-
* @param {object} config - The normalized config.
17-
* @param {object} [options] - Control options.
18-
* @param {Array} [options.only] - Limit to these target keys.
19-
* @param {string} [options.now] - ISO timestamp override (for tests).
16+
* @param {object} config The normalized config.
17+
* @param {object} [options] Control options.
18+
* @param {Array} [options.only] Limit to these target keys.
19+
* @param {string} [options.now] ISO timestamp override (for tests).
2020
* @returns {{ moved: number, expected: number }} Promotion counts.
2121
*/
2222
export function control(config, options = {}) {

0 commit comments

Comments
 (0)