@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
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
type SequentialEachChunkCallback<R, T> = (item: T, i: number) => Promise<R>;
|
|
2
|
+
/**
|
|
3
|
+
* Sequential processing each chunk
|
|
4
|
+
*
|
|
5
|
+
* 1. Split an array
|
|
6
|
+
* ┌───────── array ────────┐
|
|
7
|
+
* chunk1 │ chunk2 | chunk3
|
|
8
|
+
* ├item ├item ├item
|
|
9
|
+
* ├item ├item ├item
|
|
10
|
+
* └item └item └item
|
|
11
|
+
*
|
|
12
|
+
* 2. Runs parallel and sequential
|
|
13
|
+
* chunk1 ┬ callback(item) ┐
|
|
14
|
+
* │ ├ callback(item) │ Promise.all
|
|
15
|
+
* │ └ callback(item) ┘
|
|
16
|
+
* │
|
|
17
|
+
* (Sequential)
|
|
18
|
+
* │
|
|
19
|
+
* chunk2 ┬ callback(item) ┐
|
|
20
|
+
* │ ├ callback(item) │ Promise.all
|
|
21
|
+
* │ └ callback(item) ┘
|
|
22
|
+
* │
|
|
23
|
+
* (Sequential)
|
|
24
|
+
* │
|
|
25
|
+
* chunk3 ┬ callback(item) ┐
|
|
26
|
+
* │ ├ callback(item) │ Promise.all
|
|
27
|
+
* │ └ callback(item) ┘
|
|
28
|
+
* │
|
|
29
|
+
* (Done)
|
|
30
|
+
* @param array
|
|
31
|
+
* @param split
|
|
32
|
+
* @param callback
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
export declare function sequentialEachChunk<R, T>(array: T[], split: number, callback: SequentialEachChunkCallback<R, T>): Promise<R[]>;
|
|
36
|
+
export {};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequential processing each chunk
|
|
3
|
+
*
|
|
4
|
+
* 1. Split an array
|
|
5
|
+
* ┌───────── array ────────┐
|
|
6
|
+
* chunk1 │ chunk2 | chunk3
|
|
7
|
+
* ├item ├item ├item
|
|
8
|
+
* ├item ├item ├item
|
|
9
|
+
* └item └item └item
|
|
10
|
+
*
|
|
11
|
+
* 2. Runs parallel and sequential
|
|
12
|
+
* chunk1 ┬ callback(item) ┐
|
|
13
|
+
* │ ├ callback(item) │ Promise.all
|
|
14
|
+
* │ └ callback(item) ┘
|
|
15
|
+
* │
|
|
16
|
+
* (Sequential)
|
|
17
|
+
* │
|
|
18
|
+
* chunk2 ┬ callback(item) ┐
|
|
19
|
+
* │ ├ callback(item) │ Promise.all
|
|
20
|
+
* │ └ callback(item) ┘
|
|
21
|
+
* │
|
|
22
|
+
* (Sequential)
|
|
23
|
+
* │
|
|
24
|
+
* chunk3 ┬ callback(item) ┐
|
|
25
|
+
* │ ├ callback(item) │ Promise.all
|
|
26
|
+
* │ └ callback(item) ┘
|
|
27
|
+
* │
|
|
28
|
+
* (Done)
|
|
29
|
+
* @param array
|
|
30
|
+
* @param split
|
|
31
|
+
* @param callback
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
export async function sequentialEachChunk(array, split, callback) {
|
|
35
|
+
array = [...array];
|
|
36
|
+
const result = [];
|
|
37
|
+
do {
|
|
38
|
+
const chunk = array.splice(0, split);
|
|
39
|
+
const segRes = await Promise.all(chunk.map((item, i) => callback(item, i + result.length)));
|
|
40
|
+
result.push(...segRes);
|
|
41
|
+
} while (array.length > 0);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { UrlEventBus } from '../url-event-bus.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parameters for {@link runInWorker}.
|
|
4
|
+
* @template I - Shape of the additional data merged into `workerData`.
|
|
5
|
+
*/
|
|
6
|
+
export interface RunInWorkerParams<I extends Record<string, unknown>> {
|
|
7
|
+
/** Absolute path to the module to execute in the Worker. */
|
|
8
|
+
readonly filePath: string;
|
|
9
|
+
/** Zero-based index of the current item (for progress display). */
|
|
10
|
+
readonly num: number;
|
|
11
|
+
/** Total number of items in the batch. */
|
|
12
|
+
readonly total: number;
|
|
13
|
+
/** URL event bus; `'url'` messages from the Worker are re-emitted here. */
|
|
14
|
+
readonly emitter: UrlEventBus;
|
|
15
|
+
/** Plugin-specific data to pass to the Worker module. */
|
|
16
|
+
readonly initialData: I;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Spawns a Worker thread to execute a plugin module and returns its result.
|
|
20
|
+
*
|
|
21
|
+
* This is the bridge between the main thread's `deal()` parallelism and the
|
|
22
|
+
* per-page Worker execution. Each call creates a new Worker, passes the
|
|
23
|
+
* initial data via `workerData`, and listens for messages until the Worker
|
|
24
|
+
* signals completion.
|
|
25
|
+
*
|
|
26
|
+
* ## Why Worker threads?
|
|
27
|
+
*
|
|
28
|
+
* DOM-heavy plugins (JSDOM + axe-core, markuplint, etc.) allocate significant
|
|
29
|
+
* memory per page. Running them in Workers ensures:
|
|
30
|
+
* - **Memory isolation**: JSDOM windows are fully GC'd when the Worker exits
|
|
31
|
+
* - **Crash containment**: A plugin segfault/OOM kills only the Worker, not the process
|
|
32
|
+
* - **Signal handling**: Graceful cleanup on SIGABRT, SIGQUIT, and other signals
|
|
33
|
+
*
|
|
34
|
+
* ## Message protocol
|
|
35
|
+
*
|
|
36
|
+
* The Worker sends two types of messages:
|
|
37
|
+
* - `{ type: 'url', url: string }` - URL discovery notification, forwarded to the emitter
|
|
38
|
+
* - `{ type: 'finish', result: R }` - Execution complete, resolves the Promise
|
|
39
|
+
*
|
|
40
|
+
* ## Fallback mode
|
|
41
|
+
*
|
|
42
|
+
* When `useWorker` is `false`, execution delegates directly to
|
|
43
|
+
* {@link ./runner.ts!runner} in the main thread.
|
|
44
|
+
* @template I - Shape of the additional data merged into `workerData`.
|
|
45
|
+
* @template R - Return type expected from the Worker module.
|
|
46
|
+
* @param params - Parameters containing file path, progress info, emitter, and data.
|
|
47
|
+
* @returns The result produced by the Worker module's default export.
|
|
48
|
+
* @see {@link ./worker.ts} for the Worker-side entry point
|
|
49
|
+
* @see {@link ./runner.ts!runner} for the direct (non-Worker) execution path
|
|
50
|
+
*/
|
|
51
|
+
export declare function runInWorker<I extends Record<string, unknown>, R>(params: RunInWorkerParams<I>): Promise<R>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { Worker } from 'node:worker_threads';
|
|
3
|
+
import { runner } from './runner.js';
|
|
4
|
+
const __filename = new URL(import.meta.url).pathname;
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
/** Resolved path to the compiled Worker thread entry point ({@link ./worker.ts}). */
|
|
7
|
+
const workerPath = path.resolve(__dirname, 'worker.js');
|
|
8
|
+
/**
|
|
9
|
+
* Feature flag controlling whether plugin execution uses Worker threads.
|
|
10
|
+
* When `true` (default), each plugin invocation runs in an isolated Worker,
|
|
11
|
+
* providing memory isolation and crash protection. When `false`, the runner
|
|
12
|
+
* executes directly in the main thread (useful for debugging).
|
|
13
|
+
*/
|
|
14
|
+
const useWorker = true;
|
|
15
|
+
/**
|
|
16
|
+
* Spawns a Worker thread to execute a plugin module and returns its result.
|
|
17
|
+
*
|
|
18
|
+
* This is the bridge between the main thread's `deal()` parallelism and the
|
|
19
|
+
* per-page Worker execution. Each call creates a new Worker, passes the
|
|
20
|
+
* initial data via `workerData`, and listens for messages until the Worker
|
|
21
|
+
* signals completion.
|
|
22
|
+
*
|
|
23
|
+
* ## Why Worker threads?
|
|
24
|
+
*
|
|
25
|
+
* DOM-heavy plugins (JSDOM + axe-core, markuplint, etc.) allocate significant
|
|
26
|
+
* memory per page. Running them in Workers ensures:
|
|
27
|
+
* - **Memory isolation**: JSDOM windows are fully GC'd when the Worker exits
|
|
28
|
+
* - **Crash containment**: A plugin segfault/OOM kills only the Worker, not the process
|
|
29
|
+
* - **Signal handling**: Graceful cleanup on SIGABRT, SIGQUIT, and other signals
|
|
30
|
+
*
|
|
31
|
+
* ## Message protocol
|
|
32
|
+
*
|
|
33
|
+
* The Worker sends two types of messages:
|
|
34
|
+
* - `{ type: 'url', url: string }` - URL discovery notification, forwarded to the emitter
|
|
35
|
+
* - `{ type: 'finish', result: R }` - Execution complete, resolves the Promise
|
|
36
|
+
*
|
|
37
|
+
* ## Fallback mode
|
|
38
|
+
*
|
|
39
|
+
* When `useWorker` is `false`, execution delegates directly to
|
|
40
|
+
* {@link ./runner.ts!runner} in the main thread.
|
|
41
|
+
* @template I - Shape of the additional data merged into `workerData`.
|
|
42
|
+
* @template R - Return type expected from the Worker module.
|
|
43
|
+
* @param params - Parameters containing file path, progress info, emitter, and data.
|
|
44
|
+
* @returns The result produced by the Worker module's default export.
|
|
45
|
+
* @see {@link ./worker.ts} for the Worker-side entry point
|
|
46
|
+
* @see {@link ./runner.ts!runner} for the direct (non-Worker) execution path
|
|
47
|
+
*/
|
|
48
|
+
export function runInWorker(params) {
|
|
49
|
+
const { filePath, num, total, emitter, initialData } = params;
|
|
50
|
+
if (useWorker) {
|
|
51
|
+
const worker = new Worker(workerPath, {
|
|
52
|
+
workerData: {
|
|
53
|
+
filePath,
|
|
54
|
+
num,
|
|
55
|
+
total,
|
|
56
|
+
...initialData,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const killWorker = async (sig) => {
|
|
61
|
+
await worker.terminate();
|
|
62
|
+
worker.unref();
|
|
63
|
+
worker.removeAllListeners();
|
|
64
|
+
process.removeListener('SIGABRT', killWorker);
|
|
65
|
+
process.removeListener('SIGLOST', killWorker);
|
|
66
|
+
process.removeListener('SIGQUIT', killWorker);
|
|
67
|
+
process.removeListener('disconnect', killWorker);
|
|
68
|
+
process.removeListener('exit', killWorker);
|
|
69
|
+
process.removeListener('uncaughtException', killWorker);
|
|
70
|
+
process.removeListener('uncaughtExceptionMonitor', killWorker);
|
|
71
|
+
process.removeListener('unhandledRejection', killWorker);
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.log(`Kill Worker cause: %O`, sig);
|
|
74
|
+
reject(`SIG: ${sig}`);
|
|
75
|
+
};
|
|
76
|
+
// Changed from old issue
|
|
77
|
+
// @see https://github.com/nodejs/node-v0.x-archive/issues/6339
|
|
78
|
+
// process.once('SIGKILL', killWorker);
|
|
79
|
+
// process.once('SIGSTOP', killWorker);
|
|
80
|
+
process.once('SIGABRT', killWorker);
|
|
81
|
+
process.once('SIGLOST', killWorker);
|
|
82
|
+
process.once('SIGQUIT', killWorker);
|
|
83
|
+
process.once('disconnect', killWorker);
|
|
84
|
+
process.once('exit', killWorker);
|
|
85
|
+
process.once('uncaughtException', killWorker);
|
|
86
|
+
process.once('uncaughtExceptionMonitor', killWorker);
|
|
87
|
+
process.once('unhandledRejection', killWorker);
|
|
88
|
+
worker.once('error', killWorker);
|
|
89
|
+
worker.once('messageerror', killWorker);
|
|
90
|
+
worker.on('message', async (message) => {
|
|
91
|
+
if (!message) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (message.type === 'url') {
|
|
95
|
+
void emitter.emit('url', message.url);
|
|
96
|
+
}
|
|
97
|
+
if (message.type === 'finish') {
|
|
98
|
+
await worker.terminate();
|
|
99
|
+
worker.removeAllListeners();
|
|
100
|
+
worker.unref();
|
|
101
|
+
process.removeListener('SIGABRT', killWorker);
|
|
102
|
+
process.removeListener('SIGLOST', killWorker);
|
|
103
|
+
process.removeListener('SIGQUIT', killWorker);
|
|
104
|
+
process.removeListener('disconnect', killWorker);
|
|
105
|
+
process.removeListener('exit', killWorker);
|
|
106
|
+
process.removeListener('uncaughtException', killWorker);
|
|
107
|
+
process.removeListener('uncaughtExceptionMonitor', killWorker);
|
|
108
|
+
process.removeListener('unhandledRejection', killWorker);
|
|
109
|
+
resolve(message.result);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return runner({
|
|
115
|
+
filePath,
|
|
116
|
+
num,
|
|
117
|
+
total,
|
|
118
|
+
...initialData,
|
|
119
|
+
}, emitter);
|
|
120
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WorkerData } from './types.js';
|
|
2
|
+
import type { UrlEventBus } from '../url-event-bus.js';
|
|
3
|
+
/**
|
|
4
|
+
* Dynamically imports and executes a plugin worker module.
|
|
5
|
+
*
|
|
6
|
+
* This function is the final execution step in the worker pipeline:
|
|
7
|
+
* 1. It `import()`s the module specified by `data.filePath`
|
|
8
|
+
* 2. Calls the module's default export with the remaining data, emitter,
|
|
9
|
+
* and progress counters
|
|
10
|
+
* 3. Returns the result
|
|
11
|
+
*
|
|
12
|
+
* The `filePath` field is deleted from `data` before passing it to the
|
|
13
|
+
* module function, so the module only receives its own domain-specific data.
|
|
14
|
+
*
|
|
15
|
+
* This function is called both from the Worker thread ({@link ./worker.ts})
|
|
16
|
+
* and as a direct fallback when `useWorker` is `false` in {@link ./run-in-worker.ts}.
|
|
17
|
+
* @template I - Shape of the caller's initial data (minus the worker infrastructure fields).
|
|
18
|
+
* @template R - Return type of the plugin module's default export.
|
|
19
|
+
* @param data - Combined worker data containing the module path and plugin-specific payload.
|
|
20
|
+
* @param emitter - Event emitter for URL discovery notifications (forwarded to the plugin).
|
|
21
|
+
* @returns The result produced by the dynamically imported module.
|
|
22
|
+
* @see {@link ./types.ts!WorkerData} for the data shape
|
|
23
|
+
* @see {@link ../page-analysis-worker.ts} for a typical module loaded by this runner
|
|
24
|
+
*/
|
|
25
|
+
export declare function runner<I extends Record<string, unknown>, R>(data: WorkerData<I>, emitter: UrlEventBus): Promise<R>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamically imports and executes a plugin worker module.
|
|
3
|
+
*
|
|
4
|
+
* This function is the final execution step in the worker pipeline:
|
|
5
|
+
* 1. It `import()`s the module specified by `data.filePath`
|
|
6
|
+
* 2. Calls the module's default export with the remaining data, emitter,
|
|
7
|
+
* and progress counters
|
|
8
|
+
* 3. Returns the result
|
|
9
|
+
*
|
|
10
|
+
* The `filePath` field is deleted from `data` before passing it to the
|
|
11
|
+
* module function, so the module only receives its own domain-specific data.
|
|
12
|
+
*
|
|
13
|
+
* This function is called both from the Worker thread ({@link ./worker.ts})
|
|
14
|
+
* and as a direct fallback when `useWorker` is `false` in {@link ./run-in-worker.ts}.
|
|
15
|
+
* @template I - Shape of the caller's initial data (minus the worker infrastructure fields).
|
|
16
|
+
* @template R - Return type of the plugin module's default export.
|
|
17
|
+
* @param data - Combined worker data containing the module path and plugin-specific payload.
|
|
18
|
+
* @param emitter - Event emitter for URL discovery notifications (forwarded to the plugin).
|
|
19
|
+
* @returns The result produced by the dynamically imported module.
|
|
20
|
+
* @see {@link ./types.ts!WorkerData} for the data shape
|
|
21
|
+
* @see {@link ../page-analysis-worker.ts} for a typical module loaded by this runner
|
|
22
|
+
*/
|
|
23
|
+
export async function runner(data, emitter) {
|
|
24
|
+
const { filePath, num, total } = data;
|
|
25
|
+
const mod = await import(filePath);
|
|
26
|
+
const fn = mod.default;
|
|
27
|
+
// @ts-expect-error
|
|
28
|
+
delete data.filePath;
|
|
29
|
+
const result = await fn(data, emitter, num, total);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data payload passed to a Worker thread via `workerData`.
|
|
3
|
+
*
|
|
4
|
+
* This type merges the worker infrastructure fields (`filePath`, `num`, `total`)
|
|
5
|
+
* with the caller-supplied initial data (`I`). The `filePath` points to the
|
|
6
|
+
* module whose default export will be invoked by the {@link ../runner.ts!runner}.
|
|
7
|
+
* @template I - Shape of the additional data the caller provides
|
|
8
|
+
* (e.g. {@link ../../page-analysis-worker.ts!PageAnalysisWorkerData}).
|
|
9
|
+
* @see {@link ../run-in-worker.ts!runInWorker} for where this payload is constructed
|
|
10
|
+
* @see {@link ../runner.ts!runner} for where it is consumed
|
|
11
|
+
*/
|
|
12
|
+
export type WorkerData<I extends Record<string, unknown>> = {
|
|
13
|
+
/**
|
|
14
|
+
* Absolute path to the compiled JS module to `import()` and execute.
|
|
15
|
+
* The module must export a default function matching the
|
|
16
|
+
* `(data, emitter, num, total) => Promise<R>` signature.
|
|
17
|
+
*/
|
|
18
|
+
filePath: string;
|
|
19
|
+
/** Zero-based index of the current item in the batch (for progress tracking). */
|
|
20
|
+
num: number;
|
|
21
|
+
/** Total number of items in the batch (for progress tracking). */
|
|
22
|
+
total: number;
|
|
23
|
+
} & I;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker thread entry point for plugin execution.
|
|
3
|
+
*
|
|
4
|
+
* This module runs inside a `new Worker(...)` created by {@link ./run-in-worker.ts!runInWorker}.
|
|
5
|
+
* It uses a message-based protocol to communicate with the main thread:
|
|
6
|
+
*
|
|
7
|
+
* ## Message protocol (Worker -> Main)
|
|
8
|
+
*
|
|
9
|
+
* | `type` | Payload | Description |
|
|
10
|
+
* |------------|------------------|------------------------------------------------|
|
|
11
|
+
* | `'url'` | `{ url: string }` | A URL was discovered during analysis |
|
|
12
|
+
* | `'finish'` | `{ result: R }` | Analysis complete, carries the plugin result |
|
|
13
|
+
*
|
|
14
|
+
* ## Lifecycle
|
|
15
|
+
*
|
|
16
|
+
* 1. Reads `workerData` containing the module path + plugin data
|
|
17
|
+
* 2. Creates a local {@link ../url-event-bus.ts!UrlEventBus} that forwards `'url'`
|
|
18
|
+
* events to the main thread via `parentPort.postMessage`
|
|
19
|
+
* 3. Delegates to {@link ./runner.ts!runner} for dynamic import and execution
|
|
20
|
+
* 4. Posts the `'finish'` message with the result
|
|
21
|
+
*
|
|
22
|
+
* The main thread terminates this Worker after receiving `'finish'`.
|
|
23
|
+
* @see {@link ./run-in-worker.ts!runInWorker} for the main-thread counterpart
|
|
24
|
+
* @see {@link ./runner.ts!runner} for the actual module loading logic
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker thread entry point for plugin execution.
|
|
3
|
+
*
|
|
4
|
+
* This module runs inside a `new Worker(...)` created by {@link ./run-in-worker.ts!runInWorker}.
|
|
5
|
+
* It uses a message-based protocol to communicate with the main thread:
|
|
6
|
+
*
|
|
7
|
+
* ## Message protocol (Worker -> Main)
|
|
8
|
+
*
|
|
9
|
+
* | `type` | Payload | Description |
|
|
10
|
+
* |------------|------------------|------------------------------------------------|
|
|
11
|
+
* | `'url'` | `{ url: string }` | A URL was discovered during analysis |
|
|
12
|
+
* | `'finish'` | `{ result: R }` | Analysis complete, carries the plugin result |
|
|
13
|
+
*
|
|
14
|
+
* ## Lifecycle
|
|
15
|
+
*
|
|
16
|
+
* 1. Reads `workerData` containing the module path + plugin data
|
|
17
|
+
* 2. Creates a local {@link ../url-event-bus.ts!UrlEventBus} that forwards `'url'`
|
|
18
|
+
* events to the main thread via `parentPort.postMessage`
|
|
19
|
+
* 3. Delegates to {@link ./runner.ts!runner} for dynamic import and execution
|
|
20
|
+
* 4. Posts the `'finish'` message with the result
|
|
21
|
+
*
|
|
22
|
+
* The main thread terminates this Worker after receiving `'finish'`.
|
|
23
|
+
* @see {@link ./run-in-worker.ts!runInWorker} for the main-thread counterpart
|
|
24
|
+
* @see {@link ./runner.ts!runner} for the actual module loading logic
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
28
|
+
import { UrlEventBus } from '../url-event-bus.js';
|
|
29
|
+
import { runner } from './runner.js';
|
|
30
|
+
const data = workerData;
|
|
31
|
+
const emitter = new UrlEventBus();
|
|
32
|
+
/**
|
|
33
|
+
* Forward URL discovery events from the plugin to the main thread.
|
|
34
|
+
* The main thread's {@link ../url-event-bus.ts!UrlEventBus} re-emits these
|
|
35
|
+
* so that the orchestrator can track discovered URLs.
|
|
36
|
+
*/
|
|
37
|
+
emitter.on('url', (url) => {
|
|
38
|
+
if (!parentPort) {
|
|
39
|
+
throw new Error('Use in worker thread');
|
|
40
|
+
}
|
|
41
|
+
parentPort.postMessage({
|
|
42
|
+
type: 'url',
|
|
43
|
+
url,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
const result = await runner(data, emitter);
|
|
47
|
+
if (!parentPort) {
|
|
48
|
+
throw new Error('Use in worker thread');
|
|
49
|
+
}
|
|
50
|
+
parentPort.postMessage({
|
|
51
|
+
type: 'finish',
|
|
52
|
+
result,
|
|
53
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nitpicker/core",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Plugin-based page analysis engine for Nitpicker",
|
|
5
|
+
"author": "D-ZERO",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/d-zero-dev/nitpicker.git",
|
|
10
|
+
"directory": "packages/@nitpicker/core"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./lib/index.js",
|
|
19
|
+
"types": "./lib/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"clean": "tsc --build --clean"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@d-zero/dealer": "1.6.3",
|
|
28
|
+
"@d-zero/shared": "0.20.0",
|
|
29
|
+
"@nitpicker/crawler": "0.4.1",
|
|
30
|
+
"@nitpicker/types": "0.4.1",
|
|
31
|
+
"ansi-colors": "4.1.3",
|
|
32
|
+
"cosmiconfig": "9.0.0",
|
|
33
|
+
"jsdom": "28.1.0"
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "32b83ee38eba7dfd237adb1b41f69e049e8d4ceb"
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { discoverAnalyzePlugins } from './discover-analyze-plugins.js';
|
|
4
|
+
|
|
5
|
+
describe('discoverAnalyzePlugins', () => {
|
|
6
|
+
it('returns all standard analyze plugins', () => {
|
|
7
|
+
const plugins = discoverAnalyzePlugins();
|
|
8
|
+
expect(plugins.length).toBeGreaterThanOrEqual(6);
|
|
9
|
+
expect(plugins.map((p) => p.name)).toContain('@nitpicker/analyze-axe');
|
|
10
|
+
expect(plugins.map((p) => p.name)).toContain('@nitpicker/analyze-textlint');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns plugins with empty default settings', () => {
|
|
14
|
+
const plugins = discoverAnalyzePlugins();
|
|
15
|
+
for (const plugin of plugins) {
|
|
16
|
+
expect(plugin.module).toBe(plugin.name);
|
|
17
|
+
expect(plugin.configFilePath).toBe('');
|
|
18
|
+
expect(plugin.settings).toEqual({});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Plugin } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standard analyze plugin module names bundled with the Nitpicker CLI.
|
|
5
|
+
*
|
|
6
|
+
* These are treated as built-in plugins and always available without
|
|
7
|
+
* explicit configuration.
|
|
8
|
+
*/
|
|
9
|
+
const STANDARD_ANALYZE_PLUGINS = [
|
|
10
|
+
'@nitpicker/analyze-axe',
|
|
11
|
+
'@nitpicker/analyze-lighthouse',
|
|
12
|
+
'@nitpicker/analyze-main-contents',
|
|
13
|
+
'@nitpicker/analyze-markuplint',
|
|
14
|
+
'@nitpicker/analyze-search',
|
|
15
|
+
'@nitpicker/analyze-textlint',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the standard set of `@nitpicker/analyze-*` plugins
|
|
20
|
+
* as {@link Plugin} entries with default (empty) settings.
|
|
21
|
+
*
|
|
22
|
+
* This is used as a fallback when no configuration file is found,
|
|
23
|
+
* allowing `nitpicker analyze` to work out of the box without
|
|
24
|
+
* requiring a `.nitpickerrc` file.
|
|
25
|
+
*
|
|
26
|
+
* All analyze plugins are treated as standard packages bundled
|
|
27
|
+
* with the CLI, so no filesystem scanning is necessary.
|
|
28
|
+
* @returns Array of standard analyze plugins with empty settings.
|
|
29
|
+
*/
|
|
30
|
+
export function discoverAnalyzePlugins(): Plugin[] {
|
|
31
|
+
return STANDARD_ANALYZE_PLUGINS.map((moduleName) => ({
|
|
32
|
+
name: moduleName,
|
|
33
|
+
module: moduleName,
|
|
34
|
+
configFilePath: '',
|
|
35
|
+
settings: {},
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PluginFactory } from '../types.js';
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { definePlugin } from './define-plugin.js';
|
|
6
|
+
|
|
7
|
+
describe('definePlugin', () => {
|
|
8
|
+
it('returns the exact same function passed in', () => {
|
|
9
|
+
const factory: PluginFactory<{ lang: string }> = (options) => {
|
|
10
|
+
return {
|
|
11
|
+
headers: { score: `Score (${options.lang})` },
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const result = definePlugin(factory);
|
|
16
|
+
|
|
17
|
+
expect(result).toBe(factory);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('preserves type inference for sync factories', () => {
|
|
21
|
+
const factory = definePlugin((() => ({
|
|
22
|
+
headers: { found: 'Found' },
|
|
23
|
+
})) as PluginFactory<{ keywords: string[] }>);
|
|
24
|
+
|
|
25
|
+
expect(typeof factory).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('preserves label in the returned AnalyzePlugin', () => {
|
|
29
|
+
const factory = definePlugin((() => ({
|
|
30
|
+
label: 'テスト用プラグイン',
|
|
31
|
+
headers: { score: 'Score' },
|
|
32
|
+
})) as PluginFactory<Record<string, never>>);
|
|
33
|
+
|
|
34
|
+
const plugin = factory({} as never, '');
|
|
35
|
+
|
|
36
|
+
expect(plugin).toHaveProperty('label', 'テスト用プラグイン');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PluginFactory } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Identity function that provides full type inference for analyze plugin definitions.
|
|
5
|
+
*
|
|
6
|
+
* This function does **nothing at runtime** - it returns its argument unchanged.
|
|
7
|
+
* Its sole purpose is to give TypeScript enough context to infer the generic
|
|
8
|
+
* parameters `O` (options type) and `T` (column key union) from the plugin
|
|
9
|
+
* implementation, without requiring explicit type annotations at the call site.
|
|
10
|
+
*
|
|
11
|
+
* ## Why an identity function?
|
|
12
|
+
*
|
|
13
|
+
* Without this wrapper, plugin authors would need to manually annotate the
|
|
14
|
+
* `PluginFactory` type with both generic parameters:
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* // Without definePlugin - verbose and error-prone
|
|
18
|
+
* const factory: PluginFactory<{ lang: string }, 'score' | 'details'> = (options) => { ... };
|
|
19
|
+
* export default factory;
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* With `definePlugin`, TypeScript infers everything from the function body:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* // With definePlugin - concise and type-safe
|
|
26
|
+
* export default definePlugin(async (options: { lang: string }) => {
|
|
27
|
+
* return {
|
|
28
|
+
* label: 'Custom Analysis',
|
|
29
|
+
* headers: { score: 'Score', details: 'Details' },
|
|
30
|
+
* async eachPage({ window }) {
|
|
31
|
+
* return { page: { score: { value: 100 }, details: { value: 'OK' } } };
|
|
32
|
+
* },
|
|
33
|
+
* };
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* This pattern is sometimes called a "satisfies helper" or "builder pattern"
|
|
38
|
+
* and is common in TypeScript libraries that need generic inference from
|
|
39
|
+
* function arguments (cf. Zod's `z.object()`, tRPC's `router()`).
|
|
40
|
+
* @template O - Shape of the plugin's settings/options from the config file.
|
|
41
|
+
* @template T - String literal union of column keys contributed by this plugin.
|
|
42
|
+
* @param factory - The plugin factory function to pass through.
|
|
43
|
+
* @returns The same function, unchanged.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { definePlugin } from '@nitpicker/core';
|
|
47
|
+
*
|
|
48
|
+
* type Options = { keywords: string[] };
|
|
49
|
+
*
|
|
50
|
+
* export default definePlugin(async (options: Options) => {
|
|
51
|
+
* return {
|
|
52
|
+
* label: 'キーワード検索',
|
|
53
|
+
* headers: { found: 'Keywords Found', count: 'Match Count' },
|
|
54
|
+
* async eachPage({ html }) {
|
|
55
|
+
* const matches = options.keywords.filter(k => html.includes(k));
|
|
56
|
+
* return {
|
|
57
|
+
* page: {
|
|
58
|
+
* found: { value: matches.join(', ') },
|
|
59
|
+
* count: { value: matches.length },
|
|
60
|
+
* },
|
|
61
|
+
* };
|
|
62
|
+
* },
|
|
63
|
+
* };
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
* @see {@link ../types.ts!PluginFactory} for the function signature being wrapped
|
|
67
|
+
* @see {@link ../types.ts!AnalyzePlugin} for the returned plugin interface
|
|
68
|
+
*/
|
|
69
|
+
export function definePlugin<O, T extends string = string>(
|
|
70
|
+
factory: PluginFactory<O, T>,
|
|
71
|
+
): PluginFactory<O, T> {
|
|
72
|
+
return factory;
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { definePlugin } from './define-plugin.js';
|