@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { categorizeFilePaths, getPoetIgnore } from '@poetora/prebuild';
|
|
2
|
+
import { coreRemark } from '@poetora/shared';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import type { Root, Text } from 'mdast';
|
|
5
|
+
import type { Node } from 'mdast';
|
|
6
|
+
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { visit } from 'unist-util-visit';
|
|
9
|
+
|
|
10
|
+
export interface AccessibilityFixAttribute {
|
|
11
|
+
filePath: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
column?: number;
|
|
14
|
+
element: 'img' | 'video' | 'a';
|
|
15
|
+
tagName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MdxAccessibilityResult {
|
|
19
|
+
missingAltAttributes: AccessibilityFixAttribute[];
|
|
20
|
+
totalFiles: number;
|
|
21
|
+
filesWithIssues: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const checkAltAttributes = (filePath: string, content: string): AccessibilityFixAttribute[] => {
|
|
25
|
+
const issues: AccessibilityFixAttribute[] = [];
|
|
26
|
+
|
|
27
|
+
const visitElements = () => {
|
|
28
|
+
return (tree: Root) => {
|
|
29
|
+
visit(tree, (node) => {
|
|
30
|
+
if (node.type === 'image') {
|
|
31
|
+
if (!node.alt || node.alt.trim() === '') {
|
|
32
|
+
issues.push({
|
|
33
|
+
filePath,
|
|
34
|
+
line: node.position?.start.line,
|
|
35
|
+
column: node.position?.start.column,
|
|
36
|
+
element: 'img',
|
|
37
|
+
tagName: 'image (markdown)',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mdxJsxElement = node as MdxJsxFlowElement;
|
|
44
|
+
if (mdxJsxElement.name === 'img' || mdxJsxElement.name === 'video') {
|
|
45
|
+
const altAttrIndex = mdxJsxElement.attributes.findIndex(
|
|
46
|
+
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'alt'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const altAttribute = mdxJsxElement.attributes[altAttrIndex];
|
|
50
|
+
const hasValidAlt =
|
|
51
|
+
altAttribute &&
|
|
52
|
+
typeof altAttribute.value === 'string' &&
|
|
53
|
+
altAttribute.value.trim() !== '';
|
|
54
|
+
|
|
55
|
+
if (!hasValidAlt) {
|
|
56
|
+
issues.push({
|
|
57
|
+
filePath,
|
|
58
|
+
line: node.position?.start.line,
|
|
59
|
+
column: node.position?.start.column,
|
|
60
|
+
element: mdxJsxElement.name,
|
|
61
|
+
tagName: mdxJsxElement.name,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} else if (mdxJsxElement.name === 'a') {
|
|
65
|
+
const hasTextContent = (children: Node[]): boolean => {
|
|
66
|
+
return children.some((child) => {
|
|
67
|
+
if (child.type === 'text') {
|
|
68
|
+
const textNode = child as Text;
|
|
69
|
+
return textNode.value.trim() !== '';
|
|
70
|
+
}
|
|
71
|
+
if ('children' in child && Array.isArray(child.children)) {
|
|
72
|
+
return hasTextContent(child.children as Node[]);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (!hasTextContent(mdxJsxElement.children as Node[])) {
|
|
79
|
+
issues.push({
|
|
80
|
+
filePath,
|
|
81
|
+
line: node.position?.start.line,
|
|
82
|
+
column: node.position?.start.column,
|
|
83
|
+
element: 'a',
|
|
84
|
+
tagName: '<a>',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return tree;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
coreRemark().use(visitElements).processSync(content);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn(`Warning: Could not parse ${filePath}: ${error}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return issues;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const checkMdxAccessibility = async (
|
|
103
|
+
baseDir: string = process.cwd()
|
|
104
|
+
): Promise<MdxAccessibilityResult> => {
|
|
105
|
+
const poetIgnore = await getPoetIgnore(baseDir);
|
|
106
|
+
const { contentFilenames } = await categorizeFilePaths(baseDir, poetIgnore);
|
|
107
|
+
const mdxFiles: string[] = [];
|
|
108
|
+
for (const file of contentFilenames) {
|
|
109
|
+
mdxFiles.push(path.join(baseDir, file));
|
|
110
|
+
}
|
|
111
|
+
const allIssues: AccessibilityFixAttribute[] = [];
|
|
112
|
+
const filesWithIssues = new Set<string>();
|
|
113
|
+
|
|
114
|
+
for (const filePath of mdxFiles) {
|
|
115
|
+
try {
|
|
116
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
117
|
+
const issues = checkAltAttributes(filePath, content);
|
|
118
|
+
|
|
119
|
+
if (issues.length > 0) {
|
|
120
|
+
allIssues.push(...issues);
|
|
121
|
+
filesWithIssues.add(filePath);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn(`Warning: Could not read file ${filePath}: ${error}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
missingAltAttributes: allIssues,
|
|
130
|
+
totalFiles: mdxFiles.length,
|
|
131
|
+
filesWithIssues: filesWithIssues.size,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { addLog, ErrorLog, SuccessLog } from '@poetora/previewing';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { TerminateCode } from './accessibilityCheck.js';
|
|
6
|
+
import { checkMdxAccessibility, type AccessibilityFixAttribute } from './mdxAccessibility.js';
|
|
7
|
+
|
|
8
|
+
export const mdxLinter = async (): Promise<TerminateCode> => {
|
|
9
|
+
try {
|
|
10
|
+
addLog(
|
|
11
|
+
<Text bold color="cyan">
|
|
12
|
+
Checking mdx files for accessibility issues...
|
|
13
|
+
</Text>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const results = await checkMdxAccessibility();
|
|
17
|
+
|
|
18
|
+
if (results.missingAltAttributes.length === 0) {
|
|
19
|
+
addLog(<SuccessLog message="no accessibility issues found" />);
|
|
20
|
+
addLog(
|
|
21
|
+
<Text>
|
|
22
|
+
Checked {results.totalFiles} MDX files - all images and videos have alt attributes.
|
|
23
|
+
</Text>
|
|
24
|
+
);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const issuesByFile: Record<string, AccessibilityFixAttribute[]> = {};
|
|
29
|
+
results.missingAltAttributes.forEach((issue) => {
|
|
30
|
+
if (!issuesByFile[issue.filePath]) {
|
|
31
|
+
issuesByFile[issue.filePath] = [];
|
|
32
|
+
}
|
|
33
|
+
issuesByFile[issue.filePath]?.push(issue);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
addLog(
|
|
37
|
+
<Text bold color="red">
|
|
38
|
+
Found {results.missingAltAttributes.length} accessibility issues in{' '}
|
|
39
|
+
{results.filesWithIssues} files:
|
|
40
|
+
</Text>
|
|
41
|
+
);
|
|
42
|
+
addLog(<Text></Text>);
|
|
43
|
+
|
|
44
|
+
for (const [filePath, issues] of Object.entries(issuesByFile)) {
|
|
45
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
46
|
+
addLog(<Text bold>{relativePath}:</Text>);
|
|
47
|
+
|
|
48
|
+
for (const issue of issues) {
|
|
49
|
+
const location =
|
|
50
|
+
issue.line && issue.column ? ` (line ${issue.line}, col ${issue.column})` : '';
|
|
51
|
+
if (issue.element === 'a') {
|
|
52
|
+
addLog(
|
|
53
|
+
<Text>
|
|
54
|
+
<Text color="red"> ✗</Text> Missing text attribute <Text bold>{issue.tagName}</Text>{' '}
|
|
55
|
+
element{location}
|
|
56
|
+
</Text>
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
addLog(
|
|
60
|
+
<Text>
|
|
61
|
+
<Text color="red"> ✗</Text> Missing alt attribute on <Text bold>{issue.tagName}</Text>{' '}
|
|
62
|
+
element{location}
|
|
63
|
+
</Text>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
addLog(<Text></Text>);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
addLog(
|
|
71
|
+
<Text color="yellow">
|
|
72
|
+
<Text bold>Recommendation:</Text> Add alt attributes to all images and videos for better
|
|
73
|
+
accessibility.
|
|
74
|
+
</Text>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return 1;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
addLog(
|
|
80
|
+
<ErrorLog
|
|
81
|
+
message={`MDX accessibility check failed: ${
|
|
82
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
83
|
+
}`}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
};
|
package/src/start.ts
ADDED
package/src/update.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SpinnerLog, SuccessLog, ErrorLog, addLog, clearLogs } from '@poetora/previewing';
|
|
2
|
+
|
|
3
|
+
import { execAsync, getLatestCliVersion, getVersions, detectPackageManager } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
export const update = async ({ packageName }: { packageName: string }) => {
|
|
6
|
+
addLog(<SpinnerLog message="updating..." />);
|
|
7
|
+
const { cli: existingCliVersion } = getVersions();
|
|
8
|
+
const latestCliVersion = getLatestCliVersion(packageName);
|
|
9
|
+
const isUpToDate =
|
|
10
|
+
existingCliVersion && latestCliVersion && latestCliVersion.trim() === existingCliVersion.trim();
|
|
11
|
+
|
|
12
|
+
if (isUpToDate) {
|
|
13
|
+
addLog(<SuccessLog message="already up to date" />);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (existingCliVersion && latestCliVersion.trim() !== existingCliVersion.trim()) {
|
|
18
|
+
try {
|
|
19
|
+
clearLogs();
|
|
20
|
+
addLog(<SpinnerLog message={`updating ${packageName} package...`} />);
|
|
21
|
+
const packageManager = await detectPackageManager({ packageName });
|
|
22
|
+
if (packageManager === 'pnpm') {
|
|
23
|
+
await execAsync(`pnpm install -g ${packageName}@latest --silent`);
|
|
24
|
+
} else {
|
|
25
|
+
await execAsync(`npm install -g ${packageName}@latest --silent`);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
addLog(<ErrorLog message={`failed to update ${packageName}@latest`} />);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clearLogs();
|
|
34
|
+
addLog(
|
|
35
|
+
<SuccessLog message={`updated ${packageName} to the latest version: ${latestCliVersion}`} />
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"include": ["src/**/*"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"noEmit": false,
|
|
6
|
+
"outDir": "./bin",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noUnusedLocals": false,
|
|
9
|
+
"noUnusedParameters": false,
|
|
10
|
+
"sourceMap": false,
|
|
11
|
+
"inlineSourceMap": false,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": false,
|
|
14
|
+
"inlineSources": false
|
|
15
|
+
}
|
|
16
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./bin",
|
|
5
|
+
"baseUrl": "./src",
|
|
6
|
+
"paths": {
|
|
7
|
+
"@/*": ["*"]
|
|
8
|
+
},
|
|
9
|
+
"sourceMap": false,
|
|
10
|
+
"removeComments": true,
|
|
11
|
+
"preserveConstEnums": true,
|
|
12
|
+
"types": ["vitest/globals"],
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"jsxImportSource": "react",
|
|
15
|
+
"allowJs": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"verbatimModuleSyntax": false,
|
|
18
|
+
"noImplicitReturns": false
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "__test__/**/*.ts"]
|
|
21
|
+
}
|