@nitpicker/report-html 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,23 @@
1
1
  import type { ArchiveAccessor } from '@nitpicker/crawler';
2
2
  import type { HtmlReportPage } from '@nitpicker/viewer/report-ui';
3
+ /** Row-set restriction for {@link collectHtmlReportPages}. */
4
+ export interface CollectHtmlReportPagesOptions {
5
+ /** Directory-prefix filters. Empty or omitted lists every inner page. */
6
+ readonly directories?: readonly string[];
7
+ /**
8
+ * Exact-match URL allowlist, already normalized (see
9
+ * `resolvePageListUrlFilter`). ANDed with `directories` when both are
10
+ * given — see `PageListRowFilterOptions.urls`'s docs.
11
+ */
12
+ readonly urls?: readonly string[];
13
+ }
3
14
  /**
4
15
  * Collects inner-page rows for a static HTML report in `natural_url_rank`
5
16
  * order, attaching redirect sources and per-page resource-file tallies.
6
17
  * @param accessor - Archive whose viewer read model is already current.
7
- * @param directories - Directory-prefix filters. Empty or omitted lists every inner page.
18
+ * @param options - Row-set restriction. Empty or omitted lists every inner page.
8
19
  * @returns Page rows ready for `renderHtmlReport`.
9
20
  * @example
10
- * const pages = await collectHtmlReportPages(accessor, ['/docs']);
21
+ * const pages = await collectHtmlReportPages(accessor, { directories: ['/docs'] });
11
22
  */
