@poetora/cli 0.0.1 → 0.1.2
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +93 -0
- package/bin/accessibility.js +2 -2
- package/bin/cli-builder.d.ts +8 -0
- package/bin/cli-builder.js +178 -0
- package/bin/cli.d.ts +5 -11
- package/bin/cli.js +8 -200
- package/bin/commands/base.command.d.ts +13 -0
- package/bin/commands/base.command.js +40 -0
- package/bin/commands/check.command.d.ts +14 -0
- package/bin/commands/check.command.js +21 -0
- package/bin/commands/dev.command.d.ts +13 -0
- package/bin/commands/dev.command.js +40 -0
- package/bin/commands/index.d.ts +6 -0
- package/bin/commands/index.js +6 -0
- package/bin/commands/init.command.d.ts +16 -0
- package/bin/commands/init.command.js +88 -0
- package/bin/commands/link.command.d.ts +13 -0
- package/bin/commands/link.command.js +19 -0
- package/bin/commands/update.command.d.ts +10 -0
- package/bin/commands/update.command.js +13 -0
- package/bin/constants.js +1 -1
- package/bin/errors/cli-error.d.ts +26 -0
- package/bin/errors/cli-error.js +53 -0
- package/bin/errors/index.d.ts +1 -0
- package/bin/errors/index.js +1 -0
- package/bin/index.d.ts +1 -1
- package/bin/index.js +6 -6
- package/bin/mdxAccessibility.js +2 -2
- package/bin/services/accessibility-check.service.d.ts +10 -0
- package/bin/services/accessibility-check.service.js +144 -0
- package/bin/services/index.d.ts +7 -0
- package/bin/services/index.js +7 -0
- package/bin/services/link.service.d.ts +7 -0
- package/bin/services/link.service.js +40 -0
- package/bin/services/openapi-check.service.d.ts +7 -0
- package/bin/services/openapi-check.service.js +43 -0
- package/bin/services/port.service.d.ts +7 -0
- package/bin/services/port.service.js +26 -0
- package/bin/services/template.service.d.ts +22 -0
- package/bin/services/template.service.js +127 -0
- package/bin/services/update.service.d.ts +10 -0
- package/bin/services/update.service.js +57 -0
- package/bin/services/version.service.d.ts +16 -0
- package/bin/services/version.service.js +102 -0
- package/bin/types/common.d.ts +38 -0
- package/bin/types/common.js +21 -0
- package/bin/types/index.d.ts +2 -0
- package/bin/types/index.js +2 -0
- package/bin/types/options.d.ts +23 -0
- package/bin/types/options.js +1 -0
- package/bin/utils/console-logger.d.ts +16 -0
- package/bin/utils/console-logger.js +65 -0
- package/bin/utils/index.d.ts +2 -0
- package/bin/utils/index.js +2 -0
- package/bin/utils/logger.interface.d.ts +15 -0
- package/bin/utils/logger.interface.js +1 -0
- package/package.json +29 -29
- package/src/accessibility.ts +2 -2
- package/src/cli-builder.ts +267 -0
- package/src/cli.ts +15 -0
- package/src/commands/__tests__/base.command.test.ts +145 -0
- package/src/commands/__tests__/dev.command.test.ts +241 -0
- package/src/commands/__tests__/init.command.test.ts +281 -0
- package/{__test__ → src/commands/__tests__}/utils.ts +1 -1
- package/src/commands/base.command.ts +97 -0
- package/src/commands/check.command.ts +40 -0
- package/src/commands/dev.command.ts +63 -0
- package/src/commands/index.ts +6 -0
- package/src/commands/init.command.ts +125 -0
- package/src/commands/link.command.ts +39 -0
- package/src/commands/update.command.ts +23 -0
- package/src/constants.ts +1 -1
- package/src/errors/cli-error.ts +83 -0
- package/src/errors/index.ts +1 -0
- package/src/index.ts +6 -6
- package/src/mdxAccessibility.ts +3 -4
- package/src/services/__tests__/port.service.test.ts +83 -0
- package/src/services/__tests__/template.service.test.ts +234 -0
- package/src/services/__tests__/version.service.test.ts +165 -0
- package/src/services/accessibility-check.service.ts +226 -0
- package/src/services/index.ts +7 -0
- package/src/services/link.service.ts +65 -0
- package/src/services/openapi-check.service.ts +68 -0
- package/src/services/port.service.ts +47 -0
- package/src/services/template.service.ts +203 -0
- package/src/services/update.service.ts +76 -0
- package/src/services/version.service.ts +161 -0
- package/src/types/common.ts +53 -0
- package/src/types/index.ts +2 -0
- package/src/types/options.ts +42 -0
- package/src/utils/console-logger.ts +114 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/logger.interface.ts +70 -0
- package/tsconfig.build.json +2 -1
- package/tsconfig.json +1 -1
- package/.prettierignore +0 -2
- package/__test__/brokenLinks.test.ts +0 -93
- package/__test__/checkPort.test.ts +0 -92
- package/__test__/openApiCheck.test.ts +0 -127
- package/__test__/update.test.ts +0 -108
- package/bin/accessibilityCheck.d.ts +0 -2
- package/bin/accessibilityCheck.js +0 -70
- package/bin/helpers.d.ts +0 -17
- package/bin/helpers.js +0 -104
- package/bin/init.d.ts +0 -1
- package/bin/init.js +0 -73
- package/bin/mdxLinter.d.ts +0 -2
- package/bin/mdxLinter.js +0 -45
- package/bin/update.d.ts +0 -3
- package/bin/update.js +0 -32
- package/src/accessibilityCheck.tsx +0 -145
- package/src/cli.tsx +0 -302
- package/src/helpers.tsx +0 -131
- package/src/init.tsx +0 -93
- package/src/mdxLinter.tsx +0 -88
- package/src/update.tsx +0 -37
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { dev } from '@poetora/previewing';
|
|
2
|
+
import { InvalidEnvironmentError } from '../errors/index.js';
|
|
3
|
+
import { BaseCommand } from './base.command.js';
|
|
4
|
+
export class DevCommand extends BaseCommand {
|
|
5
|
+
versionService;
|
|
6
|
+
portService;
|
|
7
|
+
name = 'dev';
|
|
8
|
+
description = 'initialize a local preview environment';
|
|
9
|
+
constructor(logger, versionService, portService, packageName = 'poet') {
|
|
10
|
+
super(logger, packageName);
|
|
11
|
+
this.versionService = versionService;
|
|
12
|
+
this.portService = portService;
|
|
13
|
+
}
|
|
14
|
+
async validate(_options) {
|
|
15
|
+
const versionResult = this.versionService.checkNodeVersion();
|
|
16
|
+
if (!versionResult.isValid) {
|
|
17
|
+
throw new InvalidEnvironmentError(versionResult.message ?? 'Unsupported Node.js version');
|
|
18
|
+
}
|
|
19
|
+
if (versionResult.hasWarning && versionResult.message) {
|
|
20
|
+
this.logger.warn(versionResult.message);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async execute(options) {
|
|
24
|
+
const port = await this.portService.findAvailablePort(options.port);
|
|
25
|
+
const cliVersion = this.versionService.getCliVersion();
|
|
26
|
+
const devArgs = {
|
|
27
|
+
_: [],
|
|
28
|
+
$0: this.packageName,
|
|
29
|
+
port,
|
|
30
|
+
open: options.open ?? true,
|
|
31
|
+
localSchema: options.localSchema ?? false,
|
|
32
|
+
clientVersion: options.clientVersion,
|
|
33
|
+
groups: options.groups,
|
|
34
|
+
disableOpenapi: options.disableOpenapi ?? false,
|
|
35
|
+
packageName: this.packageName,
|
|
36
|
+
cliVersion,
|
|
37
|
+
};
|
|
38
|
+
await dev(devArgs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TemplateService } from '../services/template.service.js';
|
|
2
|
+
import type { InitOptions } from '../types/index.js';
|
|
3
|
+
import type { ILogger } from '../utils/index.js';
|
|
4
|
+
import { BaseCommand } from './base.command.js';
|
|
5
|
+
export declare class InitCommand extends BaseCommand {
|
|
6
|
+
private readonly templateService;
|
|
7
|
+
readonly name = "init";
|
|
8
|
+
readonly description = "Create a new Poetora documentation site";
|
|
9
|
+
constructor(logger: ILogger, templateService: TemplateService, packageName?: string);
|
|
10
|
+
protected execute(options: InitOptions): Promise<void>;
|
|
11
|
+
private promptDirectoryChoice;
|
|
12
|
+
private promptSubdirectoryName;
|
|
13
|
+
private promptProjectName;
|
|
14
|
+
private promptTheme;
|
|
15
|
+
private showOnboardingMessage;
|
|
16
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { input, select } from '@inquirer/prompts';
|
|
2
|
+
import { ValidationError } from '../errors/index.js';
|
|
3
|
+
import { BaseCommand } from './base.command.js';
|
|
4
|
+
export class InitCommand extends BaseCommand {
|
|
5
|
+
templateService;
|
|
6
|
+
name = 'init';
|
|
7
|
+
description = 'Create a new Poetora documentation site';
|
|
8
|
+
constructor(logger, templateService, packageName = 'poet') {
|
|
9
|
+
super(logger, packageName);
|
|
10
|
+
this.templateService = templateService;
|
|
11
|
+
}
|
|
12
|
+
async execute(options) {
|
|
13
|
+
let installDir = options.directory;
|
|
14
|
+
const dirStatus = await this.templateService.checkDirectory(installDir);
|
|
15
|
+
if (dirStatus.exists && dirStatus.hasContents) {
|
|
16
|
+
const choice = await this.promptDirectoryChoice(installDir);
|
|
17
|
+
if (choice === 'cancel') {
|
|
18
|
+
this.logger.info('Installation cancelled');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (choice === 'subdir') {
|
|
22
|
+
const subdir = await this.promptSubdirectoryName();
|
|
23
|
+
installDir = installDir === '.' ? subdir : `${installDir}/${subdir}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const projectName = await this.promptProjectName(installDir);
|
|
27
|
+
const theme = await this.promptTheme();
|
|
28
|
+
this.logger.info('Setting up documentation project...');
|
|
29
|
+
await this.templateService.installTemplate({
|
|
30
|
+
directory: installDir,
|
|
31
|
+
projectName,
|
|
32
|
+
theme,
|
|
33
|
+
});
|
|
34
|
+
this.showOnboardingMessage(installDir);
|
|
35
|
+
}
|
|
36
|
+
async promptDirectoryChoice(directory) {
|
|
37
|
+
const choice = await select({
|
|
38
|
+
message: `Directory ${directory} is not empty. What would you like to do?`,
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: 'Create in a subdirectory', value: 'subdir' },
|
|
41
|
+
{ name: 'Overwrite current directory (may lose contents)', value: 'overwrite' },
|
|
42
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
return choice;
|
|
46
|
+
}
|
|
47
|
+
async promptSubdirectoryName() {
|
|
48
|
+
const subdir = await input({
|
|
49
|
+
message: 'Subdirectory name:',
|
|
50
|
+
default: 'docs',
|
|
51
|
+
});
|
|
52
|
+
if (!subdir || subdir.trim() === '') {
|
|
53
|
+
throw new ValidationError('Subdirectory name cannot be empty');
|
|
54
|
+
}
|
|
55
|
+
return subdir.trim();
|
|
56
|
+
}
|
|
57
|
+
async promptProjectName(installDir) {
|
|
58
|
+
const defaultProject = installDir === '.' ? 'Poetora' : installDir;
|
|
59
|
+
const projectName = await input({
|
|
60
|
+
message: 'Project Name',
|
|
61
|
+
default: defaultProject,
|
|
62
|
+
});
|
|
63
|
+
return projectName || defaultProject;
|
|
64
|
+
}
|
|
65
|
+
async promptTheme() {
|
|
66
|
+
const themes = this.templateService.getAvailableThemes();
|
|
67
|
+
const theme = await select({
|
|
68
|
+
message: 'Theme',
|
|
69
|
+
choices: themes.map((t) => ({
|
|
70
|
+
name: t,
|
|
71
|
+
value: t,
|
|
72
|
+
})),
|
|
73
|
+
});
|
|
74
|
+
return theme;
|
|
75
|
+
}
|
|
76
|
+
showOnboardingMessage(installDir) {
|
|
77
|
+
this.logger.log('');
|
|
78
|
+
this.logger.success('Documentation Setup!');
|
|
79
|
+
this.logger.log('');
|
|
80
|
+
this.logger.log('To see your docs run:');
|
|
81
|
+
this.logger.log('');
|
|
82
|
+
if (installDir !== '.') {
|
|
83
|
+
this.logger.log(` cd ${installDir}`);
|
|
84
|
+
}
|
|
85
|
+
this.logger.log(` ${this.packageName} dev`);
|
|
86
|
+
this.logger.log('');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LinkService } from '../services/index.js';
|
|
2
|
+
import type { RenameOptions } from '../types/index.js';
|
|
3
|
+
import type { ILogger } from '../utils/index.js';
|
|
4
|
+
import { BaseCommand } from './base.command.js';
|
|
5
|
+
export declare class LinkCommand extends BaseCommand {
|
|
6
|
+
private readonly linkService;
|
|
7
|
+
readonly name = "link";
|
|
8
|
+
readonly description = "manage documentation links";
|
|
9
|
+
constructor(logger: ILogger, linkService: LinkService, packageName?: string);
|
|
10
|
+
checkBrokenLinks(): Promise<Record<string, string[]>>;
|
|
11
|
+
renameFile(options: RenameOptions): Promise<void>;
|
|
12
|
+
protected execute(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseCommand } from './base.command.js';
|
|
2
|
+
export class LinkCommand extends BaseCommand {
|
|
3
|
+
linkService;
|
|
4
|
+
name = 'link';
|
|
5
|
+
description = 'manage documentation links';
|
|
6
|
+
constructor(logger, linkService, packageName = 'poet') {
|
|
7
|
+
super(logger, packageName);
|
|
8
|
+
this.linkService = linkService;
|
|
9
|
+
}
|
|
10
|
+
async checkBrokenLinks() {
|
|
11
|
+
return await this.linkService.checkBrokenLinks();
|
|
12
|
+
}
|
|
13
|
+
async renameFile(options) {
|
|
14
|
+
await this.linkService.renameFile(options.from, options.to, options.force);
|
|
15
|
+
}
|
|
16
|
+
async execute() {
|
|
17
|
+
await this.linkService.checkBrokenLinks();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { UpdateService } from '../services/index.js';
|
|
2
|
+
import type { ILogger } from '../utils/index.js';
|
|
3
|
+
import { BaseCommand } from './base.command.js';
|
|
4
|
+
export declare class UpdateCommand extends BaseCommand {
|
|
5
|
+
private readonly updateService;
|
|
6
|
+
readonly name = "update";
|
|
7
|
+
readonly description = "update the CLI to the latest version";
|
|
8
|
+
constructor(logger: ILogger, updateService: UpdateService, packageName?: string);
|
|
9
|
+
protected execute(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from './base.command.js';
|
|
2
|
+
export class UpdateCommand extends BaseCommand {
|
|
3
|
+
updateService;
|
|
4
|
+
name = 'update';
|
|
5
|
+
description = 'update the CLI to the latest version';
|
|
6
|
+
constructor(logger, updateService, packageName = 'poet') {
|
|
7
|
+
super(logger, packageName);
|
|
8
|
+
this.updateService = updateService;
|
|
9
|
+
}
|
|
10
|
+
async execute() {
|
|
11
|
+
await this.updateService.update();
|
|
12
|
+
}
|
|
13
|
+
}
|
package/bin/constants.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class CliError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly exitCode: number;
|
|
4
|
+
constructor(message: string, code: string, exitCode?: number);
|
|
5
|
+
}
|
|
6
|
+
export declare class InvalidEnvironmentError extends CliError {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class NoAvailablePortError extends CliError {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class ConfigNotFoundError extends CliError {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class ValidationError extends CliError {
|
|
16
|
+
readonly field?: string | undefined;
|
|
17
|
+
constructor(message: string, field?: string | undefined);
|
|
18
|
+
}
|
|
19
|
+
export declare class FileSystemError extends CliError {
|
|
20
|
+
readonly filePath?: string | undefined;
|
|
21
|
+
constructor(message: string, filePath?: string | undefined);
|
|
22
|
+
}
|
|
23
|
+
export declare class ExternalServiceError extends CliError {
|
|
24
|
+
readonly service?: string | undefined;
|
|
25
|
+
constructor(message: string, service?: string | undefined);
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class CliError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(message, code, exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.exitCode = exitCode;
|
|
8
|
+
this.name = 'CliError';
|
|
9
|
+
Error.captureStackTrace(this, this.constructor);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class InvalidEnvironmentError extends CliError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message, 'INVALID_ENVIRONMENT', 1);
|
|
15
|
+
this.name = 'InvalidEnvironmentError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class NoAvailablePortError extends CliError {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message, 'NO_AVAILABLE_PORT', 1);
|
|
21
|
+
this.name = 'NoAvailablePortError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ConfigNotFoundError extends CliError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message, 'CONFIG_NOT_FOUND', 1);
|
|
27
|
+
this.name = 'ConfigNotFoundError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ValidationError extends CliError {
|
|
31
|
+
field;
|
|
32
|
+
constructor(message, field) {
|
|
33
|
+
super(message, 'VALIDATION_ERROR', 1);
|
|
34
|
+
this.field = field;
|
|
35
|
+
this.name = 'ValidationError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class FileSystemError extends CliError {
|
|
39
|
+
filePath;
|
|
40
|
+
constructor(message, filePath) {
|
|
41
|
+
super(message, 'FILE_SYSTEM_ERROR', 1);
|
|
42
|
+
this.filePath = filePath;
|
|
43
|
+
this.name = 'FileSystemError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class ExternalServiceError extends CliError {
|
|
47
|
+
service;
|
|
48
|
+
constructor(message, service) {
|
|
49
|
+
super(message, 'EXTERNAL_SERVICE_ERROR', 1);
|
|
50
|
+
this.service = service;
|
|
51
|
+
this.name = 'ExternalServiceError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './cli-error.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './cli-error.js';
|
package/bin/index.d.ts
CHANGED
package/bin/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'child_process';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
const packageName = path.basename(process.argv[1] ?? '') === 'index'
|
|
@@ -24,13 +24,13 @@ const cleanup = async () => {
|
|
|
24
24
|
}
|
|
25
25
|
resolve();
|
|
26
26
|
}, 5000);
|
|
27
|
-
cli
|
|
27
|
+
cli?.once('exit', () => {
|
|
28
28
|
clearTimeout(timeout);
|
|
29
29
|
resolve();
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
-
catch (
|
|
33
|
+
catch (_error) {
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
};
|
|
@@ -85,7 +85,7 @@ process.on('exit', () => {
|
|
|
85
85
|
try {
|
|
86
86
|
cli.kill('SIGKILL');
|
|
87
87
|
}
|
|
88
|
-
catch (
|
|
88
|
+
catch (_error) {
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
});
|
package/bin/mdxAccessibility.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { categorizeFilePaths, getPoetIgnore } from '@poetora/prebuild';
|
|
2
4
|
import { coreRemark } from '@poetora/shared';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
5
|
import { visit } from 'unist-util-visit';
|
|
6
6
|
const checkAltAttributes = (filePath, content) => {
|
|
7
7
|
const issues = [];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ILogger } from '../utils/logger.interface.js';
|
|
2
|
+
export declare class AccessibilityCheckService {
|
|
3
|
+
private readonly logger;
|
|
4
|
+
constructor(logger?: ILogger);
|
|
5
|
+
private displayContrastResult;
|
|
6
|
+
private colorize;
|
|
7
|
+
private checkColorAccessibility;
|
|
8
|
+
private checkMdxAccessibility;
|
|
9
|
+
checkAccessibility(): Promise<number>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { getConfigObj, getConfigPath } from '@poetora/prebuild';
|
|
3
|
+
import { getBackgroundColors } from '@poetora/shared';
|
|
4
|
+
import { checkDocsColors, } from '../accessibility.js';
|
|
5
|
+
import { CMD_EXEC_PATH } from '../constants.js';
|
|
6
|
+
import { checkMdxAccessibility } from '../mdxAccessibility.js';
|
|
7
|
+
import { ConsoleLogger } from '../utils/console-logger.js';
|
|
8
|
+
export class AccessibilityCheckService {
|
|
9
|
+
logger;
|
|
10
|
+
constructor(logger = new ConsoleLogger()) {
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
displayContrastResult(result, label, prefix = '') {
|
|
14
|
+
if (!result)
|
|
15
|
+
return;
|
|
16
|
+
const { recommendation } = result;
|
|
17
|
+
let statusText;
|
|
18
|
+
let detailMessage;
|
|
19
|
+
if (recommendation === 'pass') {
|
|
20
|
+
if (result.meetsAAA) {
|
|
21
|
+
statusText = '✓ Excellent';
|
|
22
|
+
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (AAA standard)`;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
statusText = '✓ Good';
|
|
26
|
+
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (AA standard)`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (recommendation === 'warning') {
|
|
30
|
+
statusText = '⚠ Acceptable';
|
|
31
|
+
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (consider improving)`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
statusText = '✗ Poor';
|
|
35
|
+
detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (below AA standard)`;
|
|
36
|
+
}
|
|
37
|
+
const labelText = `${prefix}${label}:`;
|
|
38
|
+
const statusColor = recommendation === 'pass' ? 'green' : recommendation === 'warning' ? 'yellow' : 'red';
|
|
39
|
+
this.logger.log(`${labelText} ${this.colorize(statusText, statusColor)} ${detailMessage}`);
|
|
40
|
+
}
|
|
41
|
+
colorize(text, color) {
|
|
42
|
+
const chalk = (text, color) => {
|
|
43
|
+
const colors = {
|
|
44
|
+
green: '\x1b[32m',
|
|
45
|
+
yellow: '\x1b[33m',
|
|
46
|
+
red: '\x1b[31m',
|
|
47
|
+
reset: '\x1b[0m',
|
|
48
|
+
};
|
|
49
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
50
|
+
};
|
|
51
|
+
return chalk(text, color);
|
|
52
|
+
}
|
|
53
|
+
async checkColorAccessibility() {
|
|
54
|
+
try {
|
|
55
|
+
const docsConfigPath = await getConfigPath(CMD_EXEC_PATH);
|
|
56
|
+
if (!docsConfigPath) {
|
|
57
|
+
this.logger.error('No configuration file found. Please run this command from a directory with a docs.json file.');
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const config = await getConfigObj(CMD_EXEC_PATH);
|
|
61
|
+
if (!config?.colors) {
|
|
62
|
+
this.logger.warn('No colors section found in configuration file');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
const { colors, navigation } = config;
|
|
66
|
+
const { lightHex, darkHex } = getBackgroundColors(config);
|
|
67
|
+
const results = checkDocsColors(colors, { lightHex, darkHex }, navigation);
|
|
68
|
+
this.logger.log('Checking color accessibility...');
|
|
69
|
+
this.displayContrastResult(results.primaryContrast, `Primary Color (${colors.primary}) vs Light Background`);
|
|
70
|
+
this.displayContrastResult(results.lightContrast, `Light Color (${colors.light}) vs Dark Background`);
|
|
71
|
+
this.displayContrastResult(results.darkContrast, `Dark Color (${colors.dark}) vs Dark Background`);
|
|
72
|
+
this.displayContrastResult(results.darkOnLightContrast, `Dark Color (${colors.dark}) vs Light Background`);
|
|
73
|
+
const anchorsWithResults = results.anchorResults.filter((anchor) => anchor.lightContrast || anchor.darkContrast);
|
|
74
|
+
if (anchorsWithResults.length > 0) {
|
|
75
|
+
for (const anchor of anchorsWithResults) {
|
|
76
|
+
this.displayContrastResult(anchor.lightContrast, `${anchor.name} vs Light Background`);
|
|
77
|
+
this.displayContrastResult(anchor.darkContrast, `${anchor.name} vs Dark Background`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let overallDisplay;
|
|
81
|
+
if (results.overallScore === 'fail') {
|
|
82
|
+
overallDisplay =
|
|
83
|
+
this.colorize('✗ Action needed:', 'red') +
|
|
84
|
+
' Update colors to meet accessibility standards';
|
|
85
|
+
}
|
|
86
|
+
else if (results.overallScore === 'warning') {
|
|
87
|
+
overallDisplay = `${this.colorize('⚠ Room for improvement:', 'yellow')} Consider enhancing color contrast`;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
overallDisplay = `${this.colorize('✓ All good:', 'green')} Colors meet accessibility guidelines`;
|
|
91
|
+
}
|
|
92
|
+
this.logger.logNewLine();
|
|
93
|
+
this.logger.log(overallDisplay);
|
|
94
|
+
return results.overallScore === 'fail' ? 1 : 0;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
this.logger.error(`Failed to check color accessibility: ${error}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async checkMdxAccessibility() {
|
|
102
|
+
try {
|
|
103
|
+
this.logger.log('Checking mdx files for accessibility issues...');
|
|
104
|
+
const results = await checkMdxAccessibility();
|
|
105
|
+
if (results.missingAltAttributes.length === 0) {
|
|
106
|
+
this.logger.success('no accessibility issues found');
|
|
107
|
+
this.logger.info(`Checked ${results.totalFiles} MDX files - all images and videos have alt attributes.`);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
const issuesByFile = {};
|
|
111
|
+
results.missingAltAttributes.forEach((issue) => {
|
|
112
|
+
if (!issuesByFile[issue.filePath]) {
|
|
113
|
+
issuesByFile[issue.filePath] = [];
|
|
114
|
+
}
|
|
115
|
+
issuesByFile[issue.filePath]?.push(issue);
|
|
116
|
+
});
|
|
117
|
+
this.logger.log(`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'}:`);
|
|
118
|
+
for (const [filePath, issues] of Object.entries(issuesByFile)) {
|
|
119
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
120
|
+
this.logger.log(`${relativePath}:`);
|
|
121
|
+
for (const issue of issues) {
|
|
122
|
+
const location = issue.line && issue.column ? ` (line ${issue.line}, col ${issue.column})` : '';
|
|
123
|
+
if (issue.element === 'a') {
|
|
124
|
+
this.logger.logColor(` ✗ Missing text attribute ${issue.tagName} element${location}`, 'red');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.logger.logColor(` ✗ Missing alt attribute on ${issue.tagName} element${location}`, 'red');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.logger.warn('Recommendation: Add alt attributes to all images and videos for better accessibility.');
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
this.logger.error(`Failed to check MDX accessibility: ${error}`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async checkAccessibility() {
|
|
140
|
+
const colorCheckCode = await this.checkColorAccessibility();
|
|
141
|
+
const mdxCheckCode = await this.checkMdxAccessibility();
|
|
142
|
+
return colorCheckCode || mdxCheckCode;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
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';
|
|
@@ -0,0 +1,7 @@
|
|
|
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';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ILogger } from '../utils/index.js';
|
|
2
|
+
export declare class LinkService {
|
|
3
|
+
private readonly logger;
|
|
4
|
+
constructor(logger: ILogger);
|
|
5
|
+
checkBrokenLinks(): Promise<Record<string, string[]>>;
|
|
6
|
+
renameFile(from: string, to: string, force?: boolean): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { getBrokenInternalLinks, renameFilesAndUpdateLinksInContent, } from '@poetora/link-rot';
|
|
3
|
+
export class LinkService {
|
|
4
|
+
logger;
|
|
5
|
+
constructor(logger) {
|
|
6
|
+
this.logger = logger;
|
|
7
|
+
}
|
|
8
|
+
async checkBrokenLinks() {
|
|
9
|
+
const brokenLinks = await getBrokenInternalLinks();
|
|
10
|
+
if (brokenLinks.length === 0) {
|
|
11
|
+
this.logger.success('no broken links found');
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
const brokenLinksByFile = {};
|
|
15
|
+
brokenLinks.forEach((mdxPath) => {
|
|
16
|
+
const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
|
|
17
|
+
const brokenLinksForFile = brokenLinksByFile[filename];
|
|
18
|
+
if (brokenLinksForFile) {
|
|
19
|
+
brokenLinksForFile.push(mdxPath.originalPath);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
brokenLinksByFile[filename] = [mdxPath.originalPath];
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const fileCount = Object.keys(brokenLinksByFile).length;
|
|
26
|
+
this.logger.log(`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'}`);
|
|
27
|
+
this.logger.logNewLine();
|
|
28
|
+
for (const [filename, links] of Object.entries(brokenLinksByFile)) {
|
|
29
|
+
console.log(`\x1b[4m${filename}\x1b[0m`);
|
|
30
|
+
links.forEach((link) => {
|
|
31
|
+
this.logger.logColor(` ⎿ ${link}`, 'gray');
|
|
32
|
+
});
|
|
33
|
+
this.logger.logNewLine();
|
|
34
|
+
}
|
|
35
|
+
return brokenLinksByFile;
|
|
36
|
+
}
|
|
37
|
+
async renameFile(from, to, force = false) {
|
|
38
|
+
await renameFilesAndUpdateLinksInContent(from, to, force);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { getOpenApiDocumentFromUrl, isAllowedLocalSchemaUrl, validate } from '@poetora/shared';
|
|
3
|
+
import * as yaml from 'js-yaml';
|
|
4
|
+
import { FileSystemError, ValidationError } from '../errors/index.js';
|
|
5
|
+
export class OpenApiCheckService {
|
|
6
|
+
logger;
|
|
7
|
+
constructor(logger) {
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
async validateSpec(filename, localSchema = false) {
|
|
11
|
+
if (isAllowedLocalSchemaUrl(filename, localSchema)) {
|
|
12
|
+
await getOpenApiDocumentFromUrl(filename);
|
|
13
|
+
this.logger.success('OpenAPI definition is valid.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (filename.startsWith('http://') && !localSchema) {
|
|
17
|
+
this.logger.warn('include the --local-schema flag to check locally hosted OpenAPI files');
|
|
18
|
+
this.logger.warn('only https protocol is supported in production');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const document = await this.readLocalOpenApiFile(filename);
|
|
22
|
+
if (!document) {
|
|
23
|
+
throw new ValidationError('failed to parse OpenAPI spec: could not parse file correctly, please check for any syntax errors.');
|
|
24
|
+
}
|
|
25
|
+
await validate(document);
|
|
26
|
+
this.logger.success('OpenAPI definition is valid.');
|
|
27
|
+
}
|
|
28
|
+
async readLocalOpenApiFile(filename) {
|
|
29
|
+
try {
|
|
30
|
+
const fileContents = await fs.promises.readFile(filename, 'utf-8');
|
|
31
|
+
if (filename.endsWith('.json')) {
|
|
32
|
+
return JSON.parse(fileContents);
|
|
33
|
+
}
|
|
34
|
+
return yaml.load(fileContents);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
38
|
+
throw new FileSystemError(`file not found, please check the path provided: ${filename}`);
|
|
39
|
+
}
|
|
40
|
+
throw new ValidationError(`Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|