@nitpicker/core 0.4.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +191 -0
  3. package/README.md +13 -0
  4. package/lib/discover-analyze-plugins.d.ts +14 -0
  5. package/lib/discover-analyze-plugins.js +34 -0
  6. package/lib/find-nitpicker-modules-dir.d.ts +12 -0
  7. package/lib/find-nitpicker-modules-dir.js +23 -0
  8. package/lib/hooks/actions.d.ts +9 -0
  9. package/lib/hooks/actions.js +9 -0
  10. package/lib/hooks/child-process.d.ts +1 -0
  11. package/lib/hooks/child-process.js +34 -0
  12. package/lib/hooks/define-plugin.d.ts +68 -0
  13. package/lib/hooks/define-plugin.js +69 -0
  14. package/lib/hooks/index.d.ts +1 -0
  15. package/lib/hooks/index.js +1 -0
  16. package/lib/hooks/runner.d.ts +10 -0
  17. package/lib/hooks/runner.js +32 -0
  18. package/lib/import-modules.d.ts +24 -0
  19. package/lib/import-modules.js +38 -0
  20. package/lib/index.d.ts +5 -0
  21. package/lib/index.js +5 -0
  22. package/lib/load-plugin-settings.d.ts +40 -0
  23. package/lib/load-plugin-settings.js +85 -0
  24. package/lib/nitpicker.d.ts +127 -0
  25. package/lib/nitpicker.js +338 -0
  26. package/lib/page-analysis-worker.d.ts +48 -0
  27. package/lib/page-analysis-worker.js +98 -0
  28. package/lib/read-plugin-labels.d.ts +15 -0
  29. package/lib/read-plugin-labels.js +30 -0
  30. package/lib/table.d.ts +75 -0
  31. package/lib/table.js +132 -0
  32. package/lib/types.d.ts +264 -0
  33. package/lib/types.js +1 -0
  34. package/lib/url-event-bus.d.ts +32 -0
  35. package/lib/url-event-bus.js +20 -0
  36. package/lib/utils.d.ts +36 -0
  37. package/lib/utils.js +43 -0
  38. package/lib/worker/run-in-worker.d.ts +51 -0
  39. package/lib/worker/run-in-worker.js +120 -0
  40. package/lib/worker/runner.d.ts +25 -0
  41. package/lib/worker/runner.js +31 -0
  42. package/lib/worker/types.d.ts +23 -0
  43. package/lib/worker/types.js +1 -0
  44. package/lib/worker/worker.d.ts +27 -0
  45. package/lib/worker/worker.js +53 -0
  46. package/package.json +36 -0
  47. package/src/discover-analyze-plugins.spec.ts +21 -0
  48. package/src/discover-analyze-plugins.ts +37 -0
  49. package/src/hooks/define-plugin.spec.ts +38 -0
  50. package/src/hooks/define-plugin.ts +73 -0
  51. package/src/hooks/index.ts +1 -0
  52. package/src/import-modules.spec.ts +150 -0
  53. package/src/import-modules.ts +45 -0
  54. package/src/index.ts +5 -0
  55. package/src/load-plugin-settings.spec.ts +192 -0
  56. package/src/load-plugin-settings.ts +99 -0
  57. package/src/nitpicker.ts +418 -0
  58. package/src/page-analysis-worker.spec.ts +287 -0
  59. package/src/page-analysis-worker.ts +131 -0
  60. package/src/read-plugin-labels.spec.ts +151 -0
  61. package/src/read-plugin-labels.ts +37 -0
  62. package/src/table.spec.ts +83 -0
  63. package/src/table.ts +149 -0
  64. package/src/types.ts +289 -0
  65. package/src/url-event-bus.spec.ts +28 -0
  66. package/src/url-event-bus.ts +33 -0
  67. package/src/worker/run-in-worker.ts +155 -0
  68. package/src/worker/runner.ts +38 -0
  69. package/src/worker/types.ts +25 -0
  70. package/src/worker/worker.ts +64 -0
  71. package/tsconfig.json +11 -0
  72. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Worker thread module for per-page single-plugin execution.
