@poetora/cli 0.0.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/.prettierignore +2 -0
- package/README.md +3 -0
- package/__test__/brokenLinks.test.ts +93 -0
- package/__test__/checkPort.test.ts +92 -0
- package/__test__/openApiCheck.test.ts +127 -0
- package/__test__/update.test.ts +108 -0
- package/__test__/utils.ts +20 -0
- package/bin/accessibility.d.ts +44 -0
- package/bin/accessibility.js +110 -0
- package/bin/accessibilityCheck.d.ts +2 -0
- package/bin/accessibilityCheck.js +70 -0
- package/bin/cli.d.ts +11 -0
- package/bin/cli.js +201 -0
- package/bin/constants.d.ts +2 -0
- package/bin/constants.js +3 -0
- package/bin/helpers.d.ts +17 -0
- package/bin/helpers.js +104 -0
- package/bin/index.d.ts +4 -0
- package/bin/index.js +92 -0
- package/bin/init.d.ts +1 -0
- package/bin/init.js +73 -0
- package/bin/mdxAccessibility.d.ts +13 -0
- package/bin/mdxAccessibility.js +102 -0
- package/bin/mdxLinter.d.ts +2 -0
- package/bin/mdxLinter.js +45 -0
- package/bin/start.d.ts +2 -0
- package/bin/start.js +4 -0
- package/bin/update.d.ts +3 -0
- package/bin/update.js +32 -0
- package/package.json +83 -0
- package/src/accessibility.ts +180 -0
- package/src/accessibilityCheck.tsx +145 -0
- package/src/cli.tsx +302 -0
- package/src/constants.ts +4 -0
- package/src/helpers.tsx +131 -0
- package/src/index.ts +110 -0
- package/src/init.tsx +93 -0
- package/src/mdxAccessibility.ts +133 -0
- package/src/mdxLinter.tsx +88 -0
- package/src/start.ts +6 -0
- package/src/update.tsx +37 -0
- package/tsconfig.build.json +16 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +8 -0
package/.prettierignore
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getBrokenInternalLinks, MdxPath } from '@poetora/link-rot';
|
|
2
|
+
import * as previewing from '@poetora/previewing';
|
|
3
|
+
import { mockProcessExit } from 'vitest-mock-process';
|
|
4
|
+
|
|
5
|
+
import { runCommand } from './utils';
|
|
6
|
+
|
|
7
|
+
vi.mock('@poetora/link-rot', async () => {
|
|
8
|
+
const actual = await import('@poetora/link-rot');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
getBrokenInternalLinks: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const addLogSpy = vi.spyOn(previewing, 'addLog');
|
|
16
|
+
const clearLogsSpy = vi.spyOn(previewing, 'clearLogs');
|
|
17
|
+
const processExitMock = mockProcessExit();
|
|
18
|
+
|
|
19
|
+
describe('brokenLinks', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('success with no broken links', async () => {
|
|
29
|
+
vi.mocked(getBrokenInternalLinks).mockResolvedValueOnce([]);
|
|
30
|
+
|
|
31
|
+
await runCommand('broken-links');
|
|
32
|
+
|
|
33
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
props: { message: 'checking for broken links...' },
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
props: { message: 'no broken links found' },
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('fails with broken links', async () => {
|
|
47
|
+
vi.mocked(getBrokenInternalLinks).mockResolvedValueOnce([
|
|
48
|
+
{
|
|
49
|
+
relativeDir: '.',
|
|
50
|
+
filename: 'introduction.mdx',
|
|
51
|
+
originalPath: '/api/invalid-path',
|
|
52
|
+
pathType: 'internal',
|
|
53
|
+
} as MdxPath,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
await runCommand('broken-links');
|
|
57
|
+
|
|
58
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
props: { message: 'checking for broken links...' },
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
expect(clearLogsSpy).toHaveBeenCalled();
|
|
64
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
props: {
|
|
67
|
+
brokenLinksByFile: {
|
|
68
|
+
'introduction.mdx': ['/api/invalid-path'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('fails when checking throws error', async () => {
|
|
77
|
+
vi.mocked(getBrokenInternalLinks).mockRejectedValueOnce(new Error('some error'));
|
|
78
|
+
|
|
79
|
+
await runCommand('broken-links');
|
|
80
|
+
|
|
81
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
82
|
+
expect.objectContaining({
|
|
83
|
+
props: { message: 'checking for broken links...' },
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
props: { message: 'some error' },
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as previewing from '@poetora/previewing';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { mockProcessExit } from 'vitest-mock-process';
|
|
4
|
+
|
|
5
|
+
import { runCommand } from './utils';
|
|
6
|
+
|
|
7
|
+
vi.mock('@poetora/previewing', async () => {
|
|
8
|
+
const originalModule =
|
|
9
|
+
await vi.importActual<typeof import('@poetora/previewing')>('@poetora/previewing');
|
|
10
|
+
return {
|
|
11
|
+
...originalModule,
|
|
12
|
+
dev: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('readline/promises', () => {
|
|
17
|
+
return {
|
|
18
|
+
createInterface: () => {
|
|
19
|
+
return {
|
|
20
|
+
close: () => undefined,
|
|
21
|
+
question: (question: string) => {
|
|
22
|
+
console.log(question);
|
|
23
|
+
return 'y';
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const allowedPorts = [3000, 5002];
|
|
31
|
+
|
|
32
|
+
vi.mock('detect-port', () => ({
|
|
33
|
+
default: (port: number) => (allowedPorts.includes(port) ? port : port + 1),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const devSpy = vi.spyOn(previewing, 'dev');
|
|
37
|
+
const addLogSpy = vi.spyOn(previewing, 'addLog');
|
|
38
|
+
const processExitMock = mockProcessExit();
|
|
39
|
+
|
|
40
|
+
describe('checkPort', () => {
|
|
41
|
+
let originalArgv: string[];
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Remove all cached modules, otherwise the same results are shown in subsequent tests.
|
|
45
|
+
vi.resetModules();
|
|
46
|
+
|
|
47
|
+
// mock inquirer prompt
|
|
48
|
+
vi.spyOn(inquirer, 'prompt').mockResolvedValue({ action: 'continue' });
|
|
49
|
+
|
|
50
|
+
// Keep track of original process arguments.
|
|
51
|
+
originalArgv = process.argv;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.resetAllMocks();
|
|
56
|
+
|
|
57
|
+
// Set process arguments back to the original value.
|
|
58
|
+
process.argv = originalArgv;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should run dev command', async () => {
|
|
62
|
+
await runCommand('dev');
|
|
63
|
+
|
|
64
|
+
expect(devSpy).toHaveBeenCalledWith(expect.objectContaining({ port: 3000 }));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('port 5000 and 5001 should be taken and 5002 should be accepted by the user and available.', async () => {
|
|
68
|
+
await runCommand('dev', '--port=5000');
|
|
69
|
+
|
|
70
|
+
expect(addLogSpy).toHaveBeenCalledTimes(2);
|
|
71
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
props: { message: 'port 5000 is already in use. trying 5001 instead' },
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
props: { message: 'port 5001 is already in use. trying 5002 instead' },
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('fails after the 10th used port', async () => {
|
|
84
|
+
await runCommand('dev', '--port=8000');
|
|
85
|
+
|
|
86
|
+
expect(addLogSpy).toHaveBeenCalledTimes(10);
|
|
87
|
+
expect(addLogSpy).toHaveBeenLastCalledWith(
|
|
88
|
+
expect.objectContaining({ props: { message: 'no available port found' } })
|
|
89
|
+
);
|
|
90
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as previewing from '@poetora/previewing';
|
|
2
|
+
import { getOpenApiDocumentFromUrl, isAllowedLocalSchemaUrl, validate } from '@poetora/shared';
|
|
3
|
+
import { mockProcessExit } from 'vitest-mock-process';
|
|
4
|
+
|
|
5
|
+
import { readLocalOpenApiFile } from '../src/helpers';
|
|
6
|
+
import { mockValidOpenApiDocument, runCommand } from './utils';
|
|
7
|
+
|
|
8
|
+
vi.mock('@poetora/shared', () => ({
|
|
9
|
+
getOpenApiDocumentFromUrl: vi.fn(),
|
|
10
|
+
isAllowedLocalSchemaUrl: vi.fn(),
|
|
11
|
+
validate: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('../src/helpers', async () => {
|
|
15
|
+
const originalModule = await import('../src/helpers');
|
|
16
|
+
return {
|
|
17
|
+
...originalModule,
|
|
18
|
+
readLocalOpenApiFile: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const addLogSpy = vi.spyOn(previewing, 'addLog');
|
|
23
|
+
const processExitMock = mockProcessExit();
|
|
24
|
+
|
|
25
|
+
describe('openApiCheck', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('valid openApi file from url', async () => {
|
|
35
|
+
vi.mocked(isAllowedLocalSchemaUrl).mockReturnValueOnce(true);
|
|
36
|
+
vi.mocked(getOpenApiDocumentFromUrl).mockResolvedValueOnce(mockValidOpenApiDocument);
|
|
37
|
+
|
|
38
|
+
await runCommand('openapi-check', 'https://petstore3.swagger.io/api/v3/openapi.json');
|
|
39
|
+
|
|
40
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
props: { message: 'OpenAPI definition is valid.' },
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('invalid openApi file from url', async () => {
|
|
49
|
+
vi.mocked(isAllowedLocalSchemaUrl).mockReturnValueOnce(true);
|
|
50
|
+
vi.mocked(getOpenApiDocumentFromUrl).mockRejectedValueOnce(
|
|
51
|
+
new Error('Could not parse OpenAPI document.')
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await runCommand('openapi-check', 'https://petstore3.swagger.io/api/v3/openapi.json');
|
|
55
|
+
|
|
56
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
props: { message: 'Could not parse OpenAPI document.' },
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('valid openApi file from localhost', async () => {
|
|
65
|
+
vi.mocked(isAllowedLocalSchemaUrl).mockReturnValueOnce(true);
|
|
66
|
+
vi.mocked(getOpenApiDocumentFromUrl).mockResolvedValueOnce(mockValidOpenApiDocument);
|
|
67
|
+
|
|
68
|
+
await runCommand('openapi-check', 'http://localhost:3000/openapi.json');
|
|
69
|
+
|
|
70
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
props: { message: 'OpenAPI definition is valid.' },
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('invalid openApi file from localhost', async () => {
|
|
79
|
+
vi.mocked(isAllowedLocalSchemaUrl).mockReturnValueOnce(false);
|
|
80
|
+
vi.mocked(readLocalOpenApiFile).mockResolvedValueOnce(undefined);
|
|
81
|
+
|
|
82
|
+
await runCommand('openapi-check', 'http://localhost:3000/openapi.json');
|
|
83
|
+
|
|
84
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
props: {
|
|
87
|
+
message:
|
|
88
|
+
'failed to parse OpenAPI spec: could not parse file correctly, please check for any syntax errors.',
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('valid openApi file from local file', async () => {
|
|
96
|
+
vi.mocked(isAllowedLocalSchemaUrl).mockReturnValueOnce(false);
|
|
97
|
+
vi.mocked(readLocalOpenApiFile).mockResolvedValueOnce(mockValidOpenApiDocument);
|
|
98
|
+
vi.mocked(validate).mockResolvedValueOnce({
|
|
99
|
+
valid: true,
|
|
100
|
+
errors: [],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await runCommand('openapi-check', 'test/openapi.yaml');
|
|
104
|
+
|
|
105
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
106
|
+
expect.objectContaining({
|
|
107
|
+
props: { message: 'OpenAPI definition is valid.' },
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
expect(processExitMock).toHaveBeenCalledWith(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('invalid openApi file from local file', async () => {
|
|
114
|
+
vi.mocked(isAllowedLocalSchemaUrl).mockReturnValueOnce(false);
|
|
115
|
+
vi.mocked(readLocalOpenApiFile).mockResolvedValueOnce(mockValidOpenApiDocument);
|
|
116
|
+
vi.mocked(validate).mockRejectedValueOnce(new Error('some schema parsing error'));
|
|
117
|
+
|
|
118
|
+
await runCommand('openapi-check', 'test/openapi.yaml');
|
|
119
|
+
|
|
120
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
props: { message: 'some schema parsing error' },
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as previewing from '@poetora/previewing';
|
|
2
|
+
|
|
3
|
+
import { getLatestCliVersion, getVersions, execAsync } from '../src/helpers';
|
|
4
|
+
import { update } from '../src/update';
|
|
5
|
+
|
|
6
|
+
vi.mock('@poetora/previewing', async () => {
|
|
7
|
+
const originalModule =
|
|
8
|
+
await vi.importActual<typeof import('@poetora/previewing')>('@poetora/previewing');
|
|
9
|
+
return {
|
|
10
|
+
...originalModule,
|
|
11
|
+
getClientVersion: vi.fn(),
|
|
12
|
+
downloadTargetPoet: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('../src/helpers', async () => {
|
|
17
|
+
const originalModule = await vi.importActual<typeof import('../src/helpers')>('../src/helpers');
|
|
18
|
+
return {
|
|
19
|
+
...originalModule,
|
|
20
|
+
execAsync: vi.fn(),
|
|
21
|
+
getLatestCliVersion: vi.fn(),
|
|
22
|
+
getVersions: vi.fn().mockReturnValue({
|
|
23
|
+
cli: '1.0.0',
|
|
24
|
+
client: '1.0.0',
|
|
25
|
+
}),
|
|
26
|
+
detectPackageManager: vi.fn().mockResolvedValue('pnpm'),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const addLogSpy = vi.spyOn(previewing, 'addLog');
|
|
31
|
+
|
|
32
|
+
describe('update', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.resetAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should update the cli successfully', async () => {
|
|
42
|
+
vi.mocked(getVersions).mockReturnValue({
|
|
43
|
+
cli: '1.0.0',
|
|
44
|
+
client: '1.0.0',
|
|
45
|
+
});
|
|
46
|
+
vi.mocked(getLatestCliVersion).mockReturnValue('2.0.0');
|
|
47
|
+
vi.mocked(execAsync).mockResolvedValue({ stdout: '', stderr: '' });
|
|
48
|
+
vi.mocked(previewing.downloadTargetPoet).mockResolvedValue();
|
|
49
|
+
|
|
50
|
+
await update({ packageName: 'poetora' });
|
|
51
|
+
|
|
52
|
+
expect(addLogSpy).toHaveBeenCalledTimes(3);
|
|
53
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
54
|
+
expect.objectContaining({ props: { message: 'updating...' } })
|
|
55
|
+
);
|
|
56
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
57
|
+
expect.objectContaining({ props: { message: 'updating poetora package...' } })
|
|
58
|
+
);
|
|
59
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
props: { message: 'updated poetora to the latest version: 2.0.0' },
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
expect(execAsync).toHaveBeenCalledWith('pnpm install -g poetora@latest --silent');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return when already up to date', async () => {
|
|
68
|
+
vi.mocked(getVersions).mockReturnValue({
|
|
69
|
+
cli: '1.0.0',
|
|
70
|
+
client: '1.0.0',
|
|
71
|
+
});
|
|
72
|
+
vi.mocked(getLatestCliVersion).mockReturnValue('1.0.0');
|
|
73
|
+
vi.mocked(execAsync).mockResolvedValue({ stdout: '', stderr: '' });
|
|
74
|
+
|
|
75
|
+
await update({ packageName: 'poetora' });
|
|
76
|
+
|
|
77
|
+
expect(addLogSpy).toHaveBeenCalledTimes(2);
|
|
78
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
79
|
+
expect.objectContaining({ props: { message: 'updating...' } })
|
|
80
|
+
);
|
|
81
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
82
|
+
expect.objectContaining({ props: { message: 'already up to date' } })
|
|
83
|
+
);
|
|
84
|
+
expect(execAsync).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle cli update failure', async () => {
|
|
88
|
+
vi.mocked(getVersions).mockReturnValue({
|
|
89
|
+
cli: '1.0.0',
|
|
90
|
+
client: '1.0.0',
|
|
91
|
+
});
|
|
92
|
+
vi.mocked(getLatestCliVersion).mockReturnValue('2.0.0');
|
|
93
|
+
vi.mocked(execAsync).mockRejectedValue(new Error('Update failed'));
|
|
94
|
+
|
|
95
|
+
await update({ packageName: 'poetora' });
|
|
96
|
+
|
|
97
|
+
expect(addLogSpy).toHaveBeenCalledTimes(3);
|
|
98
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
99
|
+
expect.objectContaining({ props: { message: 'updating...' } })
|
|
100
|
+
);
|
|
101
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
102
|
+
expect.objectContaining({ props: { message: 'updating poetora package...' } })
|
|
103
|
+
);
|
|
104
|
+
expect(addLogSpy).toHaveBeenCalledWith(
|
|
105
|
+
expect.objectContaining({ props: { message: 'failed to update poetora@latest' } })
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cli } from '../src/cli';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Programmatically set arguments and execute the CLI script
|
|
5
|
+
*
|
|
6
|
+
* @param {...string} args - Additional command arguments.
|
|
7
|
+
*/
|
|
8
|
+
export async function runCommand(...args: string[]) {
|
|
9
|
+
process.argv = ['node', 'cli', ...args];
|
|
10
|
+
return cli({ packageName: 'poet' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const mockValidOpenApiDocument = {
|
|
14
|
+
openapi: '3.0.0',
|
|
15
|
+
info: {
|
|
16
|
+
title: 'Test API',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
},
|
|
19
|
+
components: {},
|
|
20
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const WCAG_STANDARDS: {
|
|
2
|
+
readonly AA_NORMAL: 4.5;
|
|
3
|
+
readonly AA_LARGE: 3;
|
|
4
|
+
readonly AAA_NORMAL: 7;
|
|
5
|
+
readonly AAA_LARGE: 4.5;
|
|
6
|
+
};
|
|
7
|
+
export type ContrastResult = {
|
|
8
|
+
ratio: number;
|
|
9
|
+
meetsAA: boolean;
|
|
10
|
+
meetsAAA: boolean;
|
|
11
|
+
recommendation: 'pass' | 'warning' | 'fail';
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function checkColorContrast(foreground: string, background: string, minThreshold?: number): ContrastResult | null;
|
|
15
|
+
export interface AccessibilityCheckResult {
|
|
16
|
+
primaryContrast: ContrastResult | null;
|
|
17
|
+
lightContrast: ContrastResult | null;
|
|
18
|
+
darkContrast: ContrastResult | null;
|
|
19
|
+
darkOnLightContrast: ContrastResult | null;
|
|
20
|
+
anchorResults: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
lightContrast: ContrastResult | null;
|
|
23
|
+
darkContrast: ContrastResult | null;
|
|
24
|
+
}>;
|
|
25
|
+
overallScore: 'pass' | 'warning' | 'fail';
|
|
26
|
+
}
|
|
27
|
+
export declare function checkDocsColors(colors: {
|
|
28
|
+
primary?: string;
|
|
29
|
+
light?: string;
|
|
30
|
+
dark?: string;
|
|
31
|
+
}, background: {
|
|
32
|
+
lightHex: string;
|
|
33
|
+
darkHex: string;
|
|
34
|
+
}, navigation?: {
|
|
35
|
+
global?: {
|
|
36
|
+
anchors?: Array<{
|
|
37
|
+
anchor: string;
|
|
38
|
+
color?: {
|
|
39
|
+
light?: string;
|
|
40
|
+
dark?: string;
|
|
41
|
+
};
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
}): AccessibilityCheckResult;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import Color from 'color';
|
|
2
|
+
export const WCAG_STANDARDS = {
|
|
3
|
+
AA_NORMAL: 4.5,
|
|
4
|
+
AA_LARGE: 3,
|
|
5
|
+
AAA_NORMAL: 7,
|
|
6
|
+
AAA_LARGE: 4.5,
|
|
7
|
+
};
|
|
8
|
+
export function checkColorContrast(foreground, background, minThreshold = WCAG_STANDARDS.AA_NORMAL) {
|
|
9
|
+
try {
|
|
10
|
+
const fg = Color(foreground);
|
|
11
|
+
const bg = Color(background);
|
|
12
|
+
const ratio = fg.contrast(bg);
|
|
13
|
+
const level = fg.level(bg);
|
|
14
|
+
const meetsAA = level === 'AA' || level === 'AAA';
|
|
15
|
+
const meetsAAA = level === 'AAA';
|
|
16
|
+
let recommendation;
|
|
17
|
+
let message;
|
|
18
|
+
if (minThreshold !== WCAG_STANDARDS.AA_NORMAL) {
|
|
19
|
+
if (ratio >= minThreshold) {
|
|
20
|
+
recommendation = 'pass';
|
|
21
|
+
message = `Contrast ratio: ${ratio.toFixed(2)}:1 (meets minimum threshold of ${minThreshold}:1)`;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
recommendation = 'fail';
|
|
25
|
+
message = `Poor contrast ratio: ${ratio.toFixed(2)}:1 (fails minimum threshold, required: ${minThreshold}:1)`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
if (meetsAAA) {
|
|
30
|
+
recommendation = 'pass';
|
|
31
|
+
message = `Excellent contrast ratio: ${ratio.toFixed(2)}:1 (meets WCAG AAA)`;
|
|
32
|
+
}
|
|
33
|
+
else if (meetsAA) {
|
|
34
|
+
recommendation = 'warning';
|
|
35
|
+
message = `Good contrast ratio: ${ratio.toFixed(2)}:1 (meets WCAG AA, consider AAA for better accessibility)`;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
recommendation = 'fail';
|
|
39
|
+
message = `Poor contrast ratio: ${ratio.toFixed(2)}:1 (fails WCAG AA, minimum required: ${WCAG_STANDARDS.AA_NORMAL}:1)`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ratio,
|
|
44
|
+
meetsAA,
|
|
45
|
+
meetsAAA,
|
|
46
|
+
recommendation,
|
|
47
|
+
message,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function checkDocsColors(colors, background, navigation) {
|
|
55
|
+
const lightBackground = background.lightHex;
|
|
56
|
+
const darkBackground = background.darkHex;
|
|
57
|
+
const primaryContrast = colors.primary
|
|
58
|
+
? checkColorContrast(colors.primary, lightBackground)
|
|
59
|
+
: null;
|
|
60
|
+
const lightContrast = colors.light ? checkColorContrast(colors.light, darkBackground) : null;
|
|
61
|
+
const darkContrast = colors.dark ? checkColorContrast(colors.dark, darkBackground, 3) : null;
|
|
62
|
+
const darkOnLightContrast = colors.dark
|
|
63
|
+
? checkColorContrast(colors.dark, lightBackground, 3)
|
|
64
|
+
: null;
|
|
65
|
+
const anchorResults = [];
|
|
66
|
+
if (navigation?.global?.anchors) {
|
|
67
|
+
for (const anchor of navigation.global.anchors) {
|
|
68
|
+
if (anchor.color) {
|
|
69
|
+
const lightContrast = anchor.color.light
|
|
70
|
+
? checkColorContrast(anchor.color.light, lightBackground)
|
|
71
|
+
: null;
|
|
72
|
+
const darkContrast = anchor.color.dark
|
|
73
|
+
? checkColorContrast(anchor.color.dark, darkBackground)
|
|
74
|
+
: null;
|
|
75
|
+
anchorResults.push({
|
|
76
|
+
name: anchor.anchor,
|
|
77
|
+
lightContrast,
|
|
78
|
+
darkContrast,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const results = [
|
|
84
|
+
primaryContrast,
|
|
85
|
+
lightContrast,
|
|
86
|
+
darkContrast,
|
|
87
|
+
darkOnLightContrast,
|
|
88
|
+
...anchorResults.flatMap((anchor) => [anchor.lightContrast, anchor.darkContrast]),
|
|
89
|
+
].filter(Boolean);
|
|
90
|
+
const hasFailure = results.some((result) => result.recommendation === 'fail');
|
|
91
|
+
const hasWarning = results.some((result) => result.recommendation === 'warning');
|
|
92
|
+
let overallScore;
|
|
93
|
+
if (hasFailure) {
|
|
94
|
+
overallScore = 'fail';
|
|
95
|
+
}
|
|
96
|
+
else if (hasWarning) {
|
|
97
|
+
overallScore = 'warning';
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
overallScore = 'pass';
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
primaryContrast,
|
|
104
|
+
lightContrast,
|
|
105
|
+
darkContrast,
|
|
106
|
+
darkOnLightContrast,
|
|
107
|
+
anchorResults,
|
|
108
|
+
overallScore,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getConfigObj, getConfigPath } from '@poetora/prebuild';
|
|
3
|
+
import { addLog, ErrorLog, WarningLog } from '@poetora/previewing';
|
|
4
|
+
import { getBackgroundColors } from '@poetora/shared';
|
|
5
|
+
import { Text } from 'ink';
|
|
6
|
+
import { checkDocsColors } from './accessibility.js';
|
|
7
|
+
import { CMD_EXEC_PATH } from './constants.js';
|
|
8
|
+
export const accessibilityCheck = async () => {
|
|
9
|
+
try {
|
|
10
|
+
const docsConfigPath = await getConfigPath(CMD_EXEC_PATH);
|
|
11
|
+
if (!docsConfigPath) {
|
|
12
|
+
addLog(_jsx(ErrorLog, { message: "No configuration file found. Please run this command from a directory with a docs.json file." }));
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
const config = await getConfigObj(CMD_EXEC_PATH);
|
|
16
|
+
if (!config.colors) {
|
|
17
|
+
addLog(_jsx(WarningLog, { message: "No colors section found in configuration file" }));
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const { colors, navigation } = config;
|
|
21
|
+
const { lightHex, darkHex } = getBackgroundColors(config);
|
|
22
|
+
const results = checkDocsColors(colors, { lightHex, darkHex }, navigation);
|
|
23
|
+
const displayContrastResult = (result, label, prefix = '') => {
|
|
24
|
+
if (!result)
|
|
25
|
+
return;
|
|
26
|
+
const { recommendation, message } = result;
|
|
27
|
+
const icon = recommendation === 'pass' ? 'PASS' : recommendation === 'warning' ? 'WARN' : 'FAIL';
|
|
28
|
+
const color = recommendation === 'pass' ? 'green' : recommendation === 'warning' ? 'yellow' : 'red';
|
|
29
|
+
addLog(_jsxs(Text, { children: [_jsxs(Text, { bold: prefix === '', children: [prefix, label, ":", ' '] }), _jsxs(Text, { color: color, children: [icon, " ", message] })] }));
|
|
30
|
+
};
|
|
31
|
+
addLog(_jsx(Text, { bold: true, color: "cyan", children: "Checking color accessibility..." }));
|
|
32
|
+
addLog(_jsx(Text, {}));
|
|
33
|
+
displayContrastResult(results.primaryContrast, `Primary Color (${colors.primary}) vs Light Background`);
|
|
34
|
+
displayContrastResult(results.lightContrast, `Light Color (${colors.light}) vs Dark Background`);
|
|
35
|
+
displayContrastResult(results.darkContrast, `Dark Color (${colors.dark}) vs Dark Background`);
|
|
36
|
+
displayContrastResult(results.darkOnLightContrast, `Dark Color (${colors.dark}) vs Light Background`);
|
|
37
|
+
const anchorsWithResults = results.anchorResults.filter((anchor) => anchor.lightContrast || anchor.darkContrast);
|
|
38
|
+
if (anchorsWithResults.length > 0) {
|
|
39
|
+
addLog(_jsx(Text, {}));
|
|
40
|
+
addLog(_jsx(Text, { bold: true, color: "cyan", children: "Navigation Anchors:" }));
|
|
41
|
+
for (const anchor of anchorsWithResults) {
|
|
42
|
+
addLog(_jsxs(Text, { bold: true, children: [" ", anchor.name, ":"] }));
|
|
43
|
+
displayContrastResult(anchor.lightContrast, 'Light variant vs Light Background', ' ');
|
|
44
|
+
displayContrastResult(anchor.darkContrast, 'Dark variant vs Dark Background', ' ');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
addLog(_jsx(Text, {}));
|
|
48
|
+
const overallIcon = results.overallScore === 'pass'
|
|
49
|
+
? 'PASS'
|
|
50
|
+
: results.overallScore === 'warning'
|
|
51
|
+
? 'WARN'
|
|
52
|
+
: 'FAIL';
|
|
53
|
+
const overallColor = results.overallScore === 'pass'
|
|
54
|
+
? 'green'
|
|
55
|
+
: results.overallScore === 'warning'
|
|
56
|
+
? 'yellow'
|
|
57
|
+
: 'red';
|
|
58
|
+
const overallMessage = results.overallScore === 'pass'
|
|
59
|
+
? 'All colors meet accessibility standards!'
|
|
60
|
+
: results.overallScore === 'warning'
|
|
61
|
+
? 'Some colors could be improved for better accessibility'
|
|
62
|
+
: 'Some colors fail accessibility standards and should be updated';
|
|
63
|
+
addLog(_jsx(Text, { children: _jsxs(Text, { bold: true, color: overallColor, children: ["Overall Assessment: ", overallIcon, " ", overallMessage] }) }));
|
|
64
|
+
return results.overallScore === 'fail' ? 1 : 0;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
addLog(_jsx(ErrorLog, { message: `Accessibility check failed: ${error instanceof Error ? error.message : 'Unknown error'}` }));
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
};
|