@nitpicker/core 0.4.2 → 0.4.4
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/package.json +7 -4
- package/CHANGELOG.md +0 -16
- package/src/discover-analyze-plugins.spec.ts +0 -21
- package/src/discover-analyze-plugins.ts +0 -37
- package/src/hooks/define-plugin.spec.ts +0 -38
- package/src/hooks/define-plugin.ts +0 -73
- package/src/hooks/index.ts +0 -1
- package/src/import-modules.spec.ts +0 -150
- package/src/import-modules.ts +0 -45
- package/src/index.ts +0 -5
- package/src/load-plugin-settings.spec.ts +0 -192
- package/src/load-plugin-settings.ts +0 -99
- package/src/nitpicker.ts +0 -418
- package/src/page-analysis-worker.spec.ts +0 -287
- package/src/page-analysis-worker.ts +0 -131
- package/src/read-plugin-labels.spec.ts +0 -151
- package/src/read-plugin-labels.ts +0 -37
- package/src/table.spec.ts +0 -83
- package/src/table.ts +0 -149
- package/src/types.ts +0 -289
- package/src/url-event-bus.spec.ts +0 -28
- package/src/url-event-bus.ts +0 -33
- package/src/worker/run-in-worker.ts +0 -155
- package/src/worker/runner.ts +0 -38
- package/src/worker/types.ts +0 -25
- package/src/worker/worker.ts +0 -64
- package/tsconfig.json +0 -11
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { Config, Plugin } from './types.js';
|
|
2
|
-
import type { ConfigJSON } from '@nitpicker/types';
|
|
3
|
-
|
|
4
|
-
import { cosmiconfig } from 'cosmiconfig';
|
|
5
|
-
|
|
6
|
-
import { discoverAnalyzePlugins } from './discover-analyze-plugins.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The cosmiconfig module name used for config file discovery.
|
|
10
|
-
* Searches for: `.nitpickerrc`, `.nitpickerrc.json`, `.nitpickerrc.yaml`,
|
|
11
|
-
* `nitpicker.config.js`, `nitpicker.config.cjs`, or a `"nitpicker"` key
|
|
12
|
-
* in `package.json`.
|
|
13
|
-
*/
|
|
14
|
-
const MODULE_NAME = 'nitpicker';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Loads the analyze plugin configuration from the user's config file.
|
|
18
|
-
*
|
|
19
|
-
* Uses cosmiconfig to search the filesystem from `process.cwd()` upward
|
|
20
|
-
* for a Nitpicker configuration file. The external config format
|
|
21
|
-
* (`ConfigJSON` from `@nitpicker/types`) is normalized into the internal
|
|
22
|
-
* {@link Config} model:
|
|
23
|
-
*
|
|
24
|
-
* - `config.plugins.analyze` (object keyed by module name with settings)
|
|
25
|
-
* is converted to an ordered `Plugin[]` array
|
|
26
|
-
* - Boolean `true` settings are normalized to empty objects `{}`
|
|
27
|
-
* - The config file path is attached to each plugin for relative path resolution
|
|
28
|
-
* @param defaultConfig - Optional partial config to merge as defaults.
|
|
29
|
-
* Plugin lists are concatenated (defaults first, then discovered plugins).
|
|
30
|
-
* @returns Fully resolved {@link Config} with the `analyze` plugin list.
|
|
31
|
-
* @example
|
|
32
|
-
* ```ts
|
|
33
|
-
* // .nitpickerrc.json
|
|
34
|
-
* // {
|
|
35
|
-
* // "plugins": {
|
|
36
|
-
* // "analyze": {
|
|
37
|
-
* // "@nitpicker/analyze-axe": { "lang": "ja" },
|
|
38
|
-
* // "@nitpicker/analyze-markuplint": true
|
|
39
|
-
* // }
|
|
40
|
-
* // }
|
|
41
|
-
* // }
|
|
42
|
-
*
|
|
43
|
-
* const config = await loadPluginSettings();
|
|
44
|
-
* // config.analyze = [
|
|
45
|
-
* // { name: '@nitpicker/analyze-axe', module: '@nitpicker/analyze-axe',
|
|
46
|
-
* // configFilePath: '/path/to/.nitpickerrc.json', settings: { lang: 'ja' } },
|
|
47
|
-
* // { name: '@nitpicker/analyze-markuplint', module: '@nitpicker/analyze-markuplint',
|
|
48
|
-
* // configFilePath: '/path/to/.nitpickerrc.json', settings: {} },
|
|
49
|
-
* // ]
|
|
50
|
-
* ```
|
|
51
|
-
* @see {@link ./types.ts!Config} for the output type
|
|
52
|
-
* @see {@link ./types.ts!Plugin} for individual plugin entries
|
|
53
|
-
*/
|
|
54
|
-
export async function loadPluginSettings(
|
|
55
|
-
defaultConfig: Partial<Config> = {},
|
|
56
|
-
): Promise<Config> {
|
|
57
|
-
const explorer = cosmiconfig(MODULE_NAME);
|
|
58
|
-
const result = await explorer.search();
|
|
59
|
-
if (!result) {
|
|
60
|
-
const defaultPlugins = defaultConfig.analyze || [];
|
|
61
|
-
return {
|
|
62
|
-
analyze: defaultPlugins.length > 0 ? defaultPlugins : discoverAnalyzePlugins(),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const config = result.config as ConfigJSON;
|
|
66
|
-
const { isEmpty, filepath } = result;
|
|
67
|
-
if (!config || isEmpty) {
|
|
68
|
-
const defaultPlugins = defaultConfig.analyze || [];
|
|
69
|
-
return {
|
|
70
|
-
analyze: defaultPlugins.length > 0 ? defaultPlugins : discoverAnalyzePlugins(),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const analyzePlugins: Plugin[] = [];
|
|
75
|
-
|
|
76
|
-
if (config.plugins && config.plugins.analyze) {
|
|
77
|
-
const moduleNames = Object.keys(config.plugins.analyze);
|
|
78
|
-
|
|
79
|
-
for (const name of moduleNames) {
|
|
80
|
-
if (!config.plugins.analyze[name]) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const settings =
|
|
84
|
-
config.plugins.analyze[name] === true ? {} : config.plugins.analyze[name];
|
|
85
|
-
analyzePlugins.push({
|
|
86
|
-
name,
|
|
87
|
-
module: name,
|
|
88
|
-
configFilePath: filepath,
|
|
89
|
-
settings,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const mergedPlugins = [...(defaultConfig.analyze || []), ...analyzePlugins];
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
analyze: mergedPlugins.length > 0 ? mergedPlugins : discoverAnalyzePlugins(),
|
|
98
|
-
};
|
|
99
|
-
}
|
package/src/nitpicker.ts
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
import type { PageAnalysisWorkerData } from './page-analysis-worker.js';
|
|
2
|
-
import type {
|
|
3
|
-
AnalyzeOptions,
|
|
4
|
-
Config,
|
|
5
|
-
NitpickerEvent,
|
|
6
|
-
ReportPage,
|
|
7
|
-
TableData,
|
|
8
|
-
} from './types.js';
|
|
9
|
-
import type { Report, Violation } from '@nitpicker/types';
|
|
10
|
-
|
|
11
|
-
import os from 'node:os';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
|
|
14
|
-
import { Cache } from '@d-zero/shared/cache';
|
|
15
|
-
import { TypedAwaitEventEmitter as EventEmitter } from '@d-zero/shared/typed-await-event-emitter';
|
|
16
|
-
import { Archive } from '@nitpicker/crawler';
|
|
17
|
-
import c from 'ansi-colors';
|
|
18
|
-
|
|
19
|
-
import { importModules } from './import-modules.js';
|
|
20
|
-
import { loadPluginSettings } from './load-plugin-settings.js';
|
|
21
|
-
import { Table } from './table.js';
|
|
22
|
-
import { UrlEventBus } from './url-event-bus.js';
|
|
23
|
-
import { runInWorker } from './worker/run-in-worker.js';
|
|
24
|
-
|
|
25
|
-
export { definePlugin } from './hooks/define-plugin.js';
|
|
26
|
-
|
|
27
|
-
const __filename = new URL(import.meta.url).pathname;
|
|
28
|
-
const __dirname = path.dirname(__filename);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolved path to the compiled Worker entry point.
|
|
32
|
-
* This file is loaded by `runInWorker()` in a `new Worker(...)` call.
|
|
33
|
-
* @see {@link ./page-analysis-worker.ts} for the source
|
|
34
|
-
*/
|
|
35
|
-
const pageAnalysisWorkerPath = path.resolve(__dirname, 'page-analysis-worker.js');
|
|
36
|
-
|
|
37
|
-
/** Maximum number of concurrent Worker threads per plugin. */
|
|
38
|
-
const CONCURRENCY_LIMIT = 50;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Core orchestrator for running analyze plugins against a `.nitpicker` archive.
|
|
42
|
-
*
|
|
43
|
-
* Nitpicker opens an existing archive (produced by the crawler), loads the
|
|
44
|
-
* user's plugin configuration via cosmiconfig, then runs each plugin against
|
|
45
|
-
* every page in the archive. Results are stored back into the archive as
|
|
46
|
-
* `analysis/report`, `analysis/table`, and `analysis/violations`.
|
|
47
|
-
*
|
|
48
|
-
* ## Architecture decisions
|
|
49
|
-
*
|
|
50
|
-
* - **Worker threads for `eachPage`**: DOM-heavy analysis (JSDOM + axe-core,
|
|
51
|
-
* markuplint, etc.) runs in isolated Worker threads so that a crashing plugin
|
|
52
|
-
* cannot take down the main process and memory from JSDOM windows is reclaimed
|
|
53
|
-
* when the Worker exits. See {@link ./worker/run-in-worker.ts!runInWorker}.
|
|
54
|
-
*
|
|
55
|
-
* - **Plugin-outer, page-inner loop**: Plugins are processed sequentially,
|
|
56
|
-
* and for each plugin, pages are processed in parallel (limit: 50) using
|
|
57
|
-
* a bounded Promise pool. This enables per-plugin progress tracking via
|
|
58
|
-
* {@link https://www.npmjs.com/package/@d-zero/dealer | Lanes}.
|
|
59
|
-
*
|
|
60
|
-
* - **Cache layer**: Results are cached per `pluginName:url` using
|
|
61
|
-
* `@d-zero/shared/cache` so that re-running analysis after a partial failure
|
|
62
|
-
* skips already-processed pages. The cache is cleared at the start of each run.
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* import { Nitpicker } from '@nitpicker/core';
|
|
66
|
-
*
|
|
67
|
-
* // Open an existing archive
|
|
68
|
-
* const nitpicker = await Nitpicker.open('./example.nitpicker');
|
|
69
|
-
*
|
|
70
|
-
* // Run all configured analyze plugins
|
|
71
|
-
* await nitpicker.analyze();
|
|
72
|
-
*
|
|
73
|
-
* // Or run only specific plugins by name
|
|
74
|
-
* await nitpicker.analyze(['@nitpicker/analyze-axe']);
|
|
75
|
-
*
|
|
76
|
-
* // Write updated archive back to disk
|
|
77
|
-
* await nitpicker.write();
|
|
78
|
-
* ```
|
|
79
|
-
* @see {@link ./types.ts!NitpickerEvent} for emitted events
|
|
80
|
-
* @see {@link ./types.ts!Config} for the resolved configuration model
|
|
81
|
-
*/
|
|
82
|
-
export class Nitpicker extends EventEmitter<NitpickerEvent> {
|
|
83
|
-
/**
|
|
84
|
-
* The underlying archive instance providing access to the SQLite database
|
|
85
|
-
* and file storage. Injected via constructor or created by `Nitpicker.open()`.
|
|
86
|
-
*/
|
|
87
|
-
readonly #archive: Archive;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Lazily loaded and cached plugin configuration.
|
|
91
|
-
* `null` until `getConfig()` is first called.
|
|
92
|
-
*/
|
|
93
|
-
#config: Config | null = null;
|
|
94
|
-
|
|
95
|
-
/** The underlying archive instance. */
|
|
96
|
-
get archive() {
|
|
97
|
-
return this.#archive;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @param archive - An opened {@link Archive} instance to analyze.
|
|
102
|
-
* Use {@link Nitpicker.open} for a convenient static factory.
|
|
103
|
-
*/
|
|
104
|
-
constructor(archive: Archive) {
|
|
105
|
-
super();
|
|
106
|
-
this.#archive = archive;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Runs all configured analyze plugins (or a filtered subset) against
|
|
111
|
-
* every page in the archive.
|
|
112
|
-
*
|
|
113
|
-
* Plugins are processed **sequentially** (one at a time), while pages
|
|
114
|
-
* within each plugin are processed in **parallel** (bounded to
|
|
115
|
-
* {@link CONCURRENCY_LIMIT}). This architecture enables per-plugin
|
|
116
|
-
* progress tracking via Lanes.
|
|
117
|
-
*
|
|
118
|
-
* The analysis proceeds in two phases:
|
|
119
|
-
*
|
|
120
|
-
* 1. **`eachPage` phase** - For each plugin with `eachPage`, spawns
|
|
121
|
-
* Worker threads via a bounded Promise pool to analyze all pages.
|
|
122
|
-
* Progress is displayed via Lanes if provided in options.
|
|
123
|
-
*
|
|
124
|
-
* 2. **`eachUrl` phase** - For all plugins with `eachUrl`, runs
|
|
125
|
-
* sequentially in the main thread. These are lightweight checks
|
|
126
|
-
* that don't need DOM access.
|
|
127
|
-
*
|
|
128
|
-
* On completion, three data entries are stored in the archive:
|
|
129
|
-
* - `analysis/report` - Full {@link Report} with headers, data, and violations
|
|
130
|
-
* - `analysis/table` - The raw {@link Table} instance (serialized)
|
|
131
|
-
* - `analysis/violations` - Flat array of all {@link Violation} records
|
|
132
|
-
* @param filter - Optional list of plugin module names to run.
|
|
133
|
-
* If omitted, all configured plugins are executed.
|
|
134
|
-
* @param options - Optional settings for progress display.
|
|
135
|
-
* @example
|
|
136
|
-
* ```ts
|
|
137
|
-
* // Run all plugins
|
|
138
|
-
* await nitpicker.analyze();
|
|
139
|
-
*
|
|
140
|
-
* // Run only axe and markuplint with Lanes progress
|
|
141
|
-
* await nitpicker.analyze(
|
|
142
|
-
* ['@nitpicker/analyze-axe', '@nitpicker/analyze-markuplint'],
|
|
143
|
-
* { lanes },
|
|
144
|
-
* );
|
|
145
|
-
* ```
|
|
146
|
-
*/
|
|
147
|
-
async analyze(filter?: string[], options?: AnalyzeOptions) {
|
|
148
|
-
const config = await this.getConfig();
|
|
149
|
-
const plugins = filter
|
|
150
|
-
? config.analyze.filter((plugin) => filter?.includes(plugin.name))
|
|
151
|
-
: config.analyze;
|
|
152
|
-
|
|
153
|
-
const analyzeMods = await importModules(plugins);
|
|
154
|
-
const lanes = options?.lanes;
|
|
155
|
-
|
|
156
|
-
const table = new Table();
|
|
157
|
-
|
|
158
|
-
for (const mod of analyzeMods) {
|
|
159
|
-
if (!mod.headers) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (!mod.eachPage) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
table.addHeaders(mod.headers);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const allViolations: Violation[] = [];
|
|
170
|
-
const cache = new Cache<{
|
|
171
|
-
pages?: Record<string, TableData<string>>;
|
|
172
|
-
violations?: Violation[];
|
|
173
|
-
}>('nitpicker-axe', path.join(os.tmpdir(), 'nitpicker/cache/table'));
|
|
174
|
-
|
|
175
|
-
await cache.clear();
|
|
176
|
-
|
|
177
|
-
// Build plugin metadata: lane IDs and display labels
|
|
178
|
-
const eachPagePlugins: Array<{ plugin: (typeof plugins)[number]; modIndex: number }> =
|
|
179
|
-
[];
|
|
180
|
-
for (const [i, plugin] of plugins.entries()) {
|
|
181
|
-
if (analyzeMods[i]?.eachPage) {
|
|
182
|
-
eachPagePlugins.push({ plugin, modIndex: i });
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const pluginLaneIds = new Map<string, number>();
|
|
187
|
-
const pluginLabels = new Map<string, string>();
|
|
188
|
-
const pluginCompletionDetails = new Map<string, string>();
|
|
189
|
-
|
|
190
|
-
for (const [laneId, { plugin, modIndex }] of eachPagePlugins.entries()) {
|
|
191
|
-
pluginLaneIds.set(plugin.name, laneId);
|
|
192
|
-
pluginLabels.set(plugin.name, analyzeMods[modIndex]?.label ?? plugin.name);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Initialize all lanes as Waiting
|
|
196
|
-
for (const [name, id] of pluginLaneIds) {
|
|
197
|
-
const label = pluginLabels.get(name) ?? name;
|
|
198
|
-
lanes?.update(id, c.dim(`${label}: Waiting...`));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
await this.archive.getPagesWithRefs(
|
|
202
|
-
100_000,
|
|
203
|
-
async (pages) => {
|
|
204
|
-
const urlEmitter = new UrlEventBus();
|
|
205
|
-
|
|
206
|
-
// Phase 1: eachPage plugins (sequentially, pages in parallel)
|
|
207
|
-
for (const [pluginSeqIndex, { plugin }] of eachPagePlugins.entries()) {
|
|
208
|
-
const laneId = pluginLaneIds.get(plugin.name)!;
|
|
209
|
-
const label = pluginLabels.get(plugin.name) ?? plugin.name;
|
|
210
|
-
let done = 0;
|
|
211
|
-
let pluginViolationCount = 0;
|
|
212
|
-
|
|
213
|
-
const updateProgress = () => {
|
|
214
|
-
const pluginPercent = Math.round((done / pages.length) * 100);
|
|
215
|
-
const overallPercent = Math.round(
|
|
216
|
-
((pluginSeqIndex + done / pages.length) / eachPagePlugins.length) * 100,
|
|
217
|
-
);
|
|
218
|
-
lanes?.header(
|
|
219
|
-
`[${pluginSeqIndex + 1}/${eachPagePlugins.length}] Analyzing (${overallPercent}%)`,
|
|
220
|
-
);
|
|
221
|
-
lanes?.update(
|
|
222
|
-
laneId,
|
|
223
|
-
`${label}: ${done}/${pages.length} (${pluginPercent}%)%braille%`,
|
|
224
|
-
);
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
updateProgress();
|
|
228
|
-
|
|
229
|
-
// Bounded Promise pool (replaces deal())
|
|
230
|
-
const executing = new Set<Promise<void>>();
|
|
231
|
-
|
|
232
|
-
for (const [pageIndex, page] of pages.entries()) {
|
|
233
|
-
const task = (async () => {
|
|
234
|
-
const cacheKey = `${plugin.name}:${page.url.href}`;
|
|
235
|
-
const cached = await cache.load(cacheKey);
|
|
236
|
-
if (cached) {
|
|
237
|
-
const { pages: cachedPages, violations } = cached;
|
|
238
|
-
if (cachedPages) {
|
|
239
|
-
table.addData(cachedPages);
|
|
240
|
-
}
|
|
241
|
-
if (violations) {
|
|
242
|
-
allViolations.push(...violations);
|
|
243
|
-
pluginViolationCount += violations.length;
|
|
244
|
-
}
|
|
245
|
-
done++;
|
|
246
|
-
updateProgress();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const html = await page.getHtml();
|
|
251
|
-
if (!html) {
|
|
252
|
-
done++;
|
|
253
|
-
updateProgress();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const report = await runInWorker<
|
|
258
|
-
PageAnalysisWorkerData,
|
|
259
|
-
ReportPage<string> | null
|
|
260
|
-
>({
|
|
261
|
-
filePath: pageAnalysisWorkerPath,
|
|
262
|
-
num: pageIndex,
|
|
263
|
-
total: pages.length,
|
|
264
|
-
emitter: urlEmitter,
|
|
265
|
-
initialData: {
|
|
266
|
-
plugin,
|
|
267
|
-
pages: {
|
|
268
|
-
html,
|
|
269
|
-
url: page.url,
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const tablePages: Record<string, TableData<string>> = {};
|
|
275
|
-
|
|
276
|
-
if (report?.page) {
|
|
277
|
-
tablePages[page.url.href] = report.page;
|
|
278
|
-
table.addDataToUrl(page.url, report.page);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
await cache.store(cacheKey, {
|
|
282
|
-
pages: Object.keys(tablePages).length > 0 ? tablePages : undefined,
|
|
283
|
-
violations: report?.violations,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
if (report?.violations) {
|
|
287
|
-
allViolations.push(...report.violations);
|
|
288
|
-
pluginViolationCount += report.violations.length;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
done++;
|
|
292
|
-
updateProgress();
|
|
293
|
-
})();
|
|
294
|
-
executing.add(task);
|
|
295
|
-
task.then(
|
|
296
|
-
() => executing.delete(task),
|
|
297
|
-
() => executing.delete(task),
|
|
298
|
-
);
|
|
299
|
-
if (executing.size >= CONCURRENCY_LIMIT) {
|
|
300
|
-
await Promise.race(executing);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
await Promise.all(executing);
|
|
305
|
-
|
|
306
|
-
// Mark this plugin as Done
|
|
307
|
-
const detail =
|
|
308
|
-
pluginViolationCount > 0
|
|
309
|
-
? `${pluginViolationCount} violations`
|
|
310
|
-
: `${done} pages`;
|
|
311
|
-
pluginCompletionDetails.set(plugin.name, detail);
|
|
312
|
-
lanes?.update(laneId, c.green(`${label}: Done (${detail})`));
|
|
313
|
-
|
|
314
|
-
// Dim inactive lanes
|
|
315
|
-
for (const [name, id] of pluginLaneIds) {
|
|
316
|
-
if (name === plugin.name) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
const otherLabel = pluginLabels.get(name) ?? name;
|
|
320
|
-
const completionDetail = pluginCompletionDetails.get(name);
|
|
321
|
-
if (completionDetail) {
|
|
322
|
-
lanes?.update(id, c.dim(`${otherLabel}: Done (${completionDetail})`));
|
|
323
|
-
} else {
|
|
324
|
-
lanes?.update(id, c.dim(`${otherLabel}: Waiting...`));
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Phase 2: eachUrl plugins (main thread, sequential)
|
|
330
|
-
for (const page of pages) {
|
|
331
|
-
const url = page.url;
|
|
332
|
-
const isExternal = page.isExternal;
|
|
333
|
-
|
|
334
|
-
for (const mod of analyzeMods) {
|
|
335
|
-
if (!mod.eachUrl) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const report = await mod.eachUrl({ url, isExternal });
|
|
340
|
-
if (!report) {
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const { page: reportPage, violations } = report;
|
|
345
|
-
|
|
346
|
-
if (reportPage) {
|
|
347
|
-
table.addDataToUrl(url, reportPage);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (violations) {
|
|
351
|
-
allViolations.push(...violations);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
withRefs: false,
|
|
358
|
-
},
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
const report: Report = {
|
|
362
|
-
name: 'general',
|
|
363
|
-
pageData: table.toJSON(),
|
|
364
|
-
violations: allViolations,
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
await this.archive.setData('analysis/report', report);
|
|
368
|
-
await this.archive.setData('analysis/table', table);
|
|
369
|
-
await this.archive.setData('analysis/violations', allViolations);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Loads and caches the plugin configuration from the user's config file.
|
|
374
|
-
*
|
|
375
|
-
* Uses cosmiconfig to search for `.nitpickerrc`, `.nitpickerrc.json`,
|
|
376
|
-
* `nitpicker.config.js`, or a `nitpicker` key in `package.json`.
|
|
377
|
-
* The result is cached after the first call.
|
|
378
|
-
* @returns Resolved {@link Config} with the `analyze` plugin list.
|
|
379
|
-
*/
|
|
380
|
-
async getConfig() {
|
|
381
|
-
if (!this.#config) {
|
|
382
|
-
this.#config = await loadPluginSettings();
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return this.#config;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Writes the archive (including any new analysis results) to disk
|
|
390
|
-
* as a `.nitpicker` tar file, then emits a `writeFile` event.
|
|
391
|
-
* @fires NitpickerEvent#writeFile
|
|
392
|
-
*/
|
|
393
|
-
async write() {
|
|
394
|
-
await this.#archive.write();
|
|
395
|
-
await this.emit('writeFile', { filePath: this.#archive.filePath });
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Opens an existing `.nitpicker` archive file and returns a ready-to-use
|
|
400
|
-
* Nitpicker instance.
|
|
401
|
-
*
|
|
402
|
-
* This is the recommended way to create a Nitpicker instance. It extracts
|
|
403
|
-
* the archive to a temporary directory, opens the SQLite database, and
|
|
404
|
-
* enables plugin data access.
|
|
405
|
-
* @param filePath - Path to the `.nitpicker` archive file.
|
|
406
|
-
* @returns A new Nitpicker instance backed by the opened archive.
|
|
407
|
-
* @example
|
|
408
|
-
* ```ts
|
|
409
|
-
* const nitpicker = await Nitpicker.open('./site.nitpicker');
|
|
410
|
-
* await nitpicker.analyze();
|
|
411
|
-
* await nitpicker.write();
|
|
412
|
-
* ```
|
|
413
|
-
*/
|
|
414
|
-
static async open(filePath: string) {
|
|
415
|
-
const archive = await Archive.open({ filePath, openPluginData: true });
|
|
416
|
-
return new Nitpicker(archive);
|
|
417
|
-
}
|
|
418
|
-
}
|