12
- export declare function collectHtmlReportPages(accessor: ArchiveAccessor, directories?: readonly string[]): Promise<HtmlReportPage[]>;
23
+ export declare function collectHtmlReportPages(accessor: ArchiveAccessor, options?: CollectHtmlReportPagesOptions): Promise<HtmlReportPage[]>;
@@ -3,15 +3,15 @@ import { buildRedirectFromUrlsByDestId, getResourceFileCountsByPageIds, streamPa
3
3
  * Collects inner-page rows for a static HTML report in `natural_url_rank`
4
4
  * order, attaching redirect sources and per-page resource-file tallies.
5
5
  * @param accessor - Archive whose viewer read model is already current.
6
- * @param directories - Directory-prefix filters. Empty or omitted lists every inner page.
6
+ * @param options - Row-set restriction. Empty or omitted lists every inner page.
7
7
  * @returns Page rows ready for `renderHtmlReport`.
8
8
  * @example
9
- * const pages = await collectHtmlReportPages(accessor, ['/docs']);
9
+ * const pages = await collectHtmlReportPages(accessor, { directories: ['/docs'] });
10
10
  */
11
- export async function collectHtmlReportPages(accessor, directories = []) {
11
+ export async function collectHtmlReportPages(accessor, options = {}) {
12
12
  const redirectFrom = await buildRedirectFromUrlsByDestId(accessor);
13
13
  const pages = [];
14
- for await (const chunk of streamPageListRows(accessor, { directories })) {
14
+ for await (const chunk of streamPageListRows(accessor, options)) {
15
15
  const counts = await getResourceFileCountsByPageIds(accessor, chunk.map((row) => row.pageId));
16
16
  for (const row of chunk) {
17
17
  const resources = counts.get(row.pageId) ?? { total: 0, exists: 0 };
@@ -24,6 +24,7 @@ export async function collectHtmlReportPages(accessor, directories = []) {
24
24
  resourceFilesExists: resources.exists,
25
25
  resourceFilesTotal: resources.total,
26
26
  consoleErrorCount: row.consoleErrorCount,
27
+ redirectTo: row.redirectDestUrl,
27
28
  });
28
29
  }
29
30
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The static HTML report's inner-page ceiling, shared by both page-selection
3
+ * paths (`resolveDirectoryPrefixes`'s interactive `--html-dirs` narrowing and
4
+ * `resolvePageSelection`'s non-interactive `--urls` guard) so the two can
5
+ * never drift to different limits.
6
+ */
7
+ export declare const PAGE_REPORT_LIMIT = 10000;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The static HTML report's inner-page ceiling, shared by both page-selection
3
+ * paths (`resolveDirectoryPrefixes`'s interactive `--html-dirs` narrowing and
4
+ * `resolvePageSelection`'s non-interactive `--urls` guard) so the two can
5
+ * never drift to different limits.
6
+ */
7
+ export const PAGE_REPORT_LIMIT = 10_000;
package/lib/report.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
2
  import { TaskList } from '@d-zero/dealer';
3
- import { getViewerSummary, requireViewerReadModel } from '@nitpicker/query';
3
+ import { getViewerSummary, requireViewerReadModel, resolveAndValidatePageListUrlFilter, warnUnmatchedPageListUrls, } from '@nitpicker/query';
4
4
  import { renderHtmlReport } from '@nitpicker/viewer/report-ui';
5
5
  import { collectHtmlReportPages } from './collect-html-report-pages.js';
6
6
  import { openReportArchive } from './open-report-archive.js';
7
- import { resolveDirectoryPrefixes } from './resolve-directory-prefixes.js';
8
7
  import { resolveOutputPath } from './resolve-output-path.js';
8
+ import { resolvePageSelection } from './resolve-page-selection.js';
9
9
  /** A `write()`-only sink that renders nothing, for `params.silent`. */
10
10
  const NULL_STREAM = {
11
11
  write: () => true,
@@ -33,24 +33,32 @@ const NULL_STREAM = {
33
33
  export async function report(params) {
34
34
  await using archive = await openReportArchive(params.filePath, params.onExtractProgress);
35
35
  await requireViewerReadModel(archive.accessor);
36
- const prefixes = await resolveDirectoryPrefixes({
36
+ const warn = params.silent
37
+ ? () => { }
38
+ : (message) => {
39
+ // eslint-disable-next-line no-console
40
+ console.warn(message);
41
+ };
42
+ const normalizedUrls = params.urls === undefined
43
+ ? undefined
44
+ : await resolveAndValidatePageListUrlFilter(archive.accessor, params.urls, warn);
45
+ const selection = await resolvePageSelection({
37
46
  accessor: archive.accessor,
38
- initialInput: params.directoryInput,
47
+ directoryInput: params.directoryInput,
48
+ urls: normalizedUrls,
39
49
  interactive: params.interactive === true,
40
- onWarn: params.silent
41
- ? () => { }
42
- : (message) => {
43
- // eslint-disable-next-line no-console
44
- console.warn(message);
45
- },
50
+ onWarn: warn,
46
51
  });
47
- const directoryPrefixes = prefixes.map((prefix) => prefix.display);
52
+ const directoryPrefixes = selection.directories;
48
53
  const outputPath = resolveOutputPath(params.filePath, params.outputPath);
49
54
  let html = '';
50
55
  let pipeline = TaskList.from(0);
51
56
  pipeline = pipeline.pipe('Collect pages', async (input, ctx) => {
52
57
  ctx.progress('reading inner pages...');
53
- const pages = await collectHtmlReportPages(archive.accessor, directoryPrefixes);
58
+ const pages = await collectHtmlReportPages(archive.accessor, {
59
+ directories: directoryPrefixes,
60
+ urls: selection.urls,
61
+ });
54
62
  const summary = await getViewerSummary(archive.accessor);
55
63
  ctx.progress(`${pages.length.toLocaleString()} pages`);
56
64
  html = renderHtmlReport({
@@ -60,6 +68,9 @@ export async function report(params) {
60
68
  generatedAt: new Date().toISOString(),
61
69
  directoryPrefixes,
62
70
  });
71
+ if (selection.urls) {
72
+ await warnUnmatchedPageListUrls(archive.accessor, selection.urls, warn);
73
+ }
63
74
  return input;
64
75
  });
65
76
  pipeline = pipeline.pipe('Write HTML', async (input, ctx) => {
@@ -1,8 +1,8 @@
1
1
  import { countPageListHostnames, countPageListRows } from '@nitpicker/query';
2
2
  import enquirer from 'enquirer';
3
3
  import { HtmlReportCancelledError } from './html-report-cancelled-error.js';
4
+ import { PAGE_REPORT_LIMIT } from './page-report-limit.js';
4
5
  import { parseDirectoryInput } from './parse-directory-input.js';
5
- const PAGE_LIMIT = 10_000;
6
6
  /**
7
7
  * Asks for comma-separated directory prefixes on a TTY.
8
8
  * @returns The raw input string.
@@ -40,7 +40,7 @@ export async function resolveDirectoryPrefixes(options) {
40
40
  const unfilteredTotal = await countPageListRows(options.accessor);
41
41
  const hostnameCount = await countPageListHostnames(options.accessor);
42
42
  let input = options.initialInput;
43
- if (unfilteredTotal <= PAGE_LIMIT && input == null) {
43
+ if (unfilteredTotal <= PAGE_REPORT_LIMIT && input == null) {
44
44
  return [];
45
45
  }
46
46
  for (;;) {
@@ -68,7 +68,7 @@ export async function resolveDirectoryPrefixes(options) {
68
68
  const selectedTotal = await countPageListRows(options.accessor, {
69
69
  directories: prefixes.map((prefix) => prefix.display),
70
70
  });
71
- if (selectedTotal > PAGE_LIMIT) {
71
+ if (selectedTotal > PAGE_REPORT_LIMIT) {
72
72
  throw new Error(`The selected directories contain ${selectedTotal.toLocaleString()} inner pages; narrow them to 10,000 or fewer.`);
73
73
  }
74
74
  return prefixes;
@@ -0,0 +1,40 @@
1
+ import type { ArchiveAccessor } from '@nitpicker/crawler';
2
+ /** Row-set restriction {@link resolvePageSelection} resolves to. */
3
+ export interface PageSelection {
4
+ /** Directory-prefix filters, display form, ready for `collectHtmlReportPages`. */
5
+ readonly directories: string[];
6
+ /** Normalized URL allowlist, or `undefined` when `--urls` was not given. */
7
+ readonly urls: readonly string[] | undefined;
8
+ }
9
+ interface ResolvePageSelectionOptions {
10
+ readonly accessor: ArchiveAccessor;
11
+ readonly directoryInput?: string;
12
+ /** Already-normalized URLs (via `resolvePageListUrlFilter`), or `undefined` when `--urls` was not given. */
13
+ readonly urls?: readonly string[];
14
+ readonly interactive: boolean;
15
+ readonly onWarn: (message: string) => void;
16
+ }
17
+ /**
18
+ * Decides the report's page restriction: the `--urls` path (this function's
19
+ * own AND-combination with `--html-dirs`, no interactive prompting, no
20
+ * 10,000-page-limit *enforcement via prompting* since a URL list is expected
21
+ * to already be well under it) or the pre-existing `--html-dirs`-only path
22
+ * (delegated to {@link resolveDirectoryPrefixes} unchanged, including its
23
+ * interactive prompt loop).
24
+ *
25
+ * `--urls` skips `resolveDirectoryPrefixes`'s prompt-forcing flow entirely
26
+ * because that flow's whole purpose — interactively narrowing an
27
+ * unboundedly large report down to size — doesn't apply once the caller has
28
+ * already named the exact pages it wants. The 10,000-page ceiling itself
29
+ * still applies (checked after combining `--urls` with any `--html-dirs`
30
+ * narrowing), so a directory prefix that happens to still leave the
31
+ * URL-restricted set too large is still rejected.
32
+ * @param options - Archive, optional non-interactive directory input,
33
+ * optional normalized URL allowlist, interactivity, and warning sink.
34
+ * @returns The resolved directory prefixes and URL allowlist to pass to
35
+ * `collectHtmlReportPages`.
36
+ * @throws {Error} If the `--urls` path's combined row count exceeds the
37
+ * 10,000-page ceiling.
38
+ */
39
+ export declare function resolvePageSelection(options: ResolvePageSelectionOptions): Promise<PageSelection>;
40
+ export {};
@@ -0,0 +1,51 @@
1
+ import { countPageListRows } from '@nitpicker/query';
2
+ import { PAGE_REPORT_LIMIT } from './page-report-limit.js';
3
+ import { parseDirectoryInput } from './parse-directory-input.js';
4
+ import { resolveDirectoryPrefixes } from './resolve-directory-prefixes.js';
5
+ /**
6
+ * Decides the report's page restriction: the `--urls` path (this function's
7
+ * own AND-combination with `--html-dirs`, no interactive prompting, no
8
+ * 10,000-page-limit *enforcement via prompting* since a URL list is expected
9
+ * to already be well under it) or the pre-existing `--html-dirs`-only path
10
+ * (delegated to {@link resolveDirectoryPrefixes} unchanged, including its
11
+ * interactive prompt loop).
12
+ *
13
+ * `--urls` skips `resolveDirectoryPrefixes`'s prompt-forcing flow entirely
14
+ * because that flow's whole purpose — interactively narrowing an
15
+ * unboundedly large report down to size — doesn't apply once the caller has
16
+ * already named the exact pages it wants. The 10,000-page ceiling itself
17
+ * still applies (checked after combining `--urls` with any `--html-dirs`
18
+ * narrowing), so a directory prefix that happens to still leave the
19
+ * URL-restricted set too large is still rejected.
20
+ * @param options - Archive, optional non-interactive directory input,
21
+ * optional normalized URL allowlist, interactivity, and warning sink.
22
+ * @returns The resolved directory prefixes and URL allowlist to pass to
23
+ * `collectHtmlReportPages`.
24
+ * @throws {Error} If the `--urls` path's combined row count exceeds the
25
+ * 10,000-page ceiling.
26
+ */
27
+ export async function resolvePageSelection(options) {
28
+ if (options.urls === undefined) {
29
+ const prefixes = await resolveDirectoryPrefixes({
30
+ accessor: options.accessor,
31
+ initialInput: options.directoryInput,
32
+ interactive: options.interactive,
33
+ onWarn: options.onWarn,
34
+ });
35
+ return { directories: prefixes.map((prefix) => prefix.display), urls: undefined };
36
+ }
37
+ const directoryPrefixes = options.directoryInput
38
+ ? parseDirectoryInput(options.directoryInput)
39
+ : [];
40
+ const directories = directoryPrefixes.map((prefix) => prefix.display);
41
+ const total = await countPageListRows(options.accessor, {
42
+ urls: options.urls,
43
+ directories,
44
+ });
45
+ if (total > PAGE_REPORT_LIMIT) {
46
+ throw new Error(`--urls matched ${total.toLocaleString()} inner page(s)` +
47
+ (directories.length > 0 ? ' after --html-dirs narrowing' : '') +
48
+ `; exceeds the ${PAGE_REPORT_LIMIT.toLocaleString()}-page report limit. Narrow --urls or --html-dirs further.`);
49
+ }
50
+ return { directories, urls: options.urls };
51
+ }
package/lib/types.d.ts CHANGED
@@ -6,6 +6,16 @@ export interface HtmlReportParams {
6
6
  readonly outputPath?: string;
7
7
  /** Comma-separated directory prefixes used for non-interactive large reports. */
8
8
  readonly directoryInput?: string;
9
+ /**
10
+ * URL allowlist restricting the report to exactly these pages, already
11
+ * classified into valid/invalid by the CLI (`readUrlListFile` +
12
+ * `isValidUrl`) but not yet normalized to the archive's own URL form —
13
+ * `report()` does that via `resolveAndValidatePageListUrlFilter` once the
14
+ * archive is open. ANDed with `directoryInput` when both are given. Skips the
15
+ * interactive/10,000-page-limit-prompting flow `directoryInput` alone
16
+ * triggers — see `resolvePageSelection`.
17
+ */
18
+ readonly urls?: readonly string[];
9
19
  /** Whether the reporter may prompt for directory prefixes. */
10
20
  readonly interactive?: boolean;
11
21
  /** Suppresses completion and warning output. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitpicker/report-html",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Self-contained static HTML reports for Nitpicker archives",
5
5
  "author": "D-ZERO",
6
6
  "license": "Apache-2.0",
@@ -30,10 +30,10 @@
30
30
  "clean": "tsc --build --clean"
31
31
  },
32
32
  "dependencies": {
33
- "@d-zero/dealer": "1.13.1",
34
- "@nitpicker/crawler": "0.20.0",
35
- "@nitpicker/query": "0.20.0",
36
- "@nitpicker/viewer": "0.20.0",
33
+ "@d-zero/dealer": "1.14.0",
34
+ "@nitpicker/crawler": "0.22.0",
35
+ "@nitpicker/query": "0.22.0",
36
+ "@nitpicker/viewer": "0.22.0",
37
37
  "enquirer": "2.4.1",
38
38
  "react": "19.2.8",
39
39
  "react-dom": "19.2.8"
@@ -42,5 +42,5 @@
42
42
  "@d-zero/shared": "0.23.0",
43
43
  "vitest": "4.1.11"
44
44
  },
45
- "gitHead": "c322c99bbaca356646d06130cb6eec2706c0ddc5"
45
+ "gitHead": "934b1714f4394effb3865d84bf8348ed74f45840"
46
46
  }