@nitpicker/core 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +191 -0
  3. package/README.md +13 -0
  4. package/lib/discover-analyze-plugins.d.ts +14 -0
  5. package/lib/discover-analyze-plugins.js +34 -0
  6. package/lib/find-nitpicker-modules-dir.d.ts +12 -0
  7. package/lib/find-nitpicker-modules-dir.js +23 -0
  8. package/lib/hooks/actions.d.ts +9 -0
  9. package/lib/hooks/actions.js +9 -0
  10. package/lib/hooks/child-process.d.ts +1 -0
  11. package/lib/hooks/child-process.js +34 -0
  12. package/lib/hooks/define-plugin.d.ts +68 -0
  13. package/lib/hooks/define-plugin.js +69 -0
  14. package/lib/hooks/index.d.ts +1 -0
  15. package/lib/hooks/index.js +1 -0
  16. package/lib/hooks/runner.d.ts +10 -0
  17. package/lib/hooks/runner.js +32 -0
  18. package/lib/import-modules.d.ts +24 -0
  19. package/lib/import-modules.js +38 -0
  20. package/lib/index.d.ts +5 -0
  21. package/lib/index.js +5 -0
  22. package/lib/load-plugin-settings.d.ts +40 -0
  23. package/lib/load-plugin-settings.js +85 -0
  24. package/lib/nitpicker.d.ts +127 -0
  25. package/lib/nitpicker.js +338 -0
  26. package/lib/page-analysis-worker.d.ts +48 -0
  27. package/lib/page-analysis-worker.js +98 -0
  28. package/lib/read-plugin-labels.d.ts +15 -0
  29. package/lib/read-plugin-labels.js +30 -0
  30. package/lib/table.d.ts +75 -0
  31. package/lib/table.js +132 -0
  32. package/lib/types.d.ts +264 -0
  33. package/lib/types.js +1 -0
  34. package/lib/url-event-bus.d.ts +32 -0
  35. package/lib/url-event-bus.js +20 -0
  36. package/lib/utils.d.ts +36 -0
  37. package/lib/utils.js +43 -0
  38. package/lib/worker/run-in-worker.d.ts +51 -0
  39. package/lib/worker/run-in-worker.js +120 -0
  40. package/lib/worker/runner.d.ts +25 -0
  41. package/lib/worker/runner.js +31 -0
  42. package/lib/worker/types.d.ts +23 -0
  43. package/lib/worker/types.js +1 -0
  44. package/lib/worker/worker.d.ts +27 -0
  45. package/lib/worker/worker.js +53 -0
  46. package/package.json +36 -0
  47. package/src/discover-analyze-plugins.spec.ts +21 -0
  48. package/src/discover-analyze-plugins.ts +37 -0
  49. package/src/hooks/define-plugin.spec.ts +38 -0
  50. package/src/hooks/define-plugin.ts +73 -0
  51. package/src/hooks/index.ts +1 -0
  52. package/src/import-modules.spec.ts +150 -0
  53. package/src/import-modules.ts +45 -0
  54. package/src/index.ts +5 -0
  55. package/src/load-plugin-settings.spec.ts +192 -0
  56. package/src/load-plugin-settings.ts +99 -0
  57. package/src/nitpicker.ts +418 -0
  58. package/src/page-analysis-worker.spec.ts +287 -0
  59. package/src/page-analysis-worker.ts +131 -0
  60. package/src/read-plugin-labels.spec.ts +151 -0
  61. package/src/read-plugin-labels.ts +37 -0
  62. package/src/table.spec.ts +83 -0
  63. package/src/table.ts +149 -0
  64. package/src/types.ts +289 -0
  65. package/src/url-event-bus.spec.ts +28 -0
  66. package/src/url-event-bus.ts +33 -0
  67. package/src/worker/run-in-worker.ts +155 -0
  68. package/src/worker/runner.ts +38 -0
  69. package/src/worker/types.ts +25 -0
  70. package/src/worker/worker.ts +64 -0
  71. package/tsconfig.json +11 -0
  72. package/tsconfig.tsbuildinfo +1 -0
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';