3
+ *
4
+ * This is the default export loaded by {@link ./worker/runner.ts!runner}
5
+ * when analyzing a single page with a single plugin. It:
6
+ *
7
+ * 1. Dynamically imports the configured plugin via {@link importModules}
8
+ * 2. If the plugin implements `eachPage`:
9
+ * - Creates a JSDOM instance from the raw HTML
10
+ * - Calls the plugin's `eachPage` hook with the DOM window
11
+ * - Closes the JSDOM window to free memory
12
+ * 3. Returns the plugin result as {@link ReportPage} or `null`
13
+ *
14
+ * Each Worker invocation handles exactly one plugin, so the calling code
15
+ * can track per-plugin progress independently.
16
+ * @module
17
+ */
18
+ import { JSDOM } from 'jsdom';
19
+ import { importModules } from './import-modules.js';
20
+ /**
21
+ * Set of critical Node.js global properties that must never be overwritten
22
+ * by JSDOM window properties.
23
+ */
24
+ const PROTECTED_GLOBALS = new Set([
25
+ 'process',
26
+ 'global',
27
+ 'globalThis',
28
+ 'console',
29
+ 'Buffer',
30
+ 'setTimeout',
31
+ 'setInterval',
32
+ 'clearTimeout',
33
+ 'clearInterval',
34
+ 'setImmediate',
35
+ 'clearImmediate',
36
+ 'queueMicrotask',
37
+ ]);
38
+ /**
39
+ * Executes a single plugin's `eachPage` hook against a single page.
40
+ * @template T - Column key union from the plugin's headers.
41
+ * @param data - Contains the single plugin and page data (HTML + URL).
42
+ * @param urlEventBus - Event bus for URL discovery events (forwarded to the main thread).
43
+ * @param num - Zero-based index of the current page in the batch.
44
+ * @param total - Total number of pages being processed.
45
+ * @returns Report data from the plugin, or `null` if skipped.
46
+ * @see {@link ./worker/runner.ts!runner} for how this function is called
47
+ * @see {@link ./nitpicker.ts!Nitpicker.analyze} for the orchestration context
48
+ */
49
+ export default async function (data, urlEventBus, num, total) {
50
+ const { plugin, pages: { html, url }, } = data;
51
+ const [analyzeMod] = await importModules([plugin]);
52
+ if (!analyzeMod?.eachPage) {
53
+ return null;
54
+ }
55
+ await urlEventBus.emit('url', url.href);
56
+ const dom = new JSDOM(html, {
57
+ url: url.href,
58
+ runScripts: 'outside-only',
59
+ });
60
+ // Expose JSDOM globals so that browser-oriented libraries
61
+ // (axe-core, @medv/finder, etc.) that inspect the global scope
62
+ // can find `window`, `document`, `Node`, and other DOM APIs.
63
+ const g = globalThis;
64
+ const domGlobalKeys = [];
65
+ for (const key of Object.getOwnPropertyNames(dom.window)) {
66
+ if (key in g || PROTECTED_GLOBALS.has(key)) {
67
+ continue;
68
+ }
69
+ try {
70
+ g[key] = dom.window[key];
71
+ domGlobalKeys.push(key);
72
+ }
73
+ catch {
74
+ // Some window properties throw on access — skip them
75
+ }
76
+ }
77
+ try {
78
+ const report = await analyzeMod.eachPage({
79
+ url,
80
+ html,
81
+ window: dom.window,
82
+ num,
83
+ total,
84
+ });
85
+ return report ?? null;
86
+ }
87
+ catch (error) {
88
+ // eslint-disable-next-line no-console
89
+ console.error(`[${plugin.name}] ${error instanceof Error ? error.message : error}`);
90
+ return null;
91
+ }
92
+ finally {
93
+ for (const key of domGlobalKeys) {
94
+ delete g[key];
95
+ }
96
+ dom.window.close();
97
+ }
98
+ }
@@ -0,0 +1,15 @@
1
+ import type { Plugin } from './types.js';
2
+ /**
3
+ * Reads the `label` property from each plugin by importing and
4
+ * initializing the plugin module.
5
+ *
6
+ * Each plugin module's default export (a `PluginFactory`) is called
7
+ * with the plugin's configured settings. The resulting `AnalyzePlugin`
8
+ * object's `label` field is collected into a Map keyed by plugin name.
9
+ *
10
+ * Plugins that fail to import or initialize, or that lack a `label`,
11
+ * are silently skipped.
12
+ * @param plugins - Array of plugin definitions from the resolved config.
13
+ * @returns Map from plugin name to its human-readable label.
14
+ */
15
+ export declare function readPluginLabels(plugins: readonly Plugin[]): Promise<Map<string, string>>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Reads the `label` property from each plugin by importing and
3
+ * initializing the plugin module.
4
+ *
5
+ * Each plugin module's default export (a `PluginFactory`) is called
6
+ * with the plugin's configured settings. The resulting `AnalyzePlugin`
7
+ * object's `label` field is collected into a Map keyed by plugin name.
8
+ *
9
+ * Plugins that fail to import or initialize, or that lack a `label`,
10
+ * are silently skipped.
11
+ * @param plugins - Array of plugin definitions from the resolved config.
12
+ * @returns Map from plugin name to its human-readable label.
13
+ */
14
+ export async function readPluginLabels(plugins) {
15
+ const labels = new Map();
16
+ await Promise.all(plugins.map(async (plugin) => {
17
+ try {
18
+ const mod = await import(plugin.module);
19
+ const factory = mod.default;
20
+ const instance = await factory(plugin.settings ?? {}, plugin.configFilePath);
21
+ if (instance && typeof instance.label === 'string') {
22
+ labels.set(plugin.name, instance.label);
23
+ }
24
+ }
25
+ catch {
26
+ // Module not importable or factory failed — skip
27
+ }
28
+ }));
29
+ return labels;
30
+ }
package/lib/table.d.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { TableData, TableHeaders, TablePages } from './types.js';
2
+ import type { ExURL as URL } from '@d-zero/shared/parse-url';
3
+ /**
4
+ * In-memory accumulator for tabular report data.
5
+ *
6
+ * Table collects column headers from plugins and per-URL row data from
7
+ * analysis results. It uses `Map` internally for efficient merge operations
8
+ * (multiple plugins contribute columns to the same URL row), then serializes
9
+ * to plain objects for JSON storage in the archive.
10
+ *
11
+ * ## Merge semantics
12
+ *
13
+ * When data is added for a URL that already has entries, the new columns are
14
+ * shallow-merged with existing ones (later values overwrite earlier ones for
15
+ * the same key). This allows multiple plugins to contribute different columns
16
+ * to the same row without conflicts, as long as they use distinct column keys.
17
+ * @template T - String literal union of all column keys across plugins.
18
+ * @example
19
+ * ```ts
20
+ * const table = new Table<'title' | 'score'>();
21
+ * table.addHeaders({ title: 'Page Title', score: 'Score' });
22
+ * table.addDataToUrl(url, {
23
+ * title: { value: 'Home' },
24
+ * score: { value: 95 },
25
+ * });
26
+ * const json = table.toJSON();
27
+ * // { headers: { title: 'Page Title', score: 'Score' }, data: { 'https://...': { ... } } }
28
+ * ```
29
+ * @see {@link ./types.ts} for the underlying type aliases
30
+ */
31
+ export declare class Table<T extends string> {
32
+ #private;
33
+ /**
34
+ * Merges a batch of URL-keyed page data into the table.
35
+ * Typically called with deserialized data from a Worker thread or cache.
36
+ * @param data - Object where keys are URL strings and values are column data.
37
+ */
38
+ addData(data: TablePages<T>): void;
39
+ /**
40
+ * Adds or merges column data for a single URL.
41
+ * @param url - The page URL to associate the data with.
42
+ * @param data - Column values to store for this URL.
43
+ */
44
+ addDataToUrl(url: URL, data: TableData<T>): void;
45
+ /**
46
+ * Registers column headers from a plugin.
47
+ *
48
+ * Multiple plugins can call this independently; headers are merged by key.
49
+ * If two plugins declare the same key with different labels, the later
50
+ * registration wins.
51
+ * @param headers - Map of column keys to display labels.
52
+ */
53
+ addHeaders(headers: TableHeaders<T>): void;
54
+ /**
55
+ * Returns all row data as a plain object (serializable to JSON).
56
+ * @returns URL-keyed record of column data.
57
+ */
58
+ getData(): TablePages<T>;
59
+ /**
60
+ * Retrieves column data for a specific URL, or `undefined` if not present.
61
+ * @param url - The page URL to look up.
62
+ * @returns Column data for the URL, or `undefined`.
63
+ */
64
+ getDataByUrl(url: URL): TableData<T> | undefined;
65
+ /**
66
+ * Serializes the entire table (headers + data) to a JSON-compatible object.
67
+ *
68
+ * Used when storing the table in the archive via `archive.setData('analysis/table', ...)`.
69
+ * @returns Plain object with `headers` and `data` properties.
70
+ */
71
+ toJSON(): {
72
+ headers: Record<T, string>;
73
+ data: Record<string, TableData<T>>;
74
+ };
75
+ }
package/lib/table.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * In-memory accumulator for tabular report data.
3
+ *
4
+ * Table collects column headers from plugins and per-URL row data from
5
+ * analysis results. It uses `Map` internally for efficient merge operations
6
+ * (multiple plugins contribute columns to the same URL row), then serializes
7
+ * to plain objects for JSON storage in the archive.
8
+ *
9
+ * ## Merge semantics
10
+ *
11
+ * When data is added for a URL that already has entries, the new columns are
12
+ * shallow-merged with existing ones (later values overwrite earlier ones for
13
+ * the same key). This allows multiple plugins to contribute different columns
14
+ * to the same row without conflicts, as long as they use distinct column keys.
15
+ * @template T - String literal union of all column keys across plugins.
16
+ * @example
17
+ * ```ts
18
+ * const table = new Table<'title' | 'score'>();
19
+ * table.addHeaders({ title: 'Page Title', score: 'Score' });
20
+ * table.addDataToUrl(url, {
21
+ * title: { value: 'Home' },
22
+ * score: { value: 95 },
23
+ * });
24
+ * const json = table.toJSON();
25
+ * // { headers: { title: 'Page Title', score: 'Score' }, data: { 'https://...': { ... } } }
26
+ * ```
27
+ * @see {@link ./types.ts} for the underlying type aliases
28
+ */
29
+ export class Table {
30
+ /** Per-URL row data. Key is the URL href string. */
31
+ #data = new Map();
32
+ /** Column header definitions accumulated from all plugins. */
33
+ #headers = new Map();
34
+ /**
35
+ * Merges a batch of URL-keyed page data into the table.
36
+ * Typically called with deserialized data from a Worker thread or cache.
37
+ * @param data - Object where keys are URL strings and values are column data.
38
+ */
39
+ addData(data) {
40
+ const entries = Object.entries(data);
41
+ for (const [k, v] of entries) {
42
+ this.#add(k, v);
43
+ }
44
+ }
45
+ /**
46
+ * Adds or merges column data for a single URL.
47
+ * @param url - The page URL to associate the data with.
48
+ * @param data - Column values to store for this URL.
49
+ */
50
+ addDataToUrl(url, data) {
51
+ this.#add(url.href, data);
52
+ }
53
+ /**
54
+ * Registers column headers from a plugin.
55
+ *
56
+ * Multiple plugins can call this independently; headers are merged by key.
57
+ * If two plugins declare the same key with different labels, the later
58
+ * registration wins.
59
+ * @param headers - Map of column keys to display labels.
60
+ */
61
+ addHeaders(headers) {
62
+ const entries = Object.entries(headers);
63
+ for (const entry of entries) {
64
+ const [key, name] = entry;
65
+ this.#headers.set(key, name);
66
+ }
67
+ }
68
+ /**
69
+ * Returns all row data as a plain object (serializable to JSON).
70
+ * @returns URL-keyed record of column data.
71
+ */
72
+ getData() {
73
+ return mapToObject(this.#data);
74
+ }
75
+ /**
76
+ * Retrieves column data for a specific URL, or `undefined` if not present.
77
+ * @param url - The page URL to look up.
78
+ * @returns Column data for the URL, or `undefined`.
79
+ */
80
+ getDataByUrl(url) {
81
+ return this.#data.get(url.href);
82
+ }
83
+ /**
84
+ * Serializes the entire table (headers + data) to a JSON-compatible object.
85
+ *
86
+ * Used when storing the table in the archive via `archive.setData('analysis/table', ...)`.
87
+ * @returns Plain object with `headers` and `data` properties.
88
+ */
89
+ toJSON() {
90
+ return {
91
+ headers: mapToObject(this.#headers),
92
+ data: mapToObject(this.#data),
93
+ };
94
+ }
95
+ /**
96
+ * Internal merge-or-insert for a single URL row.
97
+ * If the URL already has data, the new values are shallow-merged.
98
+ * @param k - URL href string used as the row key.
99
+ * @param v - Column data to add or merge.
100
+ */
101
+ #add(k, v) {
102
+ const data = this.#data.get(k);
103
+ if (data) {
104
+ this.#data.set(k, {
105
+ ...data,
106
+ ...v,
107
+ });
108
+ }
109
+ else {
110
+ this.#data.set(k, v);
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * Converts a `Map<K, V>` to a plain `Record<K, V>` object.
116
+ *
117
+ * Used internally to serialize Map-based storage into JSON-compatible
118
+ * objects for archive storage and Worker message passing.
119
+ * @template K - String key type.
120
+ * @template V - Value type.
121
+ * @param map - The Map to convert.
122
+ * @returns A plain object with the same key-value pairs.
123
+ */
124
+ function mapToObject(map) {
125
+ const entries = map.entries();
126
+ const object = {};
127
+ for (const entry of entries) {
128
+ const [k, v] = entry;
129
+ object[k] = v;
130
+ }
131
+ return object;
132
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,264 @@
1
+ import type { Lanes } from '@d-zero/dealer';
2
+ import type { ExURL as URL } from '@d-zero/shared/parse-url';
3
+ import type { TableValue, Violation } from '@nitpicker/types';
4
+ import type { DOMWindow } from 'jsdom';
5
+ /**
6
+ * Represents a single analyze plugin loaded from the user's configuration.
7
+ *
8
+ * Each plugin corresponds to an npm module that exports a {@link PluginFactory}
9
+ * factory function as its default export. The `settings` object is passed
10
+ * through to that factory at initialization time.
11
+ * @see {@link ./load-plugin-settings.ts} for how plugins are discovered from cosmiconfig
12
+ * @see {@link ./import-modules.ts} for how plugins are dynamically imported
13
+ */
14
+ export interface Plugin {
15
+ /**
16
+ * Human-readable plugin name. Currently unused at runtime but kept
17
+ * for backward compatibility with older config formats.
18
+ * @deprecated Use `module` to identify plugins instead.
19
+ */
20
+ name: string;
21
+ /**
22
+ * The npm module specifier to `import()` (e.g. `"@nitpicker/analyze-axe"`).
23
+ */
24
+ module: string;
25
+ /**
26
+ * Absolute path to the configuration file where this plugin was declared.
27
+ * Passed to the plugin so it can resolve relative paths in its own config.
28
+ */
29
+ configFilePath: string;
30
+ /**
31
+ * Plugin-specific settings object parsed from the config file.
32
+ * The shape is determined by the plugin itself (e.g. `{ lang: "ja" }` for axe).
33
+ */
34
+ settings?: unknown;
35
+ }
36
+ /**
37
+ * Options for {@link ../nitpicker.ts!Nitpicker.analyze}.
38
+ *
39
+ * Allows callers to provide an external {@link https://www.npmjs.com/package/@d-zero/dealer | Lanes}
40
+ * instance for rich progress display, and a verbose flag for non-TTY environments.
41
+ */
42
+ export interface AnalyzeOptions {
43
+ /** Lanes instance for per-plugin progress display. If omitted, no progress is shown. */
44
+ readonly lanes?: Lanes;
45
+ /** When `true`, outputs plain-text progress lines instead of animated Lanes. */
46
+ readonly verbose?: boolean;
47
+ }
48
+ /**
49
+ * Internal configuration model used by {@link ../nitpicker.ts!Nitpicker}.
50
+ *
51
+ * Built by {@link ./load-plugin-settings.ts!loadPluginSettings} from the
52
+ * user's cosmiconfig file (e.g. `.nitpickerrc.json`). The external config
53
+ * format (`ConfigJSON` from `@nitpicker/types`) uses a `plugins.analyze`
54
+ * object keyed by module name; this internal type normalizes it into an
55
+ * ordered array of fully-resolved {@link Plugin} entries.
56
+ */
57
+ export interface Config {
58
+ /** Ordered list of analyze plugins to execute. */
59
+ analyze: Plugin[];
60
+ }
61
+ /**
62
+ * The runtime interface that every analyze plugin must satisfy after
63
+ * its factory function ({@link PluginFactory}) has been invoked.
64
+ *
65
+ * A plugin may implement one or both callback methods:
66
+ *
67
+ * - **`eachPage`** - Runs inside a Worker thread with full JSDOM access.
68
+ * Best for DOM-dependent analysis (markup validation, text linting,
69
+ * accessibility checks). Each invocation receives a parsed `DOMWindow`,
70
+ * so plugins can use standard DOM APIs without additional parsing.
71
+ *
72
+ * - **`eachUrl`** - Runs in the main thread, receives only the URL and
73
+ * external/internal flag. Suited for lightweight, network-based checks
74
+ * (e.g. link validation, SEO URL pattern checks).
75
+ * @template T - String literal union of the column keys this plugin
76
+ * contributes to the report table (e.g. `'title' | 'description'`).
77
+ * @see {@link ./page-analysis-worker.ts} for how `eachPage` is called inside the worker
78
+ * @see {@link ./nitpicker.ts} for how `eachUrl` is called from the main thread
79
+ */
80
+ export interface AnalyzePlugin<T extends string = string> {
81
+ /**
82
+ * Human-readable display label for interactive prompts.
83
+ * Shown instead of the raw package name (e.g. `"axe: アクセシビリティチェック"`).
84
+ */
85
+ label?: string;
86
+ /**
87
+ * Column header definitions contributed by this plugin.
88
+ * Keys are column identifiers (`T`), values are human-readable labels
89
+ * shown in the report header row.
90
+ */
91
+ headers?: TableHeaders<T>;
92
+ /**
93
+ * Per-page analysis callback executed in a Worker thread.
94
+ * @param page - Context for the current page, including the raw HTML,
95
+ * a live JSDOM window, the page URL, and progress counters.
96
+ * @param page.url - Parsed URL of the page being analyzed.
97
+ * @param page.html - Raw HTML string of the page.
98
+ * @param page.window - JSDOM window with the page's DOM tree. Closed after the callback returns.
99
+ * @param page.num - Zero-based index of the current page in the batch.
100
+ * @param page.total - Total number of pages in the batch.
101
+ * @returns Report data for this page, or `null` to skip.
102
+ */
103
+ eachPage?(page: {
104
+ /** Parsed URL of the page being analyzed. */
105
+ url: URL;
106
+ /** Raw HTML string of the page. */
107
+ html: string;
108
+ /** JSDOM window with the page's DOM tree. Closed after the callback returns. */
109
+ window: DOMWindow;
110
+ /** Zero-based index of the current page in the batch. */
111
+ num: number;
112
+ /** Total number of pages in the batch. */
113
+ total: number;
114
+ }): Promise<ReportPage<T> | null> | ReportPage<T> | null;
115
+ /**
116
+ * Per-URL analysis callback executed in the main thread.
117
+ *
118
+ * Unlike `eachPage`, this callback does **not** receive HTML or a DOM
119
+ * window. It is designed for checks that depend only on URL metadata
120
+ * (e.g. checking URL patterns, external link policies).
121
+ * @param page - URL context including external/internal classification.
122
+ * @param page.url - Parsed URL being analyzed.
123
+ * @param page.isExternal - Whether this URL is external to the crawled site.
124
+ * @returns Report data for this URL, or `null` to skip.
125
+ */
126
+ eachUrl?(page: {
127
+ /** Parsed URL being analyzed. */
128
+ url: URL;
129
+ /** Whether this URL is external to the crawled site. */
130
+ isExternal: boolean;
131
+ }): Promise<ReportPage<T> | null> | ReportPage<T> | null;
132
+ }
133
+ /**
134
+ * The return value of a single {@link AnalyzePlugin.eachPage} or {@link AnalyzePlugin.eachUrl}
135
+ * invocation for one page/URL.
136
+ *
137
+ * A plugin can contribute tabular data (displayed in spreadsheet columns)
138
+ * and/or violation records (displayed in a dedicated violations sheet).
139
+ * @template T - Column key union matching the plugin's `headers`.
140
+ */
141
+ export interface ReportPage<T extends string> {
142
+ /** Column data for this page. Keys must be a subset of `T`. */
143
+ page?: TableData<T>;
144
+ /** Violations detected on this page (e.g. a11y issues, lint errors). */
145
+ violations?: Violation[];
146
+ }
147
+ /**
148
+ * Aggregated report data from a Worker thread, keyed by page URL.
149
+ *
150
+ * This is the message payload returned from the Worker to the main thread
151
+ * via the `'finish'` message. It aggregates results from all plugins that
152
+ * ran `eachPage` for a single page.
153
+ * @template T - Column key union.
154
+ * @see {@link ./page-analysis-worker.ts} for the Worker entry point that produces this
155
+ * @see {@link ./worker/run-in-worker.ts!runInWorker} for the main-thread consumer
156
+ */
157
+ export interface ReportPages<T extends string> {
158
+ /** Per-URL table data from all plugins. */
159
+ pages?: TablePages<T>;
160
+ /** Combined violations from all plugins. */
161
+ violations?: Violation[];
162
+ }
163
+ /**
164
+ * Factory function signature that every analyze plugin module must
165
+ * export as its default export.
166
+ *
167
+ * The factory receives the user's settings object (`O`) and returns
168
+ * an {@link AnalyzePlugin} instance (or a Promise thereof). This two-phase
169
+ * pattern allows plugins to perform async initialization (e.g.
170
+ * loading locale files, compiling lint configs) once, then reuse
171
+ * the resulting plugin for every page.
172
+ *
173
+ * Use {@link ./hooks/define-plugin.ts!definePlugin} to define a
174
+ * plugin with full type inference.
175
+ * @template O - Shape of the plugin's settings from the config file.
176
+ * @template T - String literal union of column keys the plugin contributes.
177
+ * @example
178
+ * ```ts
179
+ * // In @nitpicker/analyze-search/src/index.ts
180
+ * import { definePlugin } from '@nitpicker/core';
181
+ *
182
+ * type Options = { keywords: string[] };
183
+ *
184
+ * export default definePlugin(async (options: Options) => {
185
+ * return {
186
+ * headers: { found: 'Keywords Found' },
187
+ * async eachPage({ html }) {
188
+ * const count = options.keywords.filter(k => html.includes(k)).length;
189
+ * return { page: { found: { value: count } } };
190
+ * },
191
+ * };
192
+ * });
193
+ * ```
194
+ * @see {@link AnalyzePlugin} for the runtime interface
195
+ * @see {@link ./hooks/define-plugin.ts!definePlugin} for the type-safe wrapper
196
+ */
197
+ export type PluginFactory<O, T extends string = string> = (options: O, configFilePath: string) => Promise<AnalyzePlugin<T>> | AnalyzePlugin<T>;
198
+ /**
199
+ * Column header definitions: maps column key to display label.
200
+ * @example
201
+ * ```ts
202
+ * const headers: TableHeaders<'title' | 'desc'> = {
203
+ * title: 'Page Title',
204
+ * desc: 'Meta Description',
205
+ * };
206
+ * ```
207
+ */
208
+ export type TableHeaders<K extends string> = Record<K, string>;
209
+ /** Internal Map representation of {@link TableHeaders}, used by {@link ../table.ts!Table}. */
210
+ export type TableHeaderMap<K extends string> = Map<K, string>;
211
+ /** A single row of cell values keyed by column identifier. */
212
+ export type TableData<K extends string> = Record<K, TableValue>;
213
+ /**
214
+ * Multiple rows of table data keyed by page URL.
215
+ * This is the serialized form used in Worker message payloads and JSON output.
216
+ */
217
+ export type TablePages<K extends string> = Record<string, TableData<K>>;
218
+ /**
219
+ * Internal Map representation of page-keyed table data.
220
+ * Used by {@link ../table.ts!Table} for efficient merge operations.
221
+ */
222
+ export type TableRow<K extends string> = Map<string, TableData<K>>;
223
+ /**
224
+ * Payload for starting an analyze action.
225
+ * @internal
226
+ */
227
+ export interface PluginExecutionContext {
228
+ /** Human-readable name of the plugin module. */
229
+ pluginModuleName: string;
230
+ /** Resolved file system path to the plugin module. */
231
+ pluginModulePath: string;
232
+ /** Plugin-specific settings to pass to the hook factory. */
233
+ settings: unknown;
234
+ /** Path to the config file where this action was declared. */
235
+ configFilePath: string;
236
+ /** Temporary directory where the archive is extracted. */
237
+ archiveTempDir: string;
238
+ /** Full resolved configuration. */
239
+ config: Config;
240
+ }
241
+ /**
242
+ * Event map for the {@link ../nitpicker.ts!Nitpicker} event emitter.
243
+ *
244
+ * Consumers can listen to these events via `nitpicker.on('writeFile', ...)`.
245
+ * @see {@link ../nitpicker.ts!Nitpicker} which extends `TypedAwaitEventEmitter<NitpickerEvent>`
246
+ */
247
+ export interface NitpickerEvent {
248
+ /**
249
+ * Emitted after the archive file has been successfully written to disk.
250
+ */
251
+ writeFile: {
252
+ /** Absolute path to the written `.nitpicker` archive file. */
253
+ filePath: string;
254
+ };
255
+ /**
256
+ * Emitted when a non-fatal error occurs during analysis.
257
+ */
258
+ error: {
259
+ /** Human-readable error description. */
260
+ message: string;
261
+ /** Original Error object, or `null` if unavailable. */
262
+ error: Error | null;
263
+ };
264
+ }
package/lib/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
2
+ /**
3
+ * Event map for {@link UrlEventBus}.
4
+ *
5
+ * Currently supports a single event type for URL discovery notifications.
6
+ */
7
+ export interface UrlEventBusEvent {
8
+ /**
9
+ * Emitted when a URL is discovered or being processed.
10
+ * The payload is the URL href string.
11
+ */
12
+ url: string;
13
+ }
14
+ /**
15
+ * Typed event bus for URL discovery notifications.
16
+ *
17
+ * Used as a communication channel between Worker threads and the main thread:
18
+ *
19
+ * - **Inside Workers**: The each-page worker emits `'url'` events on a local
20
+ * UrlEventBus. The Worker thread entry point ({@link ./worker/worker.ts})
21
+ * listens for these and forwards them to the main thread via `parentPort.postMessage`.
22
+ *
23
+ * - **In the main thread**: {@link ./worker/run-in-worker.ts!runInWorker} creates its own
24
+ * UrlEventBus and re-emits `'url'` messages received from the Worker.
25
+ *
26
+ * This indirection allows the same plugin code to work both in Worker threads
27
+ * and in direct execution mode (when `useWorker` is `false`).
28
+ * @see {@link ./worker/worker.ts} for Worker-side forwarding
29
+ * @see {@link ./worker/run-in-worker.ts!runInWorker} for main-thread re-emission
30
+ */
31
+ export declare class UrlEventBus extends EventEmitter<UrlEventBusEvent> {
32
+ }
@@ -0,0 +1,20 @@
1
+ import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
2
+ /**
3
+ * Typed event bus for URL discovery notifications.
4
+ *
5
+ * Used as a communication channel between Worker threads and the main thread:
6
+ *
7
+ * - **Inside Workers**: The each-page worker emits `'url'` events on a local
8
+ * UrlEventBus. The Worker thread entry point ({@link ./worker/worker.ts})
9
+ * listens for these and forwards them to the main thread via `parentPort.postMessage`.
10
+ *
11
+ * - **In the main thread**: {@link ./worker/run-in-worker.ts!runInWorker} creates its own
12
+ * UrlEventBus and re-emits `'url'` messages received from the Worker.
13
+ *
14
+ * This indirection allows the same plugin code to work both in Worker threads
15
+ * and in direct execution mode (when `useWorker` is `false`).
16
+ * @see {@link ./worker/worker.ts} for Worker-side forwarding
17
+ * @see {@link ./worker/run-in-worker.ts!runInWorker} for main-thread re-emission
18
+ */
19
+ export class UrlEventBus extends EventEmitter {
20
+ }