@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitpicker/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Plugin-based page analysis engine for Nitpicker",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib"
|
|
17
|
+
],
|
|
15
18
|
"type": "module",
|
|
16
19
|
"exports": {
|
|
17
20
|
".": {
|
|
@@ -26,11 +29,11 @@
|
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"@d-zero/dealer": "1.6.3",
|
|
28
31
|
"@d-zero/shared": "0.20.0",
|
|
29
|
-
"@nitpicker/crawler": "0.4.
|
|
30
|
-
"@nitpicker/types": "0.4.
|
|
32
|
+
"@nitpicker/crawler": "0.4.3",
|
|
33
|
+
"@nitpicker/types": "0.4.3",
|
|
31
34
|
"ansi-colors": "4.1.3",
|
|
32
35
|
"cosmiconfig": "9.0.0",
|
|
33
36
|
"jsdom": "28.1.0"
|
|
34
37
|
},
|
|
35
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "0f4ca55751be2f83dd5b6622c3502503fc7dfb41"
|
|
36
39
|
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
# Change Log
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
-
|
|
6
|
-
## [0.4.1](https://github.com/d-zero-dev/nitpicker/compare/v0.4.0...v0.4.1) (2026-02-27)
|
|
7
|
-
|
|
8
|
-
**Note:** Version bump only for package @nitpicker/core
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|
package/src/hooks/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { definePlugin } from './define-plugin.js';
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import type { AnalyzePlugin, Plugin } from './types.js';
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
-
|
|
5
|
-
vi.mock('./import-modules.js', async (importOriginal) => {
|
|
6
|
-
const original = await importOriginal<typeof import('./import-modules.js')>(); // eslint-disable-line @typescript-eslint/consistent-type-imports
|
|
7
|
-
return { importModules: original.importModules };
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
describe('importModules', () => {
|
|
11
|
-
it('imports and initializes a single plugin', async () => {
|
|
12
|
-
const mockPlugin: AnalyzePlugin = {
|
|
13
|
-
label: 'Test Plugin',
|
|
14
|
-
headers: { col: 'Column' },
|
|
15
|
-
};
|
|
16
|
-
const factory = vi.fn().mockReturnValue(mockPlugin);
|
|
17
|
-
|
|
18
|
-
vi.doMock('@nitpicker/analyze-fake-a', () => ({ default: factory }));
|
|
19
|
-
|
|
20
|
-
const { importModules } = await import('./import-modules.js');
|
|
21
|
-
|
|
22
|
-
const plugins: Plugin[] = [
|
|
23
|
-
{
|
|
24
|
-
name: '@nitpicker/analyze-fake-a',
|
|
25
|
-
module: '@nitpicker/analyze-fake-a',
|
|
26
|
-
configFilePath: '/path/to/config',
|
|
27
|
-
settings: { lang: 'ja' },
|
|
28
|
-
},
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const result = await importModules(plugins);
|
|
32
|
-
|
|
33
|
-
expect(result).toHaveLength(1);
|
|
34
|
-
expect(result[0]).toBe(mockPlugin);
|
|
35
|
-
expect(factory).toHaveBeenCalledWith({ lang: 'ja' });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('imports multiple plugins in parallel', async () => {
|
|
39
|
-
const pluginA: AnalyzePlugin = { label: 'A' };
|
|
40
|
-
const pluginB: AnalyzePlugin = { label: 'B' };
|
|
41
|
-
const factoryA = vi.fn().mockReturnValue(pluginA);
|
|
42
|
-
const factoryB = vi.fn().mockReturnValue(pluginB);
|
|
43
|
-
|
|
44
|
-
vi.doMock('@nitpicker/analyze-fake-b1', () => ({ default: factoryA }));
|
|
45
|
-
vi.doMock('@nitpicker/analyze-fake-b2', () => ({ default: factoryB }));
|
|
46
|
-
|
|
47
|
-
const { importModules } = await import('./import-modules.js');
|
|
48
|
-
|
|
49
|
-
const plugins: Plugin[] = [
|
|
50
|
-
{
|
|
51
|
-
name: '@nitpicker/analyze-fake-b1',
|
|
52
|
-
module: '@nitpicker/analyze-fake-b1',
|
|
53
|
-
configFilePath: '',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: '@nitpicker/analyze-fake-b2',
|
|
57
|
-
module: '@nitpicker/analyze-fake-b2',
|
|
58
|
-
configFilePath: '',
|
|
59
|
-
},
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
const result = await importModules(plugins);
|
|
63
|
-
|
|
64
|
-
expect(result).toHaveLength(2);
|
|
65
|
-
expect(result[0]).toBe(pluginA);
|
|
66
|
-
expect(result[1]).toBe(pluginB);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('passes undefined settings when not configured', async () => {
|
|
70
|
-
const factory = vi.fn().mockReturnValue({});
|
|
71
|
-
|
|
72
|
-
vi.doMock('@nitpicker/analyze-fake-c', () => ({ default: factory }));
|
|
73
|
-
|
|
74
|
-
const { importModules } = await import('./import-modules.js');
|
|
75
|
-
|
|
76
|
-
const plugins: Plugin[] = [
|
|
77
|
-
{
|
|
78
|
-
name: '@nitpicker/analyze-fake-c',
|
|
79
|
-
module: '@nitpicker/analyze-fake-c',
|
|
80
|
-
configFilePath: '',
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
await importModules(plugins);
|
|
85
|
-
|
|
86
|
-
expect(factory).toHaveBeenCalledWith(undefined);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('handles async factory functions', async () => {
|
|
90
|
-
const pluginResult: AnalyzePlugin = {
|
|
91
|
-
label: 'Async Plugin',
|
|
92
|
-
headers: { score: 'Score' },
|
|
93
|
-
};
|
|
94
|
-
const factory = vi.fn().mockResolvedValue(pluginResult);
|
|
95
|
-
|
|
96
|
-
vi.doMock('@nitpicker/analyze-fake-d', () => ({ default: factory }));
|
|
97
|
-
|
|
98
|
-
const { importModules } = await import('./import-modules.js');
|
|
99
|
-
|
|
100
|
-
const plugins: Plugin[] = [
|
|
101
|
-
{
|
|
102
|
-
name: '@nitpicker/analyze-fake-d',
|
|
103
|
-
module: '@nitpicker/analyze-fake-d',
|
|
104
|
-
configFilePath: '',
|
|
105
|
-
settings: {},
|
|
106
|
-
},
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
const result = await importModules(plugins);
|
|
110
|
-
|
|
111
|
-
expect(result[0]).toBe(pluginResult);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('returns empty array for empty plugin list', async () => {
|
|
115
|
-
const { importModules } = await import('./import-modules.js');
|
|
116
|
-
const result = await importModules([]);
|
|
117
|
-
|
|
118
|
-
expect(result).toEqual([]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('rejects plugin modules without the @nitpicker/analyze- prefix', async () => {
|
|
122
|
-
const { importModules } = await import('./import-modules.js');
|
|
123
|
-
|
|
124
|
-
const plugins: Plugin[] = [
|
|
125
|
-
{
|
|
126
|
-
name: 'malicious-plugin',
|
|
127
|
-
module: 'malicious-plugin',
|
|
128
|
-
configFilePath: '',
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
await expect(importModules(plugins)).rejects.toThrow(
|
|
133
|
-
'Unauthorized plugin module: "malicious-plugin". Plugin modules must start with "@nitpicker/analyze-".',
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('rejects plugin modules with a similar but incorrect prefix', async () => {
|
|
138
|
-
const { importModules } = await import('./import-modules.js');
|
|
139
|
-
|
|
140
|
-
const plugins: Plugin[] = [
|
|
141
|
-
{
|
|
142
|
-
name: 'tricky-plugin',
|
|
143
|
-
module: '@nitpicker/analyze',
|
|
144
|
-
configFilePath: '',
|
|
145
|
-
},
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
await expect(importModules(plugins)).rejects.toThrow('Unauthorized plugin module');
|
|
149
|
-
});
|
|
150
|
-
});
|
package/src/import-modules.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { AnalyzePlugin, Plugin } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Allowed prefix for analyze plugin module names.
|
|
5
|
-
* Only modules starting with this prefix are permitted to be dynamically imported.
|
|
6
|
-
*/
|
|
7
|
-
const ALLOWED_PREFIX = '@nitpicker/analyze-';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Dynamically imports and initializes all analyze plugin modules.
|
|
11
|
-
*
|
|
12
|
-
* For each plugin in the list:
|
|
13
|
-
* 1. Validates that `plugin.module` starts with `@nitpicker/analyze-`
|
|
14
|
-
* 2. Calls `import(plugin.module)` to load the npm package
|
|
15
|
-
* 3. Invokes the module's default export (a `PluginFactory` factory)
|
|
16
|
-
* with the plugin's `settings`
|
|
17
|
-
* 4. Returns the resulting `AnalyzePlugin` instance
|
|
18
|
-
*
|
|
19
|
-
* All plugins are loaded in parallel via `Promise.all` for performance.
|
|
20
|
-
*
|
|
21
|
-
* This function is called both in the main thread (for `headers` and `eachUrl`)
|
|
22
|
-
* and inside each Worker thread (for `eachPage`). The Worker-side call is
|
|
23
|
-
* necessary because plugin modules may not be transferable across threads.
|
|
24
|
-
* @param plugins - Array of plugin definitions from the resolved config.
|
|
25
|
-
* @returns Array of initialized `AnalyzePlugin` instances, in the same order.
|
|
26
|
-
* @throws {Error} If any plugin module name does not start with `@nitpicker/analyze-`.
|
|
27
|
-
* @see {@link ./types.ts} for the `PluginFactory` factory function signature
|
|
28
|
-
* @see {@link ./page-analysis-worker.ts} for Worker-side usage
|
|
29
|
-
* @see {@link ./nitpicker.ts!Nitpicker.analyze} for main-thread usage
|
|
30
|
-
*/
|
|
31
|
-
export async function importModules(plugins: Plugin[]) {
|
|
32
|
-
const analyzeMods = await Promise.all(
|
|
33
|
-
plugins.map(async (plugin) => {
|
|
34
|
-
if (!plugin.module.startsWith(ALLOWED_PREFIX)) {
|
|
35
|
-
throw new Error(
|
|
36
|
-
`Unauthorized plugin module: "${plugin.module}". Plugin modules must start with "${ALLOWED_PREFIX}".`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
const mod = await import(plugin.module);
|
|
40
|
-
const factory = mod.default;
|
|
41
|
-
return factory(plugin.settings) as AnalyzePlugin;
|
|
42
|
-
}),
|
|
43
|
-
);
|
|
44
|
-
return analyzeMods;
|
|
45
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { cosmiconfig } from 'cosmiconfig';
|
|
2
|
-
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
vi.mock('cosmiconfig', () => ({
|
|
5
|
-
cosmiconfig: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock('./discover-analyze-plugins.js', () => ({
|
|
9
|
-
discoverAnalyzePlugins: vi.fn(() => []),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
import { discoverAnalyzePlugins } from './discover-analyze-plugins.js';
|
|
13
|
-
import { loadPluginSettings } from './load-plugin-settings.js';
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
vi.mocked(discoverAnalyzePlugins).mockClear();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Set up the cosmiconfig mock to return a specific search result.
|
|
21
|
-
* @param result - The mock cosmiconfig search result, or null for no config found.
|
|
22
|
-
*/
|
|
23
|
-
function mockCosmiconfig(
|
|
24
|
-
result: { config: unknown; filepath: string; isEmpty?: boolean } | null,
|
|
25
|
-
) {
|
|
26
|
-
vi.mocked(cosmiconfig).mockReturnValue({
|
|
27
|
-
search: vi.fn().mockResolvedValue(result),
|
|
28
|
-
} as never);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('loadPluginSettings', () => {
|
|
32
|
-
it('returns empty analyze array when no config found', async () => {
|
|
33
|
-
mockCosmiconfig(null);
|
|
34
|
-
const config = await loadPluginSettings();
|
|
35
|
-
expect(config.analyze).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns empty analyze array when config is empty', async () => {
|
|
39
|
-
mockCosmiconfig({ config: null, filepath: '/path/.nitpickerrc.json', isEmpty: true });
|
|
40
|
-
const config = await loadPluginSettings();
|
|
41
|
-
expect(config.analyze).toEqual([]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('returns empty analyze array when config has no plugins', async () => {
|
|
45
|
-
mockCosmiconfig({ config: {}, filepath: '/path/.nitpickerrc.json' });
|
|
46
|
-
const config = await loadPluginSettings();
|
|
47
|
-
expect(config.analyze).toEqual([]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('converts plugins.analyze object to Plugin array', async () => {
|
|
51
|
-
mockCosmiconfig({
|
|
52
|
-
config: {
|
|
53
|
-
plugins: {
|
|
54
|
-
analyze: {
|
|
55
|
-
'@nitpicker/analyze-axe': { lang: 'ja' },
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
filepath: '/path/.nitpickerrc.json',
|
|
60
|
-
});
|
|
61
|
-
const config = await loadPluginSettings();
|
|
62
|
-
expect(config.analyze).toHaveLength(1);
|
|
63
|
-
expect(config.analyze[0]).toEqual({
|
|
64
|
-
name: '@nitpicker/analyze-axe',
|
|
65
|
-
module: '@nitpicker/analyze-axe',
|
|
66
|
-
configFilePath: '/path/.nitpickerrc.json',
|
|
67
|
-
settings: { lang: 'ja' },
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('normalizes boolean true settings to empty object', async () => {
|
|
72
|
-
mockCosmiconfig({
|
|
73
|
-
config: {
|
|
74
|
-
plugins: {
|
|
75
|
-
analyze: {
|
|
76
|
-
'@nitpicker/analyze-markuplint': true,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
filepath: '/path/.nitpickerrc.json',
|
|
81
|
-
});
|
|
82
|
-
const config = await loadPluginSettings();
|
|
83
|
-
expect(config.analyze[0].settings).toEqual({});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('skips falsy plugin entries', async () => {
|
|
87
|
-
mockCosmiconfig({
|
|
88
|
-
config: {
|
|
89
|
-
plugins: {
|
|
90
|
-
analyze: {
|
|
91
|
-
'@nitpicker/analyze-axe': { lang: 'ja' },
|
|
92
|
-
'@nitpicker/analyze-disabled': false,
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
filepath: '/path/.nitpickerrc.json',
|
|
97
|
-
});
|
|
98
|
-
const config = await loadPluginSettings();
|
|
99
|
-
expect(config.analyze).toHaveLength(1);
|
|
100
|
-
expect(config.analyze[0].name).toBe('@nitpicker/analyze-axe');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('merges defaultConfig analyze plugins before discovered plugins', async () => {
|
|
104
|
-
mockCosmiconfig({
|
|
105
|
-
config: {
|
|
106
|
-
plugins: {
|
|
107
|
-
analyze: {
|
|
108
|
-
'@nitpicker/analyze-axe': true,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
filepath: '/path/.nitpickerrc.json',
|
|
113
|
-
});
|
|
114
|
-
const defaultPlugin = {
|
|
115
|
-
name: 'default-plugin',
|
|
116
|
-
module: 'default-plugin',
|
|
117
|
-
configFilePath: '/default',
|
|
118
|
-
settings: {},
|
|
119
|
-
};
|
|
120
|
-
const config = await loadPluginSettings({ analyze: [defaultPlugin] });
|
|
121
|
-
expect(config.analyze).toHaveLength(2);
|
|
122
|
-
expect(config.analyze[0].name).toBe('default-plugin');
|
|
123
|
-
expect(config.analyze[1].name).toBe('@nitpicker/analyze-axe');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('spreads defaultConfig keys when no config found', async () => {
|
|
127
|
-
mockCosmiconfig(null);
|
|
128
|
-
const config = await loadPluginSettings({ analyze: [] });
|
|
129
|
-
expect(config.analyze).toEqual([]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('falls back to discoverAnalyzePlugins when no config found and no defaults', async () => {
|
|
133
|
-
const discovered = [
|
|
134
|
-
{
|
|
135
|
-
name: '@nitpicker/analyze-axe',
|
|
136
|
-
module: '@nitpicker/analyze-axe',
|
|
137
|
-
configFilePath: '',
|
|
138
|
-
settings: {},
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
vi.mocked(discoverAnalyzePlugins).mockReturnValue(discovered);
|
|
142
|
-
mockCosmiconfig(null);
|
|
143
|
-
const config = await loadPluginSettings();
|
|
144
|
-
expect(config.analyze).toEqual(discovered);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('falls back to discoverAnalyzePlugins when config is empty', async () => {
|
|
148
|
-
const discovered = [
|
|
149
|
-
{
|
|
150
|
-
name: '@nitpicker/analyze-markuplint',
|
|
151
|
-
module: '@nitpicker/analyze-markuplint',
|
|
152
|
-
configFilePath: '',
|
|
153
|
-
settings: {},
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
vi.mocked(discoverAnalyzePlugins).mockReturnValue(discovered);
|
|
157
|
-
mockCosmiconfig({ config: null, filepath: '/path/.nitpickerrc.json', isEmpty: true });
|
|
158
|
-
const config = await loadPluginSettings();
|
|
159
|
-
expect(config.analyze).toEqual(discovered);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('falls back to discoverAnalyzePlugins when config has no plugins section', async () => {
|
|
163
|
-
const discovered = [
|
|
164
|
-
{
|
|
165
|
-
name: '@nitpicker/analyze-axe',
|
|
166
|
-
module: '@nitpicker/analyze-axe',
|
|
167
|
-
configFilePath: '',
|
|
168
|
-
settings: {},
|
|
169
|
-
},
|
|
170
|
-
];
|
|
171
|
-
vi.mocked(discoverAnalyzePlugins).mockReturnValue(discovered);
|
|
172
|
-
mockCosmiconfig({ config: {}, filepath: '/path/.nitpickerrc.json' });
|
|
173
|
-
const config = await loadPluginSettings();
|
|
174
|
-
expect(config.analyze).toEqual(discovered);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('does not fall back when config has plugins', async () => {
|
|
178
|
-
mockCosmiconfig({
|
|
179
|
-
config: {
|
|
180
|
-
plugins: {
|
|
181
|
-
analyze: {
|
|
182
|
-
'@nitpicker/analyze-axe': { lang: 'ja' },
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
filepath: '/path/.nitpickerrc.json',
|
|
187
|
-
});
|
|
188
|
-
const config = await loadPluginSettings();
|
|
189
|
-
expect(discoverAnalyzePlugins).not.toHaveBeenCalled();
|
|
190
|
-
expect(config.analyze).toHaveLength(1);
|
|
191
|
-
});
|
|
192
|
-
});
|