@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
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Config, Plugin } from './types.js';
|
|
2
|
+
import type { ConfigJSON } from '@nitpicker/types';
|
|
3
|
+
|
|
4
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
5
|
+
|
|
6
|
+
import { discoverAnalyzePlugins } from './discover-analyze-plugins.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The cosmiconfig module name used for config file discovery.
|
|
10
|
+
* Searches for: `.nitpickerrc`, `.nitpickerrc.json`, `.nitpickerrc.yaml`,
|
|
11
|
+
* `nitpicker.config.js`, `nitpicker.config.cjs`, or a `"nitpicker"` key
|
|
12
|
+
* in `package.json`.
|
|
13
|
+
*/
|
|
14
|
+
const MODULE_NAME = 'nitpicker';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads the analyze plugin configuration from the user's config file.
|
|
18
|
+
*
|
|
19
|
+
* Uses cosmiconfig to search the filesystem from `process.cwd()` upward
|
|
20
|
+
* for a Nitpicker configuration file. The external config format
|
|
21
|
+
* (`ConfigJSON` from `@nitpicker/types`) is normalized into the internal
|
|
22
|
+
* {@link Config} model:
|
|
23
|
+
*
|
|
24
|
+
* - `config.plugins.analyze` (object keyed by module name with settings)
|
|
25
|
+
* is converted to an ordered `Plugin[]` array
|
|
26
|
+
* - Boolean `true` settings are normalized to empty objects `{}`
|
|
27
|
+
* - The config file path is attached to each plugin for relative path resolution
|
|
28
|
+
* @param defaultConfig - Optional partial config to merge as defaults.
|
|
29
|
+
* Plugin lists are concatenated (defaults first, then discovered plugins).
|
|
30
|
+
* @returns Fully resolved {@link Config} with the `analyze` plugin list.
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* // .nitpickerrc.json
|
|
34
|
+
* // {
|
|
35
|
+
* // "plugins": {
|
|
36
|
+
* // "analyze": {
|
|
37
|
+
* // "@nitpicker/analyze-axe": { "lang": "ja" },
|
|
38
|
+
* // "@nitpicker/analyze-markuplint": true
|
|
39
|
+
* // }
|
|
40
|
+
* // }
|
|
41
|
+
* // }
|
|
42
|
+
*
|
|
43
|
+
* const config = await loadPluginSettings();
|
|
44
|
+
* // config.analyze = [
|
|
45
|
+
* // { name: '@nitpicker/analyze-axe', module: '@nitpicker/analyze-axe',
|
|
46
|
+
* // configFilePath: '/path/to/.nitpickerrc.json', settings: { lang: 'ja' } },
|
|
47
|
+
* // { name: '@nitpicker/analyze-markuplint', module: '@nitpicker/analyze-markuplint',
|
|
48
|
+
* // configFilePath: '/path/to/.nitpickerrc.json', settings: {} },
|
|
49
|
+
* // ]
|
|
50
|
+
* ```
|
|
51
|
+
* @see {@link ./types.ts!Config} for the output type
|
|
52
|
+
* @see {@link ./types.ts!Plugin} for individual plugin entries
|
|
53
|
+
*/
|
|
54
|
+
export async function loadPluginSettings(
|
|
55
|
+
defaultConfig: Partial<Config> = {},
|
|
56
|
+
): Promise<Config> {
|
|
57
|
+
const explorer = cosmiconfig(MODULE_NAME);
|
|
58
|
+
const result = await explorer.search();
|
|
59
|
+
if (!result) {
|
|
60
|
+
const defaultPlugins = defaultConfig.analyze || [];
|
|
61
|
+
return {
|
|
62
|
+
analyze: defaultPlugins.length > 0 ? defaultPlugins : discoverAnalyzePlugins(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const config = result.config as ConfigJSON;
|
|
66
|
+
const { isEmpty, filepath } = result;
|
|
67
|
+
if (!config || isEmpty) {
|
|
68
|
+
const defaultPlugins = defaultConfig.analyze || [];
|
|
69
|
+
return {
|
|
70
|
+
analyze: defaultPlugins.length > 0 ? defaultPlugins : discoverAnalyzePlugins(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const analyzePlugins: Plugin[] = [];
|
|
75
|
+
|
|
76
|
+
if (config.plugins && config.plugins.analyze) {
|
|
77
|
+
const moduleNames = Object.keys(config.plugins.analyze);
|
|
78
|
+
|
|
79
|
+
for (const name of moduleNames) {
|
|
80
|
+
if (!config.plugins.analyze[name]) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const settings =
|
|
84
|
+
config.plugins.analyze[name] === true ? {} : config.plugins.analyze[name];
|
|
85
|
+
analyzePlugins.push({
|
|
86
|
+
name,
|
|
87
|
+
module: name,
|
|
88
|
+
configFilePath: filepath,
|
|
89
|
+
settings,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mergedPlugins = [...(defaultConfig.analyze || []), ...analyzePlugins];
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
analyze: mergedPlugins.length > 0 ? mergedPlugins : discoverAnalyzePlugins(),
|
|
98
|
+
};
|
|
99
|
+
}
|