@poetora/cli 0.1.9 → 0.1.11
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/bin/cli-builder.js +32 -18
- package/bin/services/link.service.js +0 -11
- package/bin/services/version.service.d.ts +1 -1
- package/bin/services/version.service.js +12 -10
- package/bin/utils/index.d.ts +1 -0
- package/bin/utils/index.js +1 -0
- package/bin/utils/terminate.d.ts +1 -0
- package/bin/utils/terminate.js +4 -0
- package/package.json +5 -1
- package/.turbo/turbo-build.log +0 -4
- package/src/accessibility.ts +0 -180
- package/src/cli-builder.ts +0 -274
- package/src/cli.ts +0 -22
- package/src/commands/__tests__/base.command.test.ts +0 -139
- package/src/commands/__tests__/dev.command.test.ts +0 -241
- package/src/commands/__tests__/init.command.test.ts +0 -281
- package/src/commands/__tests__/utils.ts +0 -20
- package/src/commands/base.command.ts +0 -97
- package/src/commands/check.command.ts +0 -40
- package/src/commands/dev.command.ts +0 -63
- package/src/commands/index.ts +0 -6
- package/src/commands/init.command.ts +0 -125
- package/src/commands/link.command.ts +0 -39
- package/src/commands/update.command.ts +0 -23
- package/src/constants.ts +0 -4
- package/src/errors/cli-error.ts +0 -83
- package/src/errors/index.ts +0 -1
- package/src/index.ts +0 -110
- package/src/mdxAccessibility.ts +0 -132
- package/src/middlewares.ts +0 -73
- package/src/services/__tests__/port.service.test.ts +0 -83
- package/src/services/__tests__/template.service.test.ts +0 -234
- package/src/services/__tests__/version.service.test.ts +0 -165
- package/src/services/accessibility-check.service.ts +0 -226
- package/src/services/index.ts +0 -7
- package/src/services/link.service.ts +0 -65
- package/src/services/openapi-check.service.ts +0 -68
- package/src/services/port.service.ts +0 -47
- package/src/services/template.service.ts +0 -203
- package/src/services/update.service.ts +0 -76
- package/src/services/version.service.ts +0 -161
- package/src/start.ts +0 -6
- package/src/types/common.ts +0 -53
- package/src/types/index.ts +0 -2
- package/src/types/options.ts +0 -42
- package/src/utils/console-logger.ts +0 -123
- package/src/utils/index.ts +0 -2
- package/src/utils/logger.interface.ts +0 -70
- package/tsconfig.build.json +0 -17
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -8
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import AdmZip from 'adm-zip';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as fse from 'fs-extra';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { ExternalServiceError, FileSystemError } from '../../errors/index.js';
|
|
6
|
-
import { TemplateService } from '../template.service.js';
|
|
7
|
-
|
|
8
|
-
vi.mock('node:fs', () => ({
|
|
9
|
-
default: {
|
|
10
|
-
promises: {
|
|
11
|
-
readdir: vi.fn(),
|
|
12
|
-
writeFile: vi.fn(),
|
|
13
|
-
readFile: vi.fn(),
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
promises: {
|
|
17
|
-
readdir: vi.fn(),
|
|
18
|
-
writeFile: vi.fn(),
|
|
19
|
-
readFile: vi.fn(),
|
|
20
|
-
},
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
vi.mock('fs-extra');
|
|
24
|
-
vi.mock('adm-zip', () => ({
|
|
25
|
-
default: vi.fn(),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
describe('TemplateService', () => {
|
|
29
|
-
let service: TemplateService;
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
service = new TemplateService();
|
|
33
|
-
vi.clearAllMocks();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('checkDirectory', () => {
|
|
41
|
-
it('should return exists=true and hasContents=true for non-empty directory', async () => {
|
|
42
|
-
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
|
|
43
|
-
vi.mocked(fs.promises.readdir).mockResolvedValue([
|
|
44
|
-
'file1.txt',
|
|
45
|
-
'file2.txt',
|
|
46
|
-
] as unknown as fs.Dirent[]);
|
|
47
|
-
|
|
48
|
-
const result = await service.checkDirectory('/test/dir');
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual({
|
|
51
|
-
exists: true,
|
|
52
|
-
hasContents: true,
|
|
53
|
-
});
|
|
54
|
-
expect(fse.ensureDir).toHaveBeenCalledWith('/test/dir');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should return exists=true and hasContents=false for empty directory', async () => {
|
|
58
|
-
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
|
|
59
|
-
vi.mocked(fs.promises.readdir).mockResolvedValue([] as unknown as fs.Dirent[]);
|
|
60
|
-
|
|
61
|
-
const result = await service.checkDirectory('/test/empty');
|
|
62
|
-
|
|
63
|
-
expect(result).toEqual({
|
|
64
|
-
exists: true,
|
|
65
|
-
hasContents: false,
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should return exists=false for non-existent directory on ENOENT', async () => {
|
|
70
|
-
const error = new Error('ENOENT') as NodeJS.ErrnoException;
|
|
71
|
-
error.code = 'ENOENT';
|
|
72
|
-
vi.mocked(fse.ensureDir).mockRejectedValue(error);
|
|
73
|
-
|
|
74
|
-
const result = await service.checkDirectory('/nonexistent');
|
|
75
|
-
|
|
76
|
-
expect(result).toEqual({
|
|
77
|
-
exists: false,
|
|
78
|
-
hasContents: false,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should throw FileSystemError for other errors', async () => {
|
|
83
|
-
vi.mocked(fse.ensureDir).mockRejectedValue(new Error('Permission denied'));
|
|
84
|
-
|
|
85
|
-
await expect(service.checkDirectory('/test/dir')).rejects.toThrow(FileSystemError);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('getAvailableThemes', () => {
|
|
90
|
-
it('should return available themes from docsConfigSchema', () => {
|
|
91
|
-
const themes = service.getAvailableThemes();
|
|
92
|
-
|
|
93
|
-
expect(themes).toBeInstanceOf(Array);
|
|
94
|
-
expect(themes.length).toBeGreaterThan(0);
|
|
95
|
-
expect(themes).toContain('ora');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('installTemplate', () => {
|
|
100
|
-
const mockFetch = vi.fn();
|
|
101
|
-
const originalFetch = global.fetch;
|
|
102
|
-
|
|
103
|
-
beforeEach(() => {
|
|
104
|
-
global.fetch = mockFetch;
|
|
105
|
-
|
|
106
|
-
// Mock successful download
|
|
107
|
-
mockFetch.mockResolvedValue({
|
|
108
|
-
ok: true,
|
|
109
|
-
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Mock file operations
|
|
113
|
-
vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined);
|
|
114
|
-
vi.mocked(fs.promises.readFile).mockResolvedValue('{}');
|
|
115
|
-
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
|
|
116
|
-
vi.mocked(fse.copy).mockResolvedValue(undefined);
|
|
117
|
-
vi.mocked(fse.remove).mockResolvedValue(undefined);
|
|
118
|
-
|
|
119
|
-
// Mock AdmZip constructor - must use function for constructor
|
|
120
|
-
vi.mocked(AdmZip).mockImplementation(function (this: AdmZip) {
|
|
121
|
-
return {
|
|
122
|
-
extractAllTo: vi.fn(),
|
|
123
|
-
} as unknown as AdmZip;
|
|
124
|
-
} as unknown as typeof AdmZip);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
afterEach(() => {
|
|
128
|
-
global.fetch = originalFetch;
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should successfully install template', async () => {
|
|
132
|
-
await service.installTemplate({
|
|
133
|
-
directory: '/test/project',
|
|
134
|
-
projectName: 'My Project',
|
|
135
|
-
theme: 'quartz',
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
139
|
-
'https://github.com/poetora/starter/archive/refs/heads/main.zip'
|
|
140
|
-
);
|
|
141
|
-
expect(fse.copy).toHaveBeenCalled();
|
|
142
|
-
expect(fs.promises.writeFile).toHaveBeenCalledWith(
|
|
143
|
-
'/test/project/docs.json',
|
|
144
|
-
expect.stringContaining('My Project'),
|
|
145
|
-
'utf-8'
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should throw ExternalServiceError on download failure', async () => {
|
|
150
|
-
mockFetch.mockResolvedValue({
|
|
151
|
-
ok: false,
|
|
152
|
-
status: 404,
|
|
153
|
-
statusText: 'Not Found',
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
await expect(
|
|
157
|
-
service.installTemplate({
|
|
158
|
-
directory: '/test/project',
|
|
159
|
-
projectName: 'My Project',
|
|
160
|
-
theme: 'quartz',
|
|
161
|
-
})
|
|
162
|
-
).rejects.toThrow(ExternalServiceError);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should cleanup temporary files after successful installation', async () => {
|
|
166
|
-
await service.installTemplate({
|
|
167
|
-
directory: '/test/project',
|
|
168
|
-
projectName: 'My Project',
|
|
169
|
-
theme: 'quartz',
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
expect(fse.remove).toHaveBeenCalledWith('poetora-starter.zip');
|
|
173
|
-
expect(fse.remove).toHaveBeenCalledWith('poetora-starter-temp');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should cleanup temporary files even on error', async () => {
|
|
177
|
-
vi.mocked(fse.copy).mockRejectedValue(new Error('Copy failed'));
|
|
178
|
-
|
|
179
|
-
await expect(
|
|
180
|
-
service.installTemplate({
|
|
181
|
-
directory: '/test/project',
|
|
182
|
-
projectName: 'My Project',
|
|
183
|
-
theme: 'quartz',
|
|
184
|
-
})
|
|
185
|
-
).rejects.toThrow();
|
|
186
|
-
|
|
187
|
-
expect(fse.remove).toHaveBeenCalled();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should handle missing docs.json by creating new config', async () => {
|
|
191
|
-
vi.mocked(fs.promises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
192
|
-
|
|
193
|
-
await service.installTemplate({
|
|
194
|
-
directory: '/test/project',
|
|
195
|
-
projectName: 'My Project',
|
|
196
|
-
theme: 'quartz',
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
expect(fs.promises.writeFile).toHaveBeenCalledWith(
|
|
200
|
-
'/test/project/docs.json',
|
|
201
|
-
expect.stringContaining('"name": "My Project"'),
|
|
202
|
-
'utf-8'
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should preserve existing config fields when updating', async () => {
|
|
207
|
-
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
|
208
|
-
JSON.stringify({
|
|
209
|
-
name: 'Old Name',
|
|
210
|
-
theme: 'old-theme',
|
|
211
|
-
customField: 'preserved',
|
|
212
|
-
})
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
await service.installTemplate({
|
|
216
|
-
directory: '/test/project',
|
|
217
|
-
projectName: 'New Project',
|
|
218
|
-
theme: 'quartz',
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const writeCall = vi
|
|
222
|
-
.mocked(fs.promises.writeFile)
|
|
223
|
-
.mock.calls.find((call) => call[0] === '/test/project/docs.json');
|
|
224
|
-
|
|
225
|
-
expect(writeCall).toBeDefined();
|
|
226
|
-
const writtenConfig = JSON.parse(writeCall?.[1] as string);
|
|
227
|
-
expect(writtenConfig).toMatchObject({
|
|
228
|
-
name: 'New Project',
|
|
229
|
-
theme: 'quartz',
|
|
230
|
-
customField: 'preserved',
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { VersionService } from '../version.service.js';
|
|
3
|
-
|
|
4
|
-
describe('VersionService', () => {
|
|
5
|
-
let service: VersionService;
|
|
6
|
-
let _originalVersion: string;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
service = new VersionService();
|
|
10
|
-
_originalVersion = process.version;
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('parseNodeVersion', () => {
|
|
18
|
-
it('should parse version with v prefix', () => {
|
|
19
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.17.0');
|
|
20
|
-
|
|
21
|
-
const version = service.parseNodeVersion();
|
|
22
|
-
|
|
23
|
-
expect(version.major).toBe(20);
|
|
24
|
-
expect(version.minor).toBe(17);
|
|
25
|
-
expect(version.patch).toBe(0);
|
|
26
|
-
expect(version.raw).toBe('20.17.0');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should parse version without v prefix', () => {
|
|
30
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('18.0.0');
|
|
31
|
-
|
|
32
|
-
const version = service.parseNodeVersion();
|
|
33
|
-
|
|
34
|
-
expect(version.major).toBe(18);
|
|
35
|
-
expect(version.minor).toBe(0);
|
|
36
|
-
expect(version.patch).toBe(0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should parse version with only major.minor', () => {
|
|
40
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.17');
|
|
41
|
-
|
|
42
|
-
const version = service.parseNodeVersion();
|
|
43
|
-
|
|
44
|
-
expect(version.major).toBe(20);
|
|
45
|
-
expect(version.minor).toBe(17);
|
|
46
|
-
expect(version.patch).toBe(0);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('checkNodeVersion', () => {
|
|
51
|
-
it('should pass for Node 20.17.0', () => {
|
|
52
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.17.0');
|
|
53
|
-
|
|
54
|
-
const result = service.checkNodeVersion();
|
|
55
|
-
|
|
56
|
-
expect(result.isValid).toBe(true);
|
|
57
|
-
expect(result.hasWarning).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should pass for Node 18.0.0 (minimum)', () => {
|
|
61
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v18.0.0');
|
|
62
|
-
|
|
63
|
-
const result = service.checkNodeVersion();
|
|
64
|
-
|
|
65
|
-
expect(result.isValid).toBe(true);
|
|
66
|
-
expect(result.hasWarning).toBe(true);
|
|
67
|
-
expect(result.message).toContain('20.17');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should pass for Node 24.x', () => {
|
|
71
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v24.0.0');
|
|
72
|
-
|
|
73
|
-
const result = service.checkNodeVersion();
|
|
74
|
-
|
|
75
|
-
expect(result.isValid).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should fail for Node 17.x', () => {
|
|
79
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v17.9.0');
|
|
80
|
-
|
|
81
|
-
const result = service.checkNodeVersion();
|
|
82
|
-
|
|
83
|
-
expect(result.isValid).toBe(false);
|
|
84
|
-
expect(result.message).toContain('18.0');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should fail for Node 25.x', () => {
|
|
88
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v25.0.0');
|
|
89
|
-
|
|
90
|
-
const result = service.checkNodeVersion();
|
|
91
|
-
|
|
92
|
-
expect(result.isValid).toBe(false);
|
|
93
|
-
expect(result.message).toContain('not supported');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should warn for Node 19.x (below recommended)', () => {
|
|
97
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v19.5.0');
|
|
98
|
-
|
|
99
|
-
const result = service.checkNodeVersion();
|
|
100
|
-
|
|
101
|
-
expect(result.isValid).toBe(true);
|
|
102
|
-
expect(result.hasWarning).toBe(true);
|
|
103
|
-
expect(result.message).toContain('recommended');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should warn for Node 20.16 (below recommended minor)', () => {
|
|
107
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.16.0');
|
|
108
|
-
|
|
109
|
-
const result = service.checkNodeVersion();
|
|
110
|
-
|
|
111
|
-
expect(result.isValid).toBe(true);
|
|
112
|
-
expect(result.hasWarning).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('getCliVersion', () => {
|
|
117
|
-
it('should return test-cli in test mode', () => {
|
|
118
|
-
const originalEnv = process.env.CLI_TEST_MODE;
|
|
119
|
-
process.env.CLI_TEST_MODE = 'true';
|
|
120
|
-
|
|
121
|
-
const version = service.getCliVersion();
|
|
122
|
-
|
|
123
|
-
expect(version).toBe('test-cli');
|
|
124
|
-
|
|
125
|
-
process.env.CLI_TEST_MODE = originalEnv;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should return version from package.json', () => {
|
|
129
|
-
const version = service.getCliVersion();
|
|
130
|
-
|
|
131
|
-
expect(version).toBeDefined();
|
|
132
|
-
expect(typeof version).toBe('string');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('getLatestCliVersion', () => {
|
|
137
|
-
it('should return latest version from npm', () => {
|
|
138
|
-
const version = service.getLatestCliVersion('@poetora/cli');
|
|
139
|
-
|
|
140
|
-
expect(version).toBeDefined();
|
|
141
|
-
expect(typeof version).toBe('string');
|
|
142
|
-
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('isVersionUpToDate', () => {
|
|
147
|
-
it('should return true when versions match', () => {
|
|
148
|
-
const result = service.isVersionUpToDate('1.0.0', '1.0.0');
|
|
149
|
-
|
|
150
|
-
expect(result).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should return false when versions differ', () => {
|
|
154
|
-
const result = service.isVersionUpToDate('1.0.0', '1.0.1');
|
|
155
|
-
|
|
156
|
-
expect(result).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should handle whitespace', () => {
|
|
160
|
-
const result = service.isVersionUpToDate('1.0.0 ', ' 1.0.0');
|
|
161
|
-
|
|
162
|
-
expect(result).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
});
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { getConfigObj, getConfigPath } from '@poetora/prebuild';
|
|
2
|
-
import { getBackgroundColors } from '@poetora/shared';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
type AccessibilityCheckResult,
|
|
6
|
-
type ContrastResult,
|
|
7
|
-
checkDocsColors,
|
|
8
|
-
} from '../accessibility.js';
|
|
9
|
-
import { CMD_EXEC_PATH } from '../constants.js';
|
|
10
|
-
import { type AccessibilityFixAttribute, checkMdxAccessibility } from '../mdxAccessibility.js';
|
|
11
|
-
import { ConsoleLogger } from '../utils/console-logger.js';
|
|
12
|
-
import type { ILogger } from '../utils/logger.interface.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Service for accessibility checking
|
|
16
|
-
*/
|
|
17
|
-
export class AccessibilityCheckService {
|
|
18
|
-
constructor(private readonly logger: ILogger = new ConsoleLogger()) {}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Display contrast result with color-coded output
|
|
22
|
-
*/
|
|
23
|
-
private displayContrastResult(
|
|
24
|
-
result: ContrastResult | null,
|
|
25
|
-
label: string,
|
|
26
|
-
prefix: string = ''
|
|
27
|
-
): void {
|
|
28
|
-
if (!result) return;
|
|
29
|
-
|
|
30
|
-
const { recommendation } = result;
|
|
31
|
-
|
|
32
|
-
// Custom messages with our own tone
|
|
33
|
-
let statusText: string;
|
|
34
|
-
let detailMessage: string;
|
|
35
|
-
|
|
36
|
-
if (recommendation === 'pass') {
|
|
37
|
-
if (result.meetsAAA) {
|
|
38
|
-
statusText = '✓ Excellent';
|
|
39
|
-
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (AAA standard)`;
|
|
40
|
-
} else {
|
|
41
|
-
statusText = '✓ Good';
|
|
42
|
-
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (AA standard)`;
|
|
43
|
-
}
|
|
44
|
-
} else if (recommendation === 'warning') {
|
|
45
|
-
statusText = '⚠ Acceptable';
|
|
46
|
-
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (consider improving)`;
|
|
47
|
-
} else {
|
|
48
|
-
statusText = '✗ Poor';
|
|
49
|
-
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (below AA standard)`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Display with label in cyan and status+message in appropriate color
|
|
53
|
-
const labelText = `${prefix}${label}:`;
|
|
54
|
-
const statusColor =
|
|
55
|
-
recommendation === 'pass' ? 'green' : recommendation === 'warning' ? 'yellow' : 'red';
|
|
56
|
-
|
|
57
|
-
this.logger.log(`${labelText} ${this.colorize(statusText, statusColor)} ${detailMessage}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private colorize(text: string, color: 'green' | 'yellow' | 'red'): string {
|
|
61
|
-
// Use chalk directly for inline coloring
|
|
62
|
-
const chalk = (text: string, color: 'green' | 'yellow' | 'red') => {
|
|
63
|
-
const colors: Record<string, string> = {
|
|
64
|
-
green: '\x1b[32m',
|
|
65
|
-
yellow: '\x1b[33m',
|
|
66
|
-
red: '\x1b[31m',
|
|
67
|
-
reset: '\x1b[0m',
|
|
68
|
-
};
|
|
69
|
-
return `${colors[color]}${text}${colors.reset}`;
|
|
70
|
-
};
|
|
71
|
-
return chalk(text, color);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Check color contrast accessibility
|
|
76
|
-
*/
|
|
77
|
-
private async checkColorAccessibility(): /* prettier-ignore */ Promise<number> {
|
|
78
|
-
try {
|
|
79
|
-
const docsConfigPath = await getConfigPath(CMD_EXEC_PATH);
|
|
80
|
-
|
|
81
|
-
if (!docsConfigPath) {
|
|
82
|
-
this.logger.error(
|
|
83
|
-
'No configuration file found. Please run this command from a directory with a docs.json file.'
|
|
84
|
-
);
|
|
85
|
-
return 1;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const config = await getConfigObj(CMD_EXEC_PATH);
|
|
89
|
-
|
|
90
|
-
if (!config?.colors) {
|
|
91
|
-
this.logger.warn('No colors section found in configuration file');
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const { colors, navigation } = config;
|
|
96
|
-
const { lightHex, darkHex } = getBackgroundColors(config);
|
|
97
|
-
|
|
98
|
-
const results: AccessibilityCheckResult = checkDocsColors(
|
|
99
|
-
colors,
|
|
100
|
-
{ lightHex, darkHex },
|
|
101
|
-
navigation
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
this.logger.log('Checking color accessibility...');
|
|
105
|
-
|
|
106
|
-
this.displayContrastResult(
|
|
107
|
-
results.primaryContrast,
|
|
108
|
-
`Primary Color (${colors.primary}) vs Light Background`
|
|
109
|
-
);
|
|
110
|
-
this.displayContrastResult(
|
|
111
|
-
results.lightContrast,
|
|
112
|
-
`Light Color (${colors.light}) vs Dark Background`
|
|
113
|
-
);
|
|
114
|
-
this.displayContrastResult(
|
|
115
|
-
results.darkContrast,
|
|
116
|
-
`Dark Color (${colors.dark}) vs Dark Background`
|
|
117
|
-
);
|
|
118
|
-
this.displayContrastResult(
|
|
119
|
-
results.darkOnLightContrast,
|
|
120
|
-
`Dark Color (${colors.dark}) vs Light Background`
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const anchorsWithResults = results.anchorResults.filter(
|
|
124
|
-
(anchor) => anchor.lightContrast || anchor.darkContrast
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
if (anchorsWithResults.length > 0) {
|
|
128
|
-
for (const anchor of anchorsWithResults) {
|
|
129
|
-
this.displayContrastResult(anchor.lightContrast, `${anchor.name} vs Light Background`);
|
|
130
|
-
this.displayContrastResult(anchor.darkContrast, `${anchor.name} vs Dark Background`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Overall assessment with custom messages
|
|
135
|
-
let overallDisplay: string;
|
|
136
|
-
if (results.overallScore === 'fail') {
|
|
137
|
-
overallDisplay =
|
|
138
|
-
this.colorize('✗ Action needed:', 'red') +
|
|
139
|
-
' Update colors to meet accessibility standards';
|
|
140
|
-
} else if (results.overallScore === 'warning') {
|
|
141
|
-
overallDisplay = `${this.colorize('⚠ Room for improvement:', 'yellow')} Consider enhancing color contrast`;
|
|
142
|
-
} else {
|
|
143
|
-
overallDisplay = `${this.colorize('✓ All good:', 'green')} Colors meet accessibility guidelines`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
this.logger.logNewLine();
|
|
147
|
-
this.logger.log(overallDisplay);
|
|
148
|
-
|
|
149
|
-
return results.overallScore === 'fail' ? 1 : 0;
|
|
150
|
-
} catch (error) {
|
|
151
|
-
this.logger.error(`Failed to check color accessibility: ${error}`);
|
|
152
|
-
return 1;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Check MDX files for accessibility issues
|
|
158
|
-
*/
|
|
159
|
-
private async checkMdxAccessibility(): /* prettier-ignore */ Promise<number> {
|
|
160
|
-
try {
|
|
161
|
-
this.logger.log('Checking mdx files for accessibility issues...');
|
|
162
|
-
|
|
163
|
-
const results = await checkMdxAccessibility();
|
|
164
|
-
|
|
165
|
-
if (results.missingAltAttributes.length === 0) {
|
|
166
|
-
this.logger.success('no accessibility issues found');
|
|
167
|
-
this.logger.info(
|
|
168
|
-
`Checked ${results.totalFiles} MDX files - all images and videos have alt attributes.`
|
|
169
|
-
);
|
|
170
|
-
return 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const issuesByFile: Record<string, AccessibilityFixAttribute[]> = {};
|
|
174
|
-
results.missingAltAttributes.forEach((issue) => {
|
|
175
|
-
if (!issuesByFile[issue.filePath]) {
|
|
176
|
-
issuesByFile[issue.filePath] = [];
|
|
177
|
-
}
|
|
178
|
-
issuesByFile[issue.filePath]?.push(issue);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
this.logger.log(
|
|
182
|
-
`Found ${this.logger.highlight?.(results.missingAltAttributes.length.toString()) ?? results.missingAltAttributes.length} accessibility issue${results.missingAltAttributes.length === 1 ? '' : 's'} in ${this.logger.highlight?.(results.filesWithIssues.toString()) ?? results.filesWithIssues} file${results.filesWithIssues === 1 ? '' : 's'}:`
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
for (const [filePath, issues] of Object.entries(issuesByFile)) {
|
|
186
|
-
const relativePath = path.relative(process.cwd(), filePath);
|
|
187
|
-
this.logger.log(`${relativePath}:`);
|
|
188
|
-
|
|
189
|
-
for (const issue of issues) {
|
|
190
|
-
const location =
|
|
191
|
-
issue.line && issue.column ? ` (line ${issue.line}, col ${issue.column})` : '';
|
|
192
|
-
if (issue.element === 'a') {
|
|
193
|
-
this.logger.logColor(
|
|
194
|
-
` ✗ Missing text attribute ${issue.tagName} element${location}`,
|
|
195
|
-
'red'
|
|
196
|
-
);
|
|
197
|
-
} else {
|
|
198
|
-
this.logger.logColor(
|
|
199
|
-
` ✗ Missing alt attribute on ${issue.tagName} element${location}`,
|
|
200
|
-
'red'
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
this.logger.warn(
|
|
207
|
-
'Recommendation: Add alt attributes to all images and videos for better accessibility.'
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
return 1;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
this.logger.error(`Failed to check MDX accessibility: ${error}`);
|
|
213
|
-
return 1;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Run accessibility checks on documentation
|
|
219
|
-
* @returns Exit code (0 for success, 1 for failure)
|
|
220
|
-
*/
|
|
221
|
-
async checkAccessibility(): /* prettier-ignore */ Promise<number> {
|
|
222
|
-
const colorCheckCode = await this.checkColorAccessibility();
|
|
223
|
-
const mdxCheckCode = await this.checkMdxAccessibility();
|
|
224
|
-
return colorCheckCode || mdxCheckCode;
|
|
225
|
-
}
|
|
226
|
-
}
|
package/src/services/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export * from './accessibility-check.service.js';
|
|
2
|
-
export * from './link.service.js';
|
|
3
|
-
export * from './openapi-check.service.js';
|
|
4
|
-
export * from './port.service.js';
|
|
5
|
-
export * from './template.service.js';
|
|
6
|
-
export * from './update.service.js';
|
|
7
|
-
export * from './version.service.js';
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getBrokenInternalLinks,
|
|
3
|
-
type MdxPath,
|
|
4
|
-
renameFilesAndUpdateLinksInContent,
|
|
5
|
-
} from '@poetora/link-rot';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
|
|
8
|
-
import type { ILogger } from '../utils/index.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Service for link management
|
|
12
|
-
*/
|
|
13
|
-
export class LinkService {
|
|
14
|
-
constructor(private readonly logger: ILogger) {}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Check for broken internal links
|
|
18
|
-
* @returns Broken links grouped by file
|
|
19
|
-
*/
|
|
20
|
-
async checkBrokenLinks(): Promise<Record<string, string[]>> {
|
|
21
|
-
const brokenLinks = await getBrokenInternalLinks();
|
|
22
|
-
|
|
23
|
-
if (brokenLinks.length === 0) {
|
|
24
|
-
this.logger.success('no broken links found');
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Group broken links by file
|
|
29
|
-
const brokenLinksByFile: Record<string, string[]> = {};
|
|
30
|
-
brokenLinks.forEach((mdxPath: MdxPath) => {
|
|
31
|
-
const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
|
|
32
|
-
const brokenLinksForFile = brokenLinksByFile[filename];
|
|
33
|
-
if (brokenLinksForFile) {
|
|
34
|
-
brokenLinksForFile.push(mdxPath.originalPath);
|
|
35
|
-
} else {
|
|
36
|
-
brokenLinksByFile[filename] = [mdxPath.originalPath];
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Display broken links with highlighted numbers
|
|
41
|
-
const fileCount = Object.keys(brokenLinksByFile).length;
|
|
42
|
-
this.logger.log(
|
|
43
|
-
`found ${this.logger.highlight?.(brokenLinks.length.toString()) ?? brokenLinks.length} broken link${brokenLinks.length === 1 ? '' : 's'} in ${this.logger.highlight?.(fileCount.toString()) ?? fileCount} file${fileCount === 1 ? '' : 's'}`
|
|
44
|
-
);
|
|
45
|
-
this.logger.logNewLine();
|
|
46
|
-
|
|
47
|
-
for (const [filename, links] of Object.entries(brokenLinksByFile)) {
|
|
48
|
-
// Underline filename for better visibility
|
|
49
|
-
this.logger.logColor(filename, 'cyan');
|
|
50
|
-
links.forEach((link) => {
|
|
51
|
-
this.logger.logColor(` ⎿ ${link}`, 'gray');
|
|
52
|
-
});
|
|
53
|
-
this.logger.logNewLine();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return brokenLinksByFile;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Rename a file and update all internal link references
|
|
61
|
-
*/
|
|
62
|
-
async renameFile(from: string, to: string, force: boolean = false): Promise<void> {
|
|
63
|
-
await renameFilesAndUpdateLinksInContent(from, to, force);
|
|
64
|
-
}
|
|
65
|
-
}
|