@nitpicker/core 0.4.1 → 0.4.3
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 -8
- 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,287 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { UrlEventBus } from './url-event-bus.js';
|
|
4
|
-
|
|
5
|
-
vi.mock('./import-modules.js', () => ({
|
|
6
|
-
importModules: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
const { importModules } = await import('./import-modules.js');
|
|
10
|
-
const mockedImportModules = vi.mocked(importModules);
|
|
11
|
-
|
|
12
|
-
const { parseUrl } = await import('@d-zero/shared/parse-url');
|
|
13
|
-
|
|
14
|
-
describe('page-analysis-worker', () => {
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
16
|
-
let workerFn: typeof import('./page-analysis-worker.js').default;
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
vi.clearAllMocks();
|
|
20
|
-
const mod = await import('./page-analysis-worker.js');
|
|
21
|
-
workerFn = mod.default;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('returns null when plugin has no eachPage', async () => {
|
|
25
|
-
mockedImportModules.mockResolvedValue([{ label: 'No-op plugin' }]);
|
|
26
|
-
|
|
27
|
-
const result = await workerFn(
|
|
28
|
-
{
|
|
29
|
-
plugin: {
|
|
30
|
-
name: 'test-plugin',
|
|
31
|
-
module: 'test-plugin',
|
|
32
|
-
configFilePath: '',
|
|
33
|
-
},
|
|
34
|
-
pages: {
|
|
35
|
-
html: '<html><body>Hello</body></html>',
|
|
36
|
-
url: parseUrl('https://example.com/'),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
new UrlEventBus(),
|
|
40
|
-
0,
|
|
41
|
-
1,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
expect(result).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('calls eachPage with correct arguments and returns report', async () => {
|
|
48
|
-
let capturedUrl: unknown;
|
|
49
|
-
let capturedHtml: unknown;
|
|
50
|
-
let capturedNum: unknown;
|
|
51
|
-
let capturedTotal: unknown;
|
|
52
|
-
let hadWindow = false;
|
|
53
|
-
let hadDocument = false;
|
|
54
|
-
|
|
55
|
-
const eachPage = vi.fn().mockImplementation((arg: Record<string, unknown>) => {
|
|
56
|
-
// Capture state inside callback before JSDOM cleanup in finally block
|
|
57
|
-
capturedUrl = arg.url;
|
|
58
|
-
capturedHtml = arg.html;
|
|
59
|
-
capturedNum = arg.num;
|
|
60
|
-
capturedTotal = arg.total;
|
|
61
|
-
hadWindow = arg.window != null;
|
|
62
|
-
hadDocument = (arg.window as { document?: unknown } | undefined)?.document != null;
|
|
63
|
-
return {
|
|
64
|
-
page: { title: { value: 'Test' } },
|
|
65
|
-
violations: [{ message: 'test violation', severity: 'error' }],
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
mockedImportModules.mockResolvedValue([{ eachPage }]);
|
|
69
|
-
|
|
70
|
-
const url = parseUrl('https://example.com/page');
|
|
71
|
-
const html = '<html><body><h1>Hello</h1></body></html>';
|
|
72
|
-
|
|
73
|
-
const result = await workerFn(
|
|
74
|
-
{
|
|
75
|
-
plugin: {
|
|
76
|
-
name: 'test-plugin',
|
|
77
|
-
module: 'test-plugin',
|
|
78
|
-
configFilePath: '',
|
|
79
|
-
},
|
|
80
|
-
pages: { html, url },
|
|
81
|
-
},
|
|
82
|
-
new UrlEventBus(),
|
|
83
|
-
3,
|
|
84
|
-
10,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
expect(eachPage).toHaveBeenCalledOnce();
|
|
88
|
-
expect(capturedUrl).toBe(url);
|
|
89
|
-
expect(capturedHtml).toBe(html);
|
|
90
|
-
expect(capturedNum).toBe(3);
|
|
91
|
-
expect(capturedTotal).toBe(10);
|
|
92
|
-
expect(hadWindow).toBe(true);
|
|
93
|
-
expect(hadDocument).toBe(true);
|
|
94
|
-
|
|
95
|
-
expect(result).toEqual({
|
|
96
|
-
page: { title: { value: 'Test' } },
|
|
97
|
-
violations: [{ message: 'test violation', severity: 'error' }],
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('emits url event on UrlEventBus', async () => {
|
|
102
|
-
mockedImportModules.mockResolvedValue([
|
|
103
|
-
{ eachPage: vi.fn().mockResolvedValue(null) },
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
const bus = new UrlEventBus();
|
|
107
|
-
const handler = vi.fn();
|
|
108
|
-
bus.on('url', handler);
|
|
109
|
-
|
|
110
|
-
await workerFn(
|
|
111
|
-
{
|
|
112
|
-
plugin: {
|
|
113
|
-
name: 'test-plugin',
|
|
114
|
-
module: 'test-plugin',
|
|
115
|
-
configFilePath: '',
|
|
116
|
-
},
|
|
117
|
-
pages: {
|
|
118
|
-
html: '<html></html>',
|
|
119
|
-
url: parseUrl('https://example.com/test'),
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
bus,
|
|
123
|
-
0,
|
|
124
|
-
1,
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
expect(handler).toHaveBeenCalledWith('https://example.com/test');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('returns null when eachPage returns undefined', async () => {
|
|
131
|
-
mockedImportModules.mockResolvedValue([{ eachPage: vi.fn().mockResolvedValue() }]);
|
|
132
|
-
|
|
133
|
-
const result = await workerFn(
|
|
134
|
-
{
|
|
135
|
-
plugin: {
|
|
136
|
-
name: 'test-plugin',
|
|
137
|
-
module: 'test-plugin',
|
|
138
|
-
configFilePath: '',
|
|
139
|
-
},
|
|
140
|
-
pages: {
|
|
141
|
-
html: '<html></html>',
|
|
142
|
-
url: parseUrl('https://example.com/'),
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
new UrlEventBus(),
|
|
146
|
-
0,
|
|
147
|
-
1,
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
expect(result).toBeNull();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('returns null and logs error when eachPage throws', async () => {
|
|
154
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
155
|
-
mockedImportModules.mockResolvedValue([
|
|
156
|
-
{
|
|
157
|
-
eachPage: vi.fn().mockRejectedValue(new Error('plugin crash')),
|
|
158
|
-
},
|
|
159
|
-
]);
|
|
160
|
-
|
|
161
|
-
const result = await workerFn(
|
|
162
|
-
{
|
|
163
|
-
plugin: {
|
|
164
|
-
name: 'broken-plugin',
|
|
165
|
-
module: 'broken-plugin',
|
|
166
|
-
configFilePath: '',
|
|
167
|
-
},
|
|
168
|
-
pages: {
|
|
169
|
-
html: '<html></html>',
|
|
170
|
-
url: parseUrl('https://example.com/'),
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
new UrlEventBus(),
|
|
174
|
-
0,
|
|
175
|
-
1,
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
expect(result).toBeNull();
|
|
179
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith('[broken-plugin] plugin crash');
|
|
180
|
-
consoleErrorSpy.mockRestore();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('cleans up JSDOM globals after eachPage completes', async () => {
|
|
184
|
-
let capturedKeys: string[] = [];
|
|
185
|
-
mockedImportModules.mockResolvedValue([
|
|
186
|
-
{
|
|
187
|
-
eachPage: vi.fn().mockImplementation(() => {
|
|
188
|
-
// Capture some JSDOM-injected globals during execution
|
|
189
|
-
capturedKeys = Object.getOwnPropertyNames(globalThis).filter(
|
|
190
|
-
(k) => k === 'HTMLElement' || k === 'NodeList',
|
|
191
|
-
);
|
|
192
|
-
return null;
|
|
193
|
-
}),
|
|
194
|
-
},
|
|
195
|
-
]);
|
|
196
|
-
|
|
197
|
-
await workerFn(
|
|
198
|
-
{
|
|
199
|
-
plugin: {
|
|
200
|
-
name: 'test-plugin',
|
|
201
|
-
module: 'test-plugin',
|
|
202
|
-
configFilePath: '',
|
|
203
|
-
},
|
|
204
|
-
pages: {
|
|
205
|
-
html: '<html><body></body></html>',
|
|
206
|
-
url: parseUrl('https://example.com/'),
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
new UrlEventBus(),
|
|
210
|
-
0,
|
|
211
|
-
1,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// JSDOM globals should be available during eachPage
|
|
215
|
-
expect(capturedKeys.length).toBeGreaterThan(0);
|
|
216
|
-
|
|
217
|
-
// But cleaned up after
|
|
218
|
-
const g = globalThis as Record<string, unknown>;
|
|
219
|
-
expect(g['HTMLElement']).toBeUndefined();
|
|
220
|
-
expect(g['NodeList']).toBeUndefined();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('cleans up JSDOM globals even when eachPage throws', async () => {
|
|
224
|
-
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
225
|
-
mockedImportModules.mockResolvedValue([
|
|
226
|
-
{
|
|
227
|
-
eachPage: vi.fn().mockRejectedValue(new Error('crash')),
|
|
228
|
-
},
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
|
-
await workerFn(
|
|
232
|
-
{
|
|
233
|
-
plugin: {
|
|
234
|
-
name: 'test-plugin',
|
|
235
|
-
module: 'test-plugin',
|
|
236
|
-
configFilePath: '',
|
|
237
|
-
},
|
|
238
|
-
pages: {
|
|
239
|
-
html: '<html><body></body></html>',
|
|
240
|
-
url: parseUrl('https://example.com/'),
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
new UrlEventBus(),
|
|
244
|
-
0,
|
|
245
|
-
1,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const g = globalThis as Record<string, unknown>;
|
|
249
|
-
expect(g['HTMLElement']).toBeUndefined();
|
|
250
|
-
vi.restoreAllMocks();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('provides a JSDOM window with the correct URL', async () => {
|
|
254
|
-
let receivedUrl = '';
|
|
255
|
-
mockedImportModules.mockResolvedValue([
|
|
256
|
-
{
|
|
257
|
-
eachPage: vi
|
|
258
|
-
.fn()
|
|
259
|
-
.mockImplementation(
|
|
260
|
-
({ window }: { window: { location: { href: string } } }) => {
|
|
261
|
-
receivedUrl = window.location.href;
|
|
262
|
-
return null;
|
|
263
|
-
},
|
|
264
|
-
),
|
|
265
|
-
},
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
|
-
await workerFn(
|
|
269
|
-
{
|
|
270
|
-
plugin: {
|
|
271
|
-
name: 'test-plugin',
|
|
272
|
-
module: 'test-plugin',
|
|
273
|
-
configFilePath: '',
|
|
274
|
-
},
|
|
275
|
-
pages: {
|
|
276
|
-
html: '<html></html>',
|
|
277
|
-
url: parseUrl('https://example.com/specific-page'),
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
new UrlEventBus(),
|
|
281
|
-
0,
|
|
282
|
-
1,
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
expect(receivedUrl).toBe('https://example.com/specific-page');
|
|
286
|
-
});
|
|
287
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
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
|
-
|
|
19
|
-
import type { Plugin, ReportPage } from './types.js';
|
|
20
|
-
import type { UrlEventBus } from './url-event-bus.js';
|
|
21
|
-
import type { ExURL as URL } from '@d-zero/shared/parse-url';
|
|
22
|
-
|
|
23
|
-
import { JSDOM } from 'jsdom';
|
|
24
|
-
|
|
25
|
-
import { importModules } from './import-modules.js';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Set of critical Node.js global properties that must never be overwritten
|
|
29
|
-
* by JSDOM window properties.
|
|
30
|
-
*/
|
|
31
|
-
const PROTECTED_GLOBALS = new Set([
|
|
32
|
-
'process',
|
|
33
|
-
'global',
|
|
34
|
-
'globalThis',
|
|
35
|
-
'console',
|
|
36
|
-
'Buffer',
|
|
37
|
-
'setTimeout',
|
|
38
|
-
'setInterval',
|
|
39
|
-
'clearTimeout',
|
|
40
|
-
'clearInterval',
|
|
41
|
-
'setImmediate',
|
|
42
|
-
'clearImmediate',
|
|
43
|
-
'queueMicrotask',
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Initial data payload for the page analysis worker.
|
|
48
|
-
* Passed via `workerData` and consumed by the default export.
|
|
49
|
-
*
|
|
50
|
-
* Uses `type` instead of `interface` because this type must satisfy the
|
|
51
|
-
* `Record<string, unknown>` constraint required by the Worker data serialization.
|
|
52
|
-
*/
|
|
53
|
-
export type PageAnalysisWorkerData = {
|
|
54
|
-
/** Single analyze plugin to execute against the page. */
|
|
55
|
-
plugin: Plugin;
|
|
56
|
-
/** Page data containing raw HTML and parsed URL. */
|
|
57
|
-
pages: { html: string; url: URL };
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Executes a single plugin's `eachPage` hook against a single page.
|
|
62
|
-
* @template T - Column key union from the plugin's headers.
|
|
63
|
-
* @param data - Contains the single plugin and page data (HTML + URL).
|
|
64
|
-
* @param urlEventBus - Event bus for URL discovery events (forwarded to the main thread).
|
|
65
|
-
* @param num - Zero-based index of the current page in the batch.
|
|
66
|
-
* @param total - Total number of pages being processed.
|
|
67
|
-
* @returns Report data from the plugin, or `null` if skipped.
|
|
68
|
-
* @see {@link ./worker/runner.ts!runner} for how this function is called
|
|
69
|
-
* @see {@link ./nitpicker.ts!Nitpicker.analyze} for the orchestration context
|
|
70
|
-
*/
|
|
71
|
-
export default async function <T extends string>(
|
|
72
|
-
data: PageAnalysisWorkerData,
|
|
73
|
-
urlEventBus: UrlEventBus,
|
|
74
|
-
num: number,
|
|
75
|
-
total: number,
|
|
76
|
-
): Promise<ReportPage<T> | null> {
|
|
77
|
-
const {
|
|
78
|
-
plugin,
|
|
79
|
-
pages: { html, url },
|
|
80
|
-
} = data;
|
|
81
|
-
const [analyzeMod] = await importModules([plugin]);
|
|
82
|
-
|
|
83
|
-
if (!analyzeMod?.eachPage) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
await urlEventBus.emit('url', url.href);
|
|
88
|
-
|
|
89
|
-
const dom = new JSDOM(html, {
|
|
90
|
-
url: url.href,
|
|
91
|
-
runScripts: 'outside-only',
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Expose JSDOM globals so that browser-oriented libraries
|
|
95
|
-
// (axe-core, @medv/finder, etc.) that inspect the global scope
|
|
96
|
-
// can find `window`, `document`, `Node`, and other DOM APIs.
|
|
97
|
-
const g = globalThis as Record<string, unknown>;
|
|
98
|
-
const domGlobalKeys: string[] = [];
|
|
99
|
-
for (const key of Object.getOwnPropertyNames(dom.window)) {
|
|
100
|
-
if (key in g || PROTECTED_GLOBALS.has(key)) {
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
try {
|
|
104
|
-
g[key] = dom.window[key as keyof typeof dom.window];
|
|
105
|
-
domGlobalKeys.push(key);
|
|
106
|
-
} catch {
|
|
107
|
-
// Some window properties throw on access — skip them
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const report = await analyzeMod.eachPage({
|
|
113
|
-
url,
|
|
114
|
-
html,
|
|
115
|
-
window: dom.window,
|
|
116
|
-
num,
|
|
117
|
-
total,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
return report ?? null;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
// eslint-disable-next-line no-console
|
|
123
|
-
console.error(`[${plugin.name}] ${error instanceof Error ? error.message : error}`);
|
|
124
|
-
return null;
|
|
125
|
-
} finally {
|
|
126
|
-
for (const key of domGlobalKeys) {
|
|
127
|
-
delete g[key];
|
|
128
|
-
}
|
|
129
|
-
dom.window.close();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from './types.js';
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
-
|
|
5
|
-
describe('readPluginLabels', () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
vi.resetModules();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('reads labels from plugins', async () => {
|
|
11
|
-
vi.doMock('label-plugin-a', () => ({
|
|
12
|
-
default: () => ({ label: 'axe: アクセシビリティチェック' }),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
16
|
-
|
|
17
|
-
const plugins: Plugin[] = [
|
|
18
|
-
{
|
|
19
|
-
name: '@nitpicker/analyze-axe',
|
|
20
|
-
module: 'label-plugin-a',
|
|
21
|
-
configFilePath: '/config',
|
|
22
|
-
},
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const labels = await readPluginLabels(plugins);
|
|
26
|
-
|
|
27
|
-
expect(labels.get('@nitpicker/analyze-axe')).toBe('axe: アクセシビリティチェック');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('reads labels from multiple plugins', async () => {
|
|
31
|
-
vi.doMock('label-plugin-b1', () => ({
|
|
32
|
-
default: () => ({ label: 'Plugin B1' }),
|
|
33
|
-
}));
|
|
34
|
-
vi.doMock('label-plugin-b2', () => ({
|
|
35
|
-
default: () => ({ label: 'Plugin B2' }),
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
39
|
-
|
|
40
|
-
const plugins: Plugin[] = [
|
|
41
|
-
{ name: 'b1', module: 'label-plugin-b1', configFilePath: '' },
|
|
42
|
-
{ name: 'b2', module: 'label-plugin-b2', configFilePath: '' },
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
const labels = await readPluginLabels(plugins);
|
|
46
|
-
|
|
47
|
-
expect(labels.size).toBe(2);
|
|
48
|
-
expect(labels.get('b1')).toBe('Plugin B1');
|
|
49
|
-
expect(labels.get('b2')).toBe('Plugin B2');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('skips plugins that lack a label', async () => {
|
|
53
|
-
vi.doMock('label-plugin-c', () => ({
|
|
54
|
-
default: () => ({ headers: { col: 'Column' } }),
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
58
|
-
|
|
59
|
-
const plugins: Plugin[] = [
|
|
60
|
-
{ name: 'no-label', module: 'label-plugin-c', configFilePath: '' },
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
const labels = await readPluginLabels(plugins);
|
|
64
|
-
|
|
65
|
-
expect(labels.size).toBe(0);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('skips plugins whose import fails', async () => {
|
|
69
|
-
vi.doMock('label-plugin-broken', () => {
|
|
70
|
-
throw new Error('module not found');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
74
|
-
|
|
75
|
-
const plugins: Plugin[] = [
|
|
76
|
-
{ name: 'broken', module: 'label-plugin-broken', configFilePath: '' },
|
|
77
|
-
];
|
|
78
|
-
|
|
79
|
-
const labels = await readPluginLabels(plugins);
|
|
80
|
-
|
|
81
|
-
expect(labels.size).toBe(0);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('skips plugins whose factory throws', async () => {
|
|
85
|
-
vi.doMock('label-plugin-throw', () => ({
|
|
86
|
-
default: () => {
|
|
87
|
-
throw new Error('factory error');
|
|
88
|
-
},
|
|
89
|
-
}));
|
|
90
|
-
|
|
91
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
92
|
-
|
|
93
|
-
const plugins: Plugin[] = [
|
|
94
|
-
{ name: 'throw', module: 'label-plugin-throw', configFilePath: '' },
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const labels = await readPluginLabels(plugins);
|
|
98
|
-
|
|
99
|
-
expect(labels.size).toBe(0);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('passes settings and configFilePath to factory', async () => {
|
|
103
|
-
const factory = vi.fn().mockReturnValue({ label: 'Configured' });
|
|
104
|
-
vi.doMock('label-plugin-d', () => ({ default: factory }));
|
|
105
|
-
|
|
106
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
107
|
-
|
|
108
|
-
const plugins: Plugin[] = [
|
|
109
|
-
{
|
|
110
|
-
name: 'configured',
|
|
111
|
-
module: 'label-plugin-d',
|
|
112
|
-
configFilePath: '/path/to/.nitpickerrc',
|
|
113
|
-
settings: { lang: 'ja' },
|
|
114
|
-
},
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
await readPluginLabels(plugins);
|
|
118
|
-
|
|
119
|
-
expect(factory).toHaveBeenCalledWith({ lang: 'ja' }, '/path/to/.nitpickerrc');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('returns empty map for empty plugin list', async () => {
|
|
123
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
124
|
-
const labels = await readPluginLabels([]);
|
|
125
|
-
|
|
126
|
-
expect(labels.size).toBe(0);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('collects labels from working plugins even when some fail', async () => {
|
|
130
|
-
vi.doMock('label-plugin-good', () => ({
|
|
131
|
-
default: () => ({ label: 'Good Plugin' }),
|
|
132
|
-
}));
|
|
133
|
-
vi.doMock('label-plugin-bad', () => ({
|
|
134
|
-
default: () => {
|
|
135
|
-
throw new Error('bad');
|
|
136
|
-
},
|
|
137
|
-
}));
|
|
138
|
-
|
|
139
|
-
const { readPluginLabels } = await import('./read-plugin-labels.js');
|
|
140
|
-
|
|
141
|
-
const plugins: Plugin[] = [
|
|
142
|
-
{ name: 'good', module: 'label-plugin-good', configFilePath: '' },
|
|
143
|
-
{ name: 'bad', module: 'label-plugin-bad', configFilePath: '' },
|
|
144
|
-
];
|
|
145
|
-
|
|
146
|
-
const labels = await readPluginLabels(plugins);
|
|
147
|
-
|
|
148
|
-
expect(labels.size).toBe(1);
|
|
149
|
-
expect(labels.get('good')).toBe('Good Plugin');
|
|
150
|
-
});
|
|
151
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Reads the `label` property from each plugin by importing and
|
|
5
|
-
* initializing the plugin module.
|
|
6
|
-
*
|
|
7
|
-
* Each plugin module's default export (a `PluginFactory`) is called
|
|
8
|
-
* with the plugin's configured settings. The resulting `AnalyzePlugin`
|
|
9
|
-
* object's `label` field is collected into a Map keyed by plugin name.
|
|
10
|
-
*
|
|
11
|
-
* Plugins that fail to import or initialize, or that lack a `label`,
|
|
12
|
-
* are silently skipped.
|
|
13
|
-
* @param plugins - Array of plugin definitions from the resolved config.
|
|
14
|
-
* @returns Map from plugin name to its human-readable label.
|
|
15
|
-
*/
|
|
16
|
-
export async function readPluginLabels(
|
|
17
|
-
plugins: readonly Plugin[],
|
|
18
|
-
): Promise<Map<string, string>> {
|
|
19
|
-
const labels = new Map<string, string>();
|
|
20
|
-
|
|
21
|
-
await Promise.all(
|
|
22
|
-
plugins.map(async (plugin) => {
|
|
23
|
-
try {
|
|
24
|
-
const mod = await import(plugin.module);
|
|
25
|
-
const factory = mod.default;
|
|
26
|
-
const instance = await factory(plugin.settings ?? {}, plugin.configFilePath);
|
|
27
|
-
if (instance && typeof instance.label === 'string') {
|
|
28
|
-
labels.set(plugin.name, instance.label);
|
|
29
|
-
}
|
|
30
|
-
} catch {
|
|
31
|
-
// Module not importable or factory failed — skip
|
|
32
|
-
}
|
|
33
|
-
}),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return labels;
|
|
37
|
-
}
|
package/src/table.spec.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { Table } from './table.js';
|
|
4
|
-
|
|
5
|
-
describe('Table', () => {
|
|
6
|
-
it('starts with empty headers and data', () => {
|
|
7
|
-
const table = new Table<'a'>();
|
|
8
|
-
const json = table.toJSON();
|
|
9
|
-
expect(json.headers).toEqual({});
|
|
10
|
-
expect(json.data).toEqual({});
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('addHeaders registers column headers', () => {
|
|
14
|
-
const table = new Table<'title' | 'score'>();
|
|
15
|
-
table.addHeaders({ title: 'Page Title', score: 'Score' });
|
|
16
|
-
const json = table.toJSON();
|
|
17
|
-
expect(json.headers).toEqual({ title: 'Page Title', score: 'Score' });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('addHeaders merges headers from multiple calls', () => {
|
|
21
|
-
const table = new Table<'a' | 'b'>();
|
|
22
|
-
table.addHeaders({ a: 'A' } as Record<'a' | 'b', string>);
|
|
23
|
-
table.addHeaders({ b: 'B' } as Record<'a' | 'b', string>);
|
|
24
|
-
const json = table.toJSON();
|
|
25
|
-
expect(json.headers).toEqual({ a: 'A', b: 'B' });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('addHeaders overwrites duplicate keys with the later value', () => {
|
|
29
|
-
const table = new Table<'x'>();
|
|
30
|
-
table.addHeaders({ x: 'First' });
|
|
31
|
-
table.addHeaders({ x: 'Second' });
|
|
32
|
-
expect(table.toJSON().headers).toEqual({ x: 'Second' });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('addDataToUrl stores data for a URL', () => {
|
|
36
|
-
const table = new Table<'col'>();
|
|
37
|
-
const url = { href: 'https://example.com/' } as { href: string };
|
|
38
|
-
table.addDataToUrl(url, { col: { value: 'hello' } });
|
|
39
|
-
const json = table.toJSON();
|
|
40
|
-
expect(json.data['https://example.com/']).toEqual({ col: { value: 'hello' } });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('addDataToUrl merges data for the same URL', () => {
|
|
44
|
-
const table = new Table<'a' | 'b'>();
|
|
45
|
-
const url = { href: 'https://example.com/' } as { href: string };
|
|
46
|
-
table.addDataToUrl(url, { a: { value: 1 } } as Record<'a' | 'b', { value: unknown }>);
|
|
47
|
-
table.addDataToUrl(url, { b: { value: 2 } } as Record<'a' | 'b', { value: unknown }>);
|
|
48
|
-
const data = table.toJSON().data['https://example.com/'];
|
|
49
|
-
expect(data).toEqual({ a: { value: 1 }, b: { value: 2 } });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('addData stores batch data', () => {
|
|
53
|
-
const table = new Table<'col'>();
|
|
54
|
-
table.addData({
|
|
55
|
-
'https://a.com/': { col: { value: 'A' } },
|
|
56
|
-
'https://b.com/': { col: { value: 'B' } },
|
|
57
|
-
});
|
|
58
|
-
const json = table.toJSON();
|
|
59
|
-
expect(Object.keys(json.data)).toHaveLength(2);
|
|
60
|
-
expect(json.data['https://a.com/']).toEqual({ col: { value: 'A' } });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('getData returns data as a plain object', () => {
|
|
64
|
-
const table = new Table<'col'>();
|
|
65
|
-
const url = { href: 'https://example.com/' } as { href: string };
|
|
66
|
-
table.addDataToUrl(url, { col: { value: 42 } });
|
|
67
|
-
const data = table.getData();
|
|
68
|
-
expect(data['https://example.com/']).toEqual({ col: { value: 42 } });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('getDataByUrl returns data for a known URL', () => {
|
|
72
|
-
const table = new Table<'col'>();
|
|
73
|
-
const url = { href: 'https://example.com/' } as { href: string };
|
|
74
|
-
table.addDataToUrl(url, { col: { value: 'x' } });
|
|
75
|
-
expect(table.getDataByUrl(url)).toEqual({ col: { value: 'x' } });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('getDataByUrl returns undefined for unknown URL', () => {
|
|
79
|
-
const table = new Table<'col'>();
|
|
80
|
-
const url = { href: 'https://unknown.com/' } as { href: string };
|
|
81
|
-
expect(table.getDataByUrl(url)).toBeUndefined();
|
|
82
|
-
});
|
|
83
|
-
});
|