@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
package/src/constants.ts
DELETED
package/src/errors/cli-error.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base CLI error class for all custom errors
|
|
3
|
-
*/
|
|
4
|
-
export class CliError extends Error {
|
|
5
|
-
constructor(
|
|
6
|
-
message: string,
|
|
7
|
-
public readonly code: string,
|
|
8
|
-
public readonly exitCode: number = 1
|
|
9
|
-
) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = 'CliError';
|
|
12
|
-
Error.captureStackTrace(this, this.constructor);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Error thrown when environment requirements are not met
|
|
18
|
-
*/
|
|
19
|
-
export class InvalidEnvironmentError extends CliError {
|
|
20
|
-
constructor(message: string) {
|
|
21
|
-
super(message, 'INVALID_ENVIRONMENT', 1);
|
|
22
|
-
this.name = 'InvalidEnvironmentError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Error thrown when no available port is found
|
|
28
|
-
*/
|
|
29
|
-
export class NoAvailablePortError extends CliError {
|
|
30
|
-
constructor(message: string) {
|
|
31
|
-
super(message, 'NO_AVAILABLE_PORT', 1);
|
|
32
|
-
this.name = 'NoAvailablePortError';
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Error thrown when configuration file is not found
|
|
38
|
-
*/
|
|
39
|
-
export class ConfigNotFoundError extends CliError {
|
|
40
|
-
constructor(message: string) {
|
|
41
|
-
super(message, 'CONFIG_NOT_FOUND', 1);
|
|
42
|
-
this.name = 'ConfigNotFoundError';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Error thrown when command validation fails
|
|
48
|
-
*/
|
|
49
|
-
export class ValidationError extends CliError {
|
|
50
|
-
constructor(
|
|
51
|
-
message: string,
|
|
52
|
-
public readonly field?: string
|
|
53
|
-
) {
|
|
54
|
-
super(message, 'VALIDATION_ERROR', 1);
|
|
55
|
-
this.name = 'ValidationError';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Error thrown when file operations fail
|
|
61
|
-
*/
|
|
62
|
-
export class FileSystemError extends CliError {
|
|
63
|
-
constructor(
|
|
64
|
-
message: string,
|
|
65
|
-
public readonly filePath?: string
|
|
66
|
-
) {
|
|
67
|
-
super(message, 'FILE_SYSTEM_ERROR', 1);
|
|
68
|
-
this.name = 'FileSystemError';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Error thrown when external service calls fail
|
|
74
|
-
*/
|
|
75
|
-
export class ExternalServiceError extends CliError {
|
|
76
|
-
constructor(
|
|
77
|
-
message: string,
|
|
78
|
-
public readonly service?: string
|
|
79
|
-
) {
|
|
80
|
-
super(message, 'EXTERNAL_SERVICE_ERROR', 1);
|
|
81
|
-
this.name = 'ExternalServiceError';
|
|
82
|
-
}
|
|
83
|
-
}
|
package/src/errors/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './cli-error.js';
|
package/src/index.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { type ChildProcess, spawn } from 'child_process';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
const packageName =
|
|
10
|
-
path.basename(process.argv[1] ?? '') === 'index'
|
|
11
|
-
? 'poet'
|
|
12
|
-
: path.basename(process.argv[1] ?? '') || 'poet';
|
|
13
|
-
|
|
14
|
-
let cli: ChildProcess | null = null;
|
|
15
|
-
let isShuttingDown = false;
|
|
16
|
-
let hasExited = false;
|
|
17
|
-
|
|
18
|
-
const cleanup = async (): Promise<void> => {
|
|
19
|
-
if (isShuttingDown) return;
|
|
20
|
-
isShuttingDown = true;
|
|
21
|
-
|
|
22
|
-
if (cli && !cli.killed) {
|
|
23
|
-
try {
|
|
24
|
-
cli.kill('SIGTERM');
|
|
25
|
-
|
|
26
|
-
await new Promise<void>((resolve) => {
|
|
27
|
-
const timeout = setTimeout(() => {
|
|
28
|
-
if (cli && !cli.killed) {
|
|
29
|
-
cli.kill('SIGKILL');
|
|
30
|
-
}
|
|
31
|
-
resolve();
|
|
32
|
-
}, 5000);
|
|
33
|
-
|
|
34
|
-
cli?.once('exit', () => {
|
|
35
|
-
clearTimeout(timeout);
|
|
36
|
-
resolve();
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
} catch (_error) {
|
|
40
|
-
// ignore
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const exitProcess = (code: number) => {
|
|
46
|
-
if (hasExited) return;
|
|
47
|
-
hasExited = true;
|
|
48
|
-
process.exit(code);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const killSignals = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP'];
|
|
52
|
-
killSignals.forEach((signal) => {
|
|
53
|
-
process.on(signal, async () => {
|
|
54
|
-
await cleanup();
|
|
55
|
-
exitProcess(0);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
process.on('uncaughtException', async () => {
|
|
60
|
-
await cleanup();
|
|
61
|
-
exitProcess(1);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
process.on('unhandledRejection', async () => {
|
|
65
|
-
await cleanup();
|
|
66
|
-
exitProcess(1);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
cli = spawn(
|
|
71
|
-
'node',
|
|
72
|
-
['--no-deprecation', path.join(__dirname, '../bin/start'), ...process.argv.slice(2)],
|
|
73
|
-
{
|
|
74
|
-
stdio: 'inherit',
|
|
75
|
-
env: {
|
|
76
|
-
...process.env,
|
|
77
|
-
POETORA_PACKAGE_NAME: packageName,
|
|
78
|
-
CLI_TEST_MODE: process.env.CLI_TEST_MODE ?? 'false',
|
|
79
|
-
},
|
|
80
|
-
shell: process.platform === 'win32',
|
|
81
|
-
windowsHide: process.platform === 'win32',
|
|
82
|
-
detached: false,
|
|
83
|
-
}
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
cli.on('error', async (error) => {
|
|
87
|
-
console.error(`Failed to start ${packageName}: ${error.message}`);
|
|
88
|
-
await cleanup();
|
|
89
|
-
exitProcess(1);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
cli.on('exit', (code) => {
|
|
93
|
-
exitProcess(code ?? 0);
|
|
94
|
-
});
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error(`Failed to start ${packageName}: ${error}`);
|
|
97
|
-
exitProcess(1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
process.on('exit', () => {
|
|
101
|
-
if (cli && !cli.killed) {
|
|
102
|
-
try {
|
|
103
|
-
cli.kill('SIGKILL');
|
|
104
|
-
} catch (_error) {
|
|
105
|
-
// ignore
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
export { cli };
|
package/src/mdxAccessibility.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { categorizeFilePaths, getPoetIgnore } from '@poetora/prebuild';
|
|
2
|
-
import { coreRemark } from '@poetora/shared';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import type { Node, Root, Text } from 'mdast';
|
|
5
|
-
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { visit } from 'unist-util-visit';
|
|
8
|
-
|
|
9
|
-
export interface AccessibilityFixAttribute {
|
|
10
|
-
filePath: string;
|
|
11
|
-
line?: number;
|
|
12
|
-
column?: number;
|
|
13
|
-
element: 'img' | 'video' | 'a';
|
|
14
|
-
tagName: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface MdxAccessibilityResult {
|
|
18
|
-
missingAltAttributes: AccessibilityFixAttribute[];
|
|
19
|
-
totalFiles: number;
|
|
20
|
-
filesWithIssues: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const checkAltAttributes = (filePath: string, content: string): AccessibilityFixAttribute[] => {
|
|
24
|
-
const issues: AccessibilityFixAttribute[] = [];
|
|
25
|
-
|
|
26
|
-
const visitElements = () => {
|
|
27
|
-
return (tree: Root) => {
|
|
28
|
-
visit(tree, (node) => {
|
|
29
|
-
if (node.type === 'image') {
|
|
30
|
-
if (!node.alt || node.alt.trim() === '') {
|
|
31
|
-
issues.push({
|
|
32
|
-
filePath,
|
|
33
|
-
line: node.position?.start.line,
|
|
34
|
-
column: node.position?.start.column,
|
|
35
|
-
element: 'img',
|
|
36
|
-
tagName: 'image (markdown)',
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const mdxJsxElement = node as MdxJsxFlowElement;
|
|
43
|
-
if (mdxJsxElement.name === 'img' || mdxJsxElement.name === 'video') {
|
|
44
|
-
const altAttrIndex = mdxJsxElement.attributes.findIndex(
|
|
45
|
-
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'alt'
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const altAttribute = mdxJsxElement.attributes[altAttrIndex];
|
|
49
|
-
const hasValidAlt =
|
|
50
|
-
altAttribute &&
|
|
51
|
-
typeof altAttribute.value === 'string' &&
|
|
52
|
-
altAttribute.value.trim() !== '';
|
|
53
|
-
|
|
54
|
-
if (!hasValidAlt) {
|
|
55
|
-
issues.push({
|
|
56
|
-
filePath,
|
|
57
|
-
line: node.position?.start.line,
|
|
58
|
-
column: node.position?.start.column,
|
|
59
|
-
element: mdxJsxElement.name,
|
|
60
|
-
tagName: mdxJsxElement.name,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
} else if (mdxJsxElement.name === 'a') {
|
|
64
|
-
const hasTextContent = (children: Node[]): boolean => {
|
|
65
|
-
return children.some((child) => {
|
|
66
|
-
if (child.type === 'text') {
|
|
67
|
-
const textNode = child as Text;
|
|
68
|
-
return textNode.value.trim() !== '';
|
|
69
|
-
}
|
|
70
|
-
if ('children' in child && Array.isArray(child.children)) {
|
|
71
|
-
return hasTextContent(child.children as Node[]);
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (!hasTextContent(mdxJsxElement.children as Node[])) {
|
|
78
|
-
issues.push({
|
|
79
|
-
filePath,
|
|
80
|
-
line: node.position?.start.line,
|
|
81
|
-
column: node.position?.start.column,
|
|
82
|
-
element: 'a',
|
|
83
|
-
tagName: '<a>',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
return tree;
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
coreRemark().use(visitElements).processSync(content);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.warn(`Warning: Could not parse ${filePath}: ${error}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return issues;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export const checkMdxAccessibility = async (
|
|
102
|
-
baseDir: string = process.cwd()
|
|
103
|
-
): Promise<MdxAccessibilityResult> => {
|
|
104
|
-
const poetIgnore = await getPoetIgnore(baseDir);
|
|
105
|
-
const { contentFilenames } = await categorizeFilePaths(baseDir, poetIgnore);
|
|
106
|
-
const mdxFiles: string[] = [];
|
|
107
|
-
for (const file of contentFilenames) {
|
|
108
|
-
mdxFiles.push(path.join(baseDir, file));
|
|
109
|
-
}
|
|
110
|
-
const allIssues: AccessibilityFixAttribute[] = [];
|
|
111
|
-
const filesWithIssues = new Set<string>();
|
|
112
|
-
|
|
113
|
-
for (const filePath of mdxFiles) {
|
|
114
|
-
try {
|
|
115
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
116
|
-
const issues = checkAltAttributes(filePath, content);
|
|
117
|
-
|
|
118
|
-
if (issues.length > 0) {
|
|
119
|
-
allIssues.push(...issues);
|
|
120
|
-
filesWithIssues.add(filePath);
|
|
121
|
-
}
|
|
122
|
-
} catch (error) {
|
|
123
|
-
console.warn(`Warning: Could not read file ${filePath}: ${error}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
missingAltAttributes: allIssues,
|
|
129
|
-
totalFiles: mdxFiles.length,
|
|
130
|
-
filesWithIssues: filesWithIssues.size,
|
|
131
|
-
};
|
|
132
|
-
};
|
package/src/middlewares.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { addLog, ErrorLog } from '@poetora/previewing';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Yargs middleware functions
|
|
6
|
-
* These run before command execution
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Check if Node.js version is supported
|
|
11
|
-
* Middleware that validates Node version before any command runs
|
|
12
|
-
*
|
|
13
|
-
* Minimum requirement: Node.js >= 18.0.0
|
|
14
|
-
* Note: Individual commands may have stricter requirements (e.g., dev command)
|
|
15
|
-
*/
|
|
16
|
-
export const checkNodeVersion = async (): Promise<void> => {
|
|
17
|
-
let nodeVersionString = process.version;
|
|
18
|
-
if (nodeVersionString.charAt(0) === 'v') {
|
|
19
|
-
nodeVersionString = nodeVersionString.slice(1);
|
|
20
|
-
}
|
|
21
|
-
const versionArr = nodeVersionString.split('.');
|
|
22
|
-
const versionStr = versionArr[0];
|
|
23
|
-
if (!versionStr) {
|
|
24
|
-
addLog(
|
|
25
|
-
React.createElement(ErrorLog, {
|
|
26
|
-
message: `Unable to determine Node.js version (got "${process.version}"). Please ensure you are running Node.js >= 18.0.0.`,
|
|
27
|
-
})
|
|
28
|
-
);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
const majorVersion = parseInt(versionStr, 10);
|
|
32
|
-
|
|
33
|
-
if (majorVersion < 18) {
|
|
34
|
-
addLog(
|
|
35
|
-
React.createElement(ErrorLog, {
|
|
36
|
-
message: `poetora requires Node.js >= 18.0.0 (current version ${nodeVersionString}). Please upgrade Node.js and try again.`,
|
|
37
|
-
})
|
|
38
|
-
);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Suppress common console warnings that don't affect functionality
|
|
45
|
-
* Filters out known noise from dependencies
|
|
46
|
-
*/
|
|
47
|
-
export const suppressConsoleWarnings = (): void => {
|
|
48
|
-
// Ignore tailwind warnings and punycode deprecation warning
|
|
49
|
-
const ignoredMessages = [
|
|
50
|
-
'No utility classes were detected',
|
|
51
|
-
'https://tailwindcss.com/docs/content-configuration',
|
|
52
|
-
'DeprecationWarning',
|
|
53
|
-
'punycode',
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const originalConsoleError = console.error;
|
|
57
|
-
console.error = (...args) => {
|
|
58
|
-
const message = args.join(' ');
|
|
59
|
-
if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
originalConsoleError.apply(console, args);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const originalConsoleWarn = console.warn;
|
|
66
|
-
console.warn = (...args) => {
|
|
67
|
-
const message = args.join(' ');
|
|
68
|
-
if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
originalConsoleWarn.apply(console, args);
|
|
72
|
-
};
|
|
73
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import detect from 'detect-port';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { NoAvailablePortError } from '../../errors/index.js';
|
|
5
|
-
import { CLI_CONSTANTS } from '../../types/index.js';
|
|
6
|
-
import { PortService } from '../port.service.js';
|
|
7
|
-
|
|
8
|
-
vi.mock('detect-port');
|
|
9
|
-
|
|
10
|
-
describe('PortService', () => {
|
|
11
|
-
let service: PortService;
|
|
12
|
-
const mockDetect = vi.mocked(detect);
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
service = new PortService();
|
|
16
|
-
vi.clearAllMocks();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('findAvailablePort', () => {
|
|
20
|
-
it('should return preferred port when available', async () => {
|
|
21
|
-
mockDetect.mockResolvedValue(3000);
|
|
22
|
-
|
|
23
|
-
const port = await service.findAvailablePort(3000);
|
|
24
|
-
|
|
25
|
-
expect(port).toBe(3000);
|
|
26
|
-
expect(mockDetect).toHaveBeenCalledWith(3000);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should return default port when no preferred port specified', async () => {
|
|
30
|
-
mockDetect.mockResolvedValue(CLI_CONSTANTS.PORT.DEFAULT);
|
|
31
|
-
|
|
32
|
-
const port = await service.findAvailablePort();
|
|
33
|
-
|
|
34
|
-
expect(port).toBe(CLI_CONSTANTS.PORT.DEFAULT);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should try next port when first is occupied', async () => {
|
|
38
|
-
mockDetect.mockResolvedValueOnce(3001); // 3000 occupied
|
|
39
|
-
mockDetect.mockResolvedValueOnce(3001); // 3001 available
|
|
40
|
-
|
|
41
|
-
const port = await service.findAvailablePort(3000);
|
|
42
|
-
|
|
43
|
-
expect(port).toBe(3001);
|
|
44
|
-
expect(mockDetect).toHaveBeenCalledTimes(2);
|
|
45
|
-
expect(mockDetect).toHaveBeenNthCalledWith(1, 3000);
|
|
46
|
-
expect(mockDetect).toHaveBeenNthCalledWith(2, 3001);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should try multiple ports until finding available one', async () => {
|
|
50
|
-
// Ports 3000, 3001, 3002 are occupied
|
|
51
|
-
mockDetect.mockResolvedValueOnce(3001);
|
|
52
|
-
mockDetect.mockResolvedValueOnce(3002);
|
|
53
|
-
mockDetect.mockResolvedValueOnce(3003);
|
|
54
|
-
// Port 3003 is available
|
|
55
|
-
mockDetect.mockResolvedValueOnce(3003);
|
|
56
|
-
|
|
57
|
-
const port = await service.findAvailablePort(3000);
|
|
58
|
-
|
|
59
|
-
expect(port).toBe(3003);
|
|
60
|
-
expect(mockDetect).toHaveBeenCalledTimes(4);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should throw NoAvailablePortError after MAX_ATTEMPTS', async () => {
|
|
64
|
-
// All ports are occupied
|
|
65
|
-
mockDetect.mockImplementation((port) => Promise.resolve((port as number) + 1));
|
|
66
|
-
|
|
67
|
-
await expect(service.findAvailablePort(3000)).rejects.toThrow(NoAvailablePortError);
|
|
68
|
-
expect(mockDetect).toHaveBeenCalledTimes(CLI_CONSTANTS.PORT.MAX_ATTEMPTS);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should include start port in error message', async () => {
|
|
72
|
-
mockDetect.mockImplementation((port) => Promise.resolve((port as number) + 1));
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await service.findAvailablePort(5000);
|
|
76
|
-
expect.fail('Should have thrown error');
|
|
77
|
-
} catch (error) {
|
|
78
|
-
expect(error).toBeInstanceOf(NoAvailablePortError);
|
|
79
|
-
expect((error as NoAvailablePortError).message).toContain('5000');
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
});
|