@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +191 -0
- package/README.md +13 -0
- package/lib/discover-analyze-plugins.d.ts +14 -0
- package/lib/discover-analyze-plugins.js +34 -0
- package/lib/find-nitpicker-modules-dir.d.ts +12 -0
- package/lib/find-nitpicker-modules-dir.js +23 -0
- package/lib/hooks/actions.d.ts +9 -0
- package/lib/hooks/actions.js +9 -0
- package/lib/hooks/child-process.d.ts +1 -0
- package/lib/hooks/child-process.js +34 -0
- package/lib/hooks/define-plugin.d.ts +68 -0
- package/lib/hooks/define-plugin.js +69 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/runner.d.ts +10 -0
- package/lib/hooks/runner.js +32 -0
- package/lib/import-modules.d.ts +24 -0
- package/lib/import-modules.js +38 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +5 -0
- package/lib/load-plugin-settings.d.ts +40 -0
- package/lib/load-plugin-settings.js +85 -0
- package/lib/nitpicker.d.ts +127 -0
- package/lib/nitpicker.js +338 -0
- package/lib/page-analysis-worker.d.ts +48 -0
- package/lib/page-analysis-worker.js +98 -0
- package/lib/read-plugin-labels.d.ts +15 -0
- package/lib/read-plugin-labels.js +30 -0
- package/lib/table.d.ts +75 -0
- package/lib/table.js +132 -0
- package/lib/types.d.ts +264 -0
- package/lib/types.js +1 -0
- package/lib/url-event-bus.d.ts +32 -0
- package/lib/url-event-bus.js +20 -0
- package/lib/utils.d.ts +36 -0
- package/lib/utils.js +43 -0
- package/lib/worker/run-in-worker.d.ts +51 -0
- package/lib/worker/run-in-worker.js +120 -0
- package/lib/worker/runner.d.ts +25 -0
- package/lib/worker/runner.js +31 -0
- package/lib/worker/types.d.ts +23 -0
- package/lib/worker/types.js +1 -0
- package/lib/worker/worker.d.ts +27 -0
- package/lib/worker/worker.js +53 -0
- package/package.json +36 -0
- package/src/discover-analyze-plugins.spec.ts +21 -0
- package/src/discover-analyze-plugins.ts +37 -0
- package/src/hooks/define-plugin.spec.ts +38 -0
- package/src/hooks/define-plugin.ts +73 -0
- package/src/hooks/index.ts +1 -0
- package/src/import-modules.spec.ts +150 -0
- package/src/import-modules.ts +45 -0
- package/src/index.ts +5 -0
- package/src/load-plugin-settings.spec.ts +192 -0
- package/src/load-plugin-settings.ts +99 -0
- package/src/nitpicker.ts +418 -0
- package/src/page-analysis-worker.spec.ts +287 -0
- package/src/page-analysis-worker.ts +131 -0
- package/src/read-plugin-labels.spec.ts +151 -0
- package/src/read-plugin-labels.ts +37 -0
- package/src/table.spec.ts +83 -0
- package/src/table.ts +149 -0
- package/src/types.ts +289 -0
- package/src/url-event-bus.spec.ts +28 -0
- package/src/url-event-bus.ts +33 -0
- package/src/worker/run-in-worker.ts +155 -0
- package/src/worker/runner.ts +38 -0
- package/src/worker/types.ts +25 -0
- package/src/worker/worker.ts +64 -0
- package/tsconfig.json +11 -0
- 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
|
+
}
|