@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/bin/cli.js ADDED
@@ -0,0 +1,201 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getBrokenInternalLinks, renameFilesAndUpdateLinksInContent } from '@poetora/link-rot';
3
+ import { addLog, dev, ErrorLog, SpinnerLog, SuccessLog, Logs, clearLogs, BrokenLinksLog, WarningLog, } from '@poetora/previewing';
4
+ import { validate, getOpenApiDocumentFromUrl, isAllowedLocalSchemaUrl } from '@poetora/shared';
5
+ import { render, Text } from 'ink';
6
+ import path from 'path';
7
+ import yargs from 'yargs';
8
+ import { hideBin } from 'yargs/helpers';
9
+ import { accessibilityCheck } from './accessibilityCheck.js';
10
+ import { checkPort, checkNodeVersion, getVersions, suppressConsoleWarnings, terminate, readLocalOpenApiFile, } from './helpers.js';
11
+ import { init } from './init.js';
12
+ import { mdxLinter } from './mdxLinter.js';
13
+ import { update } from './update.js';
14
+ export const cli = ({ packageName = 'poet' }) => {
15
+ render(_jsx(Logs, {}));
16
+ return (yargs(hideBin(process.argv))
17
+ .scriptName(packageName)
18
+ .middleware(checkNodeVersion)
19
+ .middleware(suppressConsoleWarnings)
20
+ .command('dev', 'initialize a local preview environment', (yargs) => yargs
21
+ .option('open', {
22
+ type: 'boolean',
23
+ default: true,
24
+ description: 'open a local preview in the browser',
25
+ })
26
+ .option('local-schema', {
27
+ type: 'boolean',
28
+ default: false,
29
+ hidden: true,
30
+ description: 'use a locally hosted schema file (note: only https protocol is supported in production)',
31
+ })
32
+ .option('client-version', {
33
+ type: 'string',
34
+ hidden: true,
35
+ description: 'the version of the client to use for cli testing',
36
+ })
37
+ .option('groups', {
38
+ type: 'array',
39
+ description: 'Mock user groups for local development and testing',
40
+ example: '--groups admin user',
41
+ })
42
+ .option('disable-openapi', {
43
+ type: 'boolean',
44
+ default: false,
45
+ description: 'Disable OpenAPI file generation',
46
+ })
47
+ .usage('usage: poetora dev [options]')
48
+ .example('poetora dev', 'run with default settings (opens in browser)')
49
+ .example('poetora dev --no-open', 'run without opening in browser'), async (argv) => {
50
+ let nodeVersionString = process.version;
51
+ if (nodeVersionString.charAt(0) === 'v') {
52
+ nodeVersionString = nodeVersionString.slice(1);
53
+ }
54
+ const versionArr = nodeVersionString.split('.');
55
+ const majorVersion = parseInt(versionArr[0], 10);
56
+ const minorVersion = parseInt(versionArr[1], 10);
57
+ if (majorVersion >= 25) {
58
+ addLog(_jsx(ErrorLog, { message: "poet dev is not supported on node versions 25+. Please downgrade to an LTS node version." }));
59
+ await terminate(1);
60
+ }
61
+ if (majorVersion < 20 || (majorVersion === 20 && minorVersion < 17)) {
62
+ addLog(_jsx(ErrorLog, { message: "poet dev is not supported on node versions below 20.17 Please upgrade to an LTS node version." }));
63
+ await terminate(1);
64
+ }
65
+ const port = await checkPort(argv);
66
+ const { cli: cliVersion } = getVersions();
67
+ if (port != undefined) {
68
+ await dev({
69
+ ...argv,
70
+ port,
71
+ packageName,
72
+ cliVersion,
73
+ });
74
+ }
75
+ else {
76
+ addLog(_jsx(ErrorLog, { message: "no available port found" }));
77
+ await terminate(1);
78
+ }
79
+ })
80
+ .command('openapi-check <filename>', 'check if an OpenAPI spec is valid', (yargs) => yargs
81
+ .positional('filename', {
82
+ describe: 'the filename of the OpenAPI spec (e.g. ./openapi.yaml) or the URL to the OpenAPI spec (e.g. https://petstore3.swagger.io/api/v3/openapi.json)',
83
+ type: 'string',
84
+ demandOption: true,
85
+ })
86
+ .option('local-schema', {
87
+ type: 'boolean',
88
+ default: false,
89
+ description: 'use a locally hosted schema file (note: only https protocol is supported in production)',
90
+ }), async ({ filename, 'local-schema': localSchema }) => {
91
+ try {
92
+ if (isAllowedLocalSchemaUrl(filename, localSchema)) {
93
+ await getOpenApiDocumentFromUrl(filename);
94
+ addLog(_jsx(SuccessLog, { message: "OpenAPI definition is valid." }));
95
+ await terminate(0);
96
+ }
97
+ if (filename.startsWith('http://') && !localSchema) {
98
+ addLog(_jsx(WarningLog, { message: "include the --local-schema flag to check locally hosted OpenAPI files" }));
99
+ addLog(_jsx(WarningLog, { message: "only https protocol is supported in production" }));
100
+ await terminate(0);
101
+ }
102
+ const document = await readLocalOpenApiFile(filename);
103
+ if (!document) {
104
+ throw new Error('failed to parse OpenAPI spec: could not parse file correctly, please check for any syntax errors.');
105
+ }
106
+ await validate(document);
107
+ addLog(_jsx(SuccessLog, { message: "OpenAPI definition is valid." }));
108
+ }
109
+ catch (err) {
110
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
111
+ addLog(_jsx(ErrorLog, { message: `file not found, please check the path provided: ${filename}` }));
112
+ }
113
+ else {
114
+ addLog(_jsx(ErrorLog, { message: err instanceof Error ? err.message : 'unknown error' }));
115
+ }
116
+ await terminate(1);
117
+ }
118
+ await terminate(0);
119
+ })
120
+ .command('broken-links', 'check for invalid internal links', () => undefined, async () => {
121
+ addLog(_jsx(SpinnerLog, { message: "checking for broken links..." }));
122
+ try {
123
+ const brokenLinks = await getBrokenInternalLinks();
124
+ if (brokenLinks.length === 0) {
125
+ clearLogs();
126
+ addLog(_jsx(SuccessLog, { message: "no broken links found" }));
127
+ await terminate(0);
128
+ }
129
+ const brokenLinksByFile = {};
130
+ brokenLinks.forEach((mdxPath) => {
131
+ const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
132
+ const brokenLinksForFile = brokenLinksByFile[filename];
133
+ if (brokenLinksForFile) {
134
+ brokenLinksForFile.push(mdxPath.originalPath);
135
+ }
136
+ else {
137
+ brokenLinksByFile[filename] = [mdxPath.originalPath];
138
+ }
139
+ });
140
+ clearLogs();
141
+ addLog(_jsx(BrokenLinksLog, { brokenLinksByFile: brokenLinksByFile }));
142
+ }
143
+ catch (err) {
144
+ addLog(_jsx(ErrorLog, { message: err instanceof Error ? err.message : 'unknown error' }));
145
+ await terminate(1);
146
+ }
147
+ await terminate(1);
148
+ })
149
+ .command('rename <from> <to>', 'rename a file and update all internal link references', (yargs) => yargs
150
+ .positional('from', {
151
+ describe: 'the file to rename',
152
+ type: 'string',
153
+ })
154
+ .positional('to', {
155
+ describe: 'the new name for the file',
156
+ type: 'string',
157
+ })
158
+ .demandOption(['from', 'to'])
159
+ .option('force', {
160
+ type: 'boolean',
161
+ default: false,
162
+ description: 'rename files and skip errors',
163
+ })
164
+ .epilog('example: `poetora rename introduction.mdx overview.mdx`'), async ({ from, to, force }) => {
165
+ await renameFilesAndUpdateLinksInContent(from, to, force);
166
+ await terminate(0);
167
+ })
168
+ .command('update', 'update the CLI to the latest version', () => undefined, async () => {
169
+ await update({ packageName });
170
+ await terminate(0);
171
+ })
172
+ .command(['a11y', 'accessibility-check', 'a11y-check', 'accessibility'], 'check for accessibility issues in documentation', () => undefined, async () => {
173
+ const accessibilityCheckTerminateCode = await accessibilityCheck();
174
+ const mdxLinterTerminateCode = await mdxLinter();
175
+ await terminate(accessibilityCheckTerminateCode || mdxLinterTerminateCode);
176
+ })
177
+ .command(['version', 'v'], 'display the current version of the CLI and client', () => undefined, async () => {
178
+ const { cli, client } = getVersions();
179
+ addLog(_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "cli version" }), ' ', cli] }));
180
+ addLog(_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "green", children: "client version" }), ' ', client] }));
181
+ })
182
+ .command('new [directory]', 'Create a new Poetora documentation site', (yargs) => yargs.positional('directory', {
183
+ describe: 'The directory to initialize your documentation',
184
+ type: 'string',
185
+ default: '.',
186
+ }), async ({ directory }) => {
187
+ try {
188
+ await init(directory);
189
+ await terminate(0);
190
+ }
191
+ catch (error) {
192
+ addLog(_jsx(ErrorLog, { message: error instanceof Error ? error.message : 'error occurred' }));
193
+ await terminate(1);
194
+ }
195
+ })
196
+ .strictCommands()
197
+ .demandCommand(1, 'unknown command. see above for the list of supported commands.')
198
+ .alias('h', 'help')
199
+ .alias('v', 'version')
200
+ .parse());
201
+ };
@@ -0,0 +1,2 @@
1
+ export declare const HOME_DIR: string;
2
+ export declare const CMD_EXEC_PATH: string;
@@ -0,0 +1,3 @@
1
+ import os from 'os';
2
+ export const HOME_DIR = os.homedir();
3
+ export const CMD_EXEC_PATH = process.cwd();
@@ -0,0 +1,17 @@
1
+ import { exec } from 'node:child_process';
2
+ import type { ArgumentsCamelCase } from 'yargs';
3
+ export declare const checkPort: (argv: ArgumentsCamelCase) => Promise<number | undefined>;
4
+ export declare const checkNodeVersion: () => Promise<void>;
5
+ export declare const getCliVersion: () => string | undefined;
6
+ export declare const getVersions: () => {
7
+ cli: string | undefined;
8
+ client: string | undefined;
9
+ };
10
+ export declare const getLatestCliVersion: (packageName: string) => string;
11
+ export declare const suppressConsoleWarnings: () => void;
12
+ export declare const readLocalOpenApiFile: (filename: string) => Promise<Record<string, unknown> | undefined>;
13
+ export declare const terminate: (code: number) => Promise<never>;
14
+ export declare const execAsync: typeof exec.__promisify__;
15
+ export declare const detectPackageManager: ({ packageName }: {
16
+ packageName: string;
17
+ }) => Promise<"pnpm" | "npm">;
package/bin/helpers.js ADDED
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { addLog, ErrorLog, getClientVersion, InfoLog, LOCAL_LINKED_CLI_VERSION, } from '@poetora/previewing';
3
+ import detect from 'detect-port';
4
+ import fs from 'fs/promises';
5
+ import yaml from 'js-yaml';
6
+ import { exec, execSync } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import path from 'path';
9
+ import yargs from 'yargs';
10
+ export const checkPort = async (argv) => {
11
+ const initialPort = typeof argv.port === 'number' ? argv.port : 3000;
12
+ if (initialPort === (await detect(initialPort)))
13
+ return initialPort;
14
+ for (let port = initialPort + 1; port < initialPort + 10; port++) {
15
+ addLog(_jsx(InfoLog, { message: `port ${port - 1} is already in use. trying ${port} instead` }));
16
+ if (port === (await detect(port)))
17
+ return port;
18
+ }
19
+ };
20
+ export const checkNodeVersion = async () => {
21
+ let nodeVersionString = process.version;
22
+ if (nodeVersionString.charAt(0) === 'v') {
23
+ nodeVersionString = nodeVersionString.slice(1);
24
+ }
25
+ const versionArr = nodeVersionString.split('.');
26
+ const majorVersion = parseInt(versionArr[0], 10);
27
+ if (majorVersion < 18) {
28
+ addLog(_jsx(ErrorLog, { message: `poetora requires a node version >= 18.0.0 (current version ${nodeVersionString}). try removing the poetora package, upgrading node, reinstalling poetora, and running again.` }));
29
+ }
30
+ };
31
+ export const getCliVersion = () => {
32
+ const y = yargs();
33
+ let version = undefined;
34
+ y.showVersion((s) => {
35
+ version = s;
36
+ return false;
37
+ });
38
+ if (process.env.CLI_TEST_MODE === 'true') {
39
+ return 'test-cli';
40
+ }
41
+ if (version === 'unknown') {
42
+ version = LOCAL_LINKED_CLI_VERSION;
43
+ }
44
+ return version;
45
+ };
46
+ export const getVersions = () => {
47
+ const cli = getCliVersion();
48
+ const client = getClientVersion().trim();
49
+ return { cli, client };
50
+ };
51
+ export const getLatestCliVersion = (packageName) => {
52
+ return execSync(`npm view ${packageName} version --silent`, {
53
+ encoding: 'utf-8',
54
+ stdio: ['pipe', 'pipe', 'pipe'],
55
+ }).trim();
56
+ };
57
+ export const suppressConsoleWarnings = () => {
58
+ const ignoredMessages = [
59
+ 'No utility classes were detected',
60
+ 'https://tailwindcss.com/docs/content-configuration',
61
+ 'DeprecationWarning',
62
+ ];
63
+ const originalConsoleError = console.error;
64
+ console.error = (...args) => {
65
+ const message = args.join(' ');
66
+ if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) {
67
+ return;
68
+ }
69
+ originalConsoleError.apply(console, args);
70
+ };
71
+ const originalConsoleWarn = console.warn;
72
+ console.warn = (...args) => {
73
+ const message = args.join(' ');
74
+ if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) {
75
+ return;
76
+ }
77
+ originalConsoleWarn.apply(console, args);
78
+ };
79
+ };
80
+ export const readLocalOpenApiFile = async (filename) => {
81
+ const pathname = path.resolve(process.cwd(), filename);
82
+ const file = await fs.readFile(pathname, 'utf-8');
83
+ const document = yaml.load(file);
84
+ return document;
85
+ };
86
+ export const terminate = async (code) => {
87
+ await new Promise((resolve) => setTimeout(resolve, 50));
88
+ process.exit(code);
89
+ };
90
+ export const execAsync = promisify(exec);
91
+ export const detectPackageManager = async ({ packageName }) => {
92
+ try {
93
+ const { stdout: packagePath } = await execAsync(`which ${packageName}`);
94
+ if (packagePath.includes('pnpm')) {
95
+ return 'pnpm';
96
+ }
97
+ else {
98
+ return 'npm';
99
+ }
100
+ }
101
+ catch (error) {
102
+ return 'npm';
103
+ }
104
+ };
package/bin/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { type ChildProcess } from 'child_process';
3
+ declare let cli: ChildProcess | null;
4
+ export { cli };
package/bin/index.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const packageName = path.basename(process.argv[1] ?? '') === 'index'
8
+ ? 'poet'
9
+ : path.basename(process.argv[1] ?? '') || 'poet';
10
+ let cli = null;
11
+ let isShuttingDown = false;
12
+ let hasExited = false;
13
+ const cleanup = async () => {
14
+ if (isShuttingDown)
15
+ return;
16
+ isShuttingDown = true;
17
+ if (cli && !cli.killed) {
18
+ try {
19
+ cli.kill('SIGTERM');
20
+ await new Promise((resolve) => {
21
+ const timeout = setTimeout(() => {
22
+ if (cli && !cli.killed) {
23
+ cli.kill('SIGKILL');
24
+ }
25
+ resolve();
26
+ }, 5000);
27
+ cli.once('exit', () => {
28
+ clearTimeout(timeout);
29
+ resolve();
30
+ });
31
+ });
32
+ }
33
+ catch (error) {
34
+ }
35
+ }
36
+ };
37
+ const exitProcess = (code) => {
38
+ if (hasExited)
39
+ return;
40
+ hasExited = true;
41
+ process.exit(code);
42
+ };
43
+ const killSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'];
44
+ killSignals.forEach((signal) => {
45
+ process.on(signal, async () => {
46
+ await cleanup();
47
+ exitProcess(0);
48
+ });
49
+ });
50
+ process.on('uncaughtException', async () => {
51
+ await cleanup();
52
+ exitProcess(1);
53
+ });
54
+ process.on('unhandledRejection', async () => {
55
+ await cleanup();
56
+ exitProcess(1);
57
+ });
58
+ try {
59
+ cli = spawn('node', ['--no-deprecation', path.join(__dirname, '../bin/start'), ...process.argv.slice(2)], {
60
+ stdio: 'inherit',
61
+ env: {
62
+ ...process.env,
63
+ POETORA_PACKAGE_NAME: packageName,
64
+ CLI_TEST_MODE: process.env.CLI_TEST_MODE ?? 'false',
65
+ },
66
+ shell: process.platform === 'win32',
67
+ windowsHide: process.platform === 'win32',
68
+ detached: false,
69
+ });
70
+ cli.on('error', async (error) => {
71
+ console.error(`Failed to start ${packageName}: ${error.message}`);
72
+ await cleanup();
73
+ exitProcess(1);
74
+ });
75
+ cli.on('exit', (code) => {
76
+ exitProcess(code ?? 0);
77
+ });
78
+ }
79
+ catch (error) {
80
+ console.error(`Failed to start ${packageName}: ${error}`);
81
+ exitProcess(1);
82
+ }
83
+ process.on('exit', () => {
84
+ if (cli && !cli.killed) {
85
+ try {
86
+ cli.kill('SIGKILL');
87
+ }
88
+ catch (error) {
89
+ }
90
+ }
91
+ });
92
+ export { cli };
package/bin/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function init(installDir: string): Promise<void>;
package/bin/init.js ADDED
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { select, input } from '@inquirer/prompts';
3
+ import { addLogs, addLog, SpinnerLog, removeLastLog } from '@poetora/previewing';
4
+ import { docsConfigSchema } from '@poetora/validation';
5
+ import AdmZip from 'adm-zip';
6
+ import fse from 'fs-extra';
7
+ import { Box, Text } from 'ink';
8
+ const sendOnboardingMessage = (installDir) => {
9
+ addLogs(_jsx(Text, { bold: true, children: "Documentation Setup!" }), _jsx(Text, { children: "To see your docs run" }), _jsxs(Box, { children: [_jsx(Text, { color: "blue", children: "cd" }), _jsxs(Text, { children: [" ", installDir] })] }), _jsx(Text, { color: "blue", children: "poet dev" }));
10
+ };
11
+ export async function init(installDir) {
12
+ await fse.ensureDir(installDir);
13
+ const dirContents = await fse.readdir(installDir).catch(() => []);
14
+ if (dirContents.length > 0) {
15
+ const choice = await select({
16
+ message: `Directory ${installDir} is not empty. What would you like to do?`,
17
+ choices: [
18
+ { name: 'Create in a subdirectory', value: 'subdir' },
19
+ { name: 'Overwrite current directory (may lose contents)', value: 'overwrite' },
20
+ { name: 'Cancel', value: 'cancel' },
21
+ ],
22
+ });
23
+ if (choice === 'cancel') {
24
+ return;
25
+ }
26
+ if (choice === 'subdir') {
27
+ const subdir = await input({
28
+ message: 'Subdirectory name:',
29
+ default: 'docs',
30
+ });
31
+ if (!subdir || subdir.trim() === '') {
32
+ throw new Error('Subdirectory name cannot be empty');
33
+ }
34
+ installDir = installDir === '.' ? subdir : `${installDir}/${subdir}`;
35
+ await fse.ensureDir(installDir);
36
+ }
37
+ }
38
+ const defaultProject = installDir == '.' ? 'Poetora' : installDir;
39
+ const projectName = await input({
40
+ message: 'Project Name',
41
+ default: defaultProject,
42
+ });
43
+ const themes = docsConfigSchema.options.map((option) => {
44
+ return option.shape.theme._def.value;
45
+ });
46
+ const theme = await select({
47
+ message: 'Theme',
48
+ choices: themes.map((t) => ({
49
+ name: t,
50
+ value: t,
51
+ })),
52
+ });
53
+ addLog(_jsx(SpinnerLog, { message: "downloading starter template..." }));
54
+ const response = await fetch('https://github.com/poetora/starter/archive/refs/heads/main.zip');
55
+ const buffer = await response.arrayBuffer();
56
+ await fse.writeFile(installDir + '/starter.zip', Buffer.from(buffer));
57
+ removeLastLog();
58
+ addLog(_jsx(SpinnerLog, { message: "extracting..." }));
59
+ new AdmZip(installDir + '/starter.zip').extractAllTo(installDir, true);
60
+ removeLastLog();
61
+ await fse.copy(installDir + '/starter-main', installDir, {
62
+ overwrite: true,
63
+ filter: (src) => !src.includes('starter-main/starter-main'),
64
+ });
65
+ await fse.remove(installDir + '/starter.zip');
66
+ await fse.remove(installDir + '/starter-main');
67
+ const docsJsonPath = installDir + '/docs.json';
68
+ const docsConfig = await fse.readJson(docsJsonPath);
69
+ docsConfig.theme = theme;
70
+ docsConfig.name = projectName;
71
+ await fse.writeJson(docsJsonPath, docsConfig, { spaces: 2 });
72
+ sendOnboardingMessage(installDir);
73
+ }
@@ -0,0 +1,13 @@
1
+ export interface AccessibilityFixAttribute {
2
+ filePath: string;
3
+ line?: number;
4
+ column?: number;
5
+ element: 'img' | 'video' | 'a';
6
+ tagName: string;
7
+ }
8
+ export interface MdxAccessibilityResult {
9
+ missingAltAttributes: AccessibilityFixAttribute[];
10
+ totalFiles: number;
11
+ filesWithIssues: number;
12
+ }
13
+ export declare const checkMdxAccessibility: (baseDir?: string) => Promise<MdxAccessibilityResult>;
@@ -0,0 +1,102 @@
1
+ import { categorizeFilePaths, getPoetIgnore } from '@poetora/prebuild';
2
+ import { coreRemark } from '@poetora/shared';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { visit } from 'unist-util-visit';
6
+ const checkAltAttributes = (filePath, content) => {
7
+ const issues = [];
8
+ const visitElements = () => {
9
+ return (tree) => {
10
+ visit(tree, (node) => {
11
+ if (node.type === 'image') {
12
+ if (!node.alt || node.alt.trim() === '') {
13
+ issues.push({
14
+ filePath,
15
+ line: node.position?.start.line,
16
+ column: node.position?.start.column,
17
+ element: 'img',
18
+ tagName: 'image (markdown)',
19
+ });
20
+ }
21
+ return;
22
+ }
23
+ const mdxJsxElement = node;
24
+ if (mdxJsxElement.name === 'img' || mdxJsxElement.name === 'video') {
25
+ const altAttrIndex = mdxJsxElement.attributes.findIndex((attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'alt');
26
+ const altAttribute = mdxJsxElement.attributes[altAttrIndex];
27
+ const hasValidAlt = altAttribute &&
28
+ typeof altAttribute.value === 'string' &&
29
+ altAttribute.value.trim() !== '';
30
+ if (!hasValidAlt) {
31
+ issues.push({
32
+ filePath,
33
+ line: node.position?.start.line,
34
+ column: node.position?.start.column,
35
+ element: mdxJsxElement.name,
36
+ tagName: mdxJsxElement.name,
37
+ });
38
+ }
39
+ }
40
+ else if (mdxJsxElement.name === 'a') {
41
+ const hasTextContent = (children) => {
42
+ return children.some((child) => {
43
+ if (child.type === 'text') {
44
+ const textNode = child;
45
+ return textNode.value.trim() !== '';
46
+ }
47
+ if ('children' in child && Array.isArray(child.children)) {
48
+ return hasTextContent(child.children);
49
+ }
50
+ return false;
51
+ });
52
+ };
53
+ if (!hasTextContent(mdxJsxElement.children)) {
54
+ issues.push({
55
+ filePath,
56
+ line: node.position?.start.line,
57
+ column: node.position?.start.column,
58
+ element: 'a',
59
+ tagName: '<a>',
60
+ });
61
+ }
62
+ }
63
+ });
64
+ return tree;
65
+ };
66
+ };
67
+ try {
68
+ coreRemark().use(visitElements).processSync(content);
69
+ }
70
+ catch (error) {
71
+ console.warn(`Warning: Could not parse ${filePath}: ${error}`);
72
+ }
73
+ return issues;
74
+ };
75
+ export const checkMdxAccessibility = async (baseDir = process.cwd()) => {
76
+ const poetIgnore = await getPoetIgnore(baseDir);
77
+ const { contentFilenames } = await categorizeFilePaths(baseDir, poetIgnore);
78
+ const mdxFiles = [];
79
+ for (const file of contentFilenames) {
80
+ mdxFiles.push(path.join(baseDir, file));
81
+ }
82
+ const allIssues = [];
83
+ const filesWithIssues = new Set();
84
+ for (const filePath of mdxFiles) {
85
+ try {
86
+ const content = fs.readFileSync(filePath, 'utf-8');
87
+ const issues = checkAltAttributes(filePath, content);
88
+ if (issues.length > 0) {
89
+ allIssues.push(...issues);
90
+ filesWithIssues.add(filePath);
91
+ }
92
+ }
93
+ catch (error) {
94
+ console.warn(`Warning: Could not read file ${filePath}: ${error}`);
95
+ }
96
+ }
97
+ return {
98
+ missingAltAttributes: allIssues,
99
+ totalFiles: mdxFiles.length,
100
+ filesWithIssues: filesWithIssues.size,
101
+ };
102
+ };
@@ -0,0 +1,2 @@
1
+ import { TerminateCode } from './accessibilityCheck.js';
2
+ export declare const mdxLinter: () => Promise<TerminateCode>;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { addLog, ErrorLog, SuccessLog } from '@poetora/previewing';
3
+ import { Text } from 'ink';
4
+ import path from 'path';
5
+ import { checkMdxAccessibility } from './mdxAccessibility.js';
6
+ export const mdxLinter = async () => {
7
+ try {
8
+ addLog(_jsx(Text, { bold: true, color: "cyan", children: "Checking mdx files for accessibility issues..." }));
9
+ const results = await checkMdxAccessibility();
10
+ if (results.missingAltAttributes.length === 0) {
11
+ addLog(_jsx(SuccessLog, { message: "no accessibility issues found" }));
12
+ addLog(_jsxs(Text, { children: ["Checked ", results.totalFiles, " MDX files - all images and videos have alt attributes."] }));
13
+ return 0;
14
+ }
15
+ const issuesByFile = {};
16
+ results.missingAltAttributes.forEach((issue) => {
17
+ if (!issuesByFile[issue.filePath]) {
18
+ issuesByFile[issue.filePath] = [];
19
+ }
20
+ issuesByFile[issue.filePath]?.push(issue);
21
+ });
22
+ addLog(_jsxs(Text, { bold: true, color: "red", children: ["Found ", results.missingAltAttributes.length, " accessibility issues in", ' ', results.filesWithIssues, " files:"] }));
23
+ addLog(_jsx(Text, {}));
24
+ for (const [filePath, issues] of Object.entries(issuesByFile)) {
25
+ const relativePath = path.relative(process.cwd(), filePath);
26
+ addLog(_jsxs(Text, { bold: true, children: [relativePath, ":"] }));
27
+ for (const issue of issues) {
28
+ const location = issue.line && issue.column ? ` (line ${issue.line}, col ${issue.column})` : '';
29
+ if (issue.element === 'a') {
30
+ addLog(_jsxs(Text, { children: [_jsx(Text, { color: "red", children: " \u2717" }), " Missing text attribute ", _jsx(Text, { bold: true, children: issue.tagName }), ' ', "element", location] }));
31
+ }
32
+ else {
33
+ addLog(_jsxs(Text, { children: [_jsx(Text, { color: "red", children: " \u2717" }), " Missing alt attribute on ", _jsx(Text, { bold: true, children: issue.tagName }), ' ', "element", location] }));
34
+ }
35
+ }
36
+ addLog(_jsx(Text, {}));
37
+ }
38
+ addLog(_jsxs(Text, { color: "yellow", children: [_jsx(Text, { bold: true, children: "Recommendation:" }), " Add alt attributes to all images and videos for better accessibility."] }));
39
+ return 1;
40
+ }
41
+ catch (error) {
42
+ addLog(_jsx(ErrorLog, { message: `MDX accessibility check failed: ${error instanceof Error ? error.message : 'Unknown error'}` }));
43
+ return 1;
44
+ }
45
+ };