@titannio/webtoolkit-cli 1.3.0
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/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +268 -0
- package/dist/bundle-audit.d.ts +7 -0
- package/dist/bundle-audit.js +111 -0
- package/dist/cleaner.d.ts +15 -0
- package/dist/cleaner.js +359 -0
- package/dist/config-reference.d.ts +8 -0
- package/dist/config-reference.js +805 -0
- package/dist/config.d.ts +306 -0
- package/dist/config.js +173 -0
- package/dist/dev-grid.d.ts +7 -0
- package/dist/dev-grid.js +181 -0
- package/dist/dev-watch.d.ts +13 -0
- package/dist/dev-watch.js +184 -0
- package/dist/environment.d.ts +10 -0
- package/dist/environment.js +172 -0
- package/dist/guard-registry.d.ts +1 -0
- package/dist/guard-registry.js +17 -0
- package/dist/guard-runner.d.ts +4 -0
- package/dist/guard-runner.js +36 -0
- package/dist/guards/any-guard.d.ts +16 -0
- package/dist/guards/any-guard.js +121 -0
- package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
- package/dist/guards/assert-no-tests-in-dist.js +56 -0
- package/dist/guards/check-mojibake.d.ts +52 -0
- package/dist/guards/check-mojibake.js +378 -0
- package/dist/guards/code-pattern-guard.d.ts +71 -0
- package/dist/guards/code-pattern-guard.js +654 -0
- package/dist/guards/dal-service-repository-check.d.ts +13 -0
- package/dist/guards/dal-service-repository-check.js +264 -0
- package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
- package/dist/guards/dependency-cruiser-guard.js +69 -0
- package/dist/guards/documentation-guard.d.ts +3 -0
- package/dist/guards/documentation-guard.js +370 -0
- package/dist/guards/guard-config.d.ts +19 -0
- package/dist/guards/guard-config.js +87 -0
- package/dist/guards/internal-link-guard.d.ts +12 -0
- package/dist/guards/internal-link-guard.js +272 -0
- package/dist/guards/package-surface-guard.d.ts +37 -0
- package/dist/guards/package-surface-guard.js +234 -0
- package/dist/guards/pnpm-workspace-config.d.ts +2 -0
- package/dist/guards/pnpm-workspace-config.js +40 -0
- package/dist/guards/rebuild-preflight.d.ts +29 -0
- package/dist/guards/rebuild-preflight.js +137 -0
- package/dist/guards/repository-hygiene-guard.d.ts +12 -0
- package/dist/guards/repository-hygiene-guard.js +70 -0
- package/dist/guards/schema-guard.d.ts +10 -0
- package/dist/guards/schema-guard.js +160 -0
- package/dist/guards/singleton-deps-guard.d.ts +21 -0
- package/dist/guards/singleton-deps-guard.js +183 -0
- package/dist/guards/tsconfig-guard.d.ts +5 -0
- package/dist/guards/tsconfig-guard.js +105 -0
- package/dist/guards/workspace-manifest-guard.d.ts +21 -0
- package/dist/guards/workspace-manifest-guard.js +210 -0
- package/dist/jsdoc-report.d.ts +7 -0
- package/dist/jsdoc-report.js +456 -0
- package/dist/process.d.ts +20 -0
- package/dist/process.js +86 -0
- package/dist/ready-service.d.ts +7 -0
- package/dist/ready-service.js +135 -0
- package/dist/release-gate.d.ts +7 -0
- package/dist/release-gate.js +35 -0
- package/dist/repo-check.d.ts +7 -0
- package/dist/repo-check.js +121 -0
- package/dist/tasks.d.ts +17 -0
- package/dist/tasks.js +195 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/upgrade.js +674 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +51 -0
- package/dist/workspace-tests.d.ts +33 -0
- package/dist/workspace-tests.js +529 -0
- package/package.json +57 -0
package/dist/validate.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { executeBuiltinGuard } from './guard-runner.js';
|
|
2
|
+
import { formatCommand, runCommandBuffered } from './process.js';
|
|
3
|
+
async function runStepWithStatus(step, runtime) {
|
|
4
|
+
if (!step.command && !step.builtinGuard) {
|
|
5
|
+
throw new Error(`Validate step "${step.label}" must define command or builtinGuard.`);
|
|
6
|
+
}
|
|
7
|
+
const label = 'Running ';
|
|
8
|
+
process.stdout.write(`- ${label} \x1b[1m${step.label.padEnd(10)}\x1b[0m... `);
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
const result = step.builtinGuard
|
|
11
|
+
? { code: executeBuiltinGuard(step.builtinGuard, step.args ?? [], runtime.cwd), output: '' }
|
|
12
|
+
: await runCommandBuffered({
|
|
13
|
+
command: step.command,
|
|
14
|
+
args: step.args ?? [],
|
|
15
|
+
cwd: step.cwd,
|
|
16
|
+
env: step.env,
|
|
17
|
+
}, runtime.cwd);
|
|
18
|
+
const duration = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
19
|
+
if (result.code === 0) {
|
|
20
|
+
console.info(`\x1b[32mOK\x1b[0m (${duration}s)`);
|
|
21
|
+
if (result.output.trim())
|
|
22
|
+
console.info(result.output.trim());
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
console.info(`\x1b[31mFALHA\x1b[0m (${duration}s)`);
|
|
26
|
+
console.info(`\n\x1b[31mDetalhes da falha em\x1b[0m \x1b[1m${step.label}\x1b[0m:`);
|
|
27
|
+
console.info('\x1b[90m' + '-'.repeat(process.stdout.columns || 50) + '\x1b[0m');
|
|
28
|
+
const commandText = step.builtinGuard
|
|
29
|
+
? formatCommand('webtoolkit', ['guard', step.builtinGuard, ...(step.args ?? [])])
|
|
30
|
+
: formatCommand(step.command, step.args ?? []);
|
|
31
|
+
console.info(result.output.trim() || `Command failed: ${commandText}`);
|
|
32
|
+
console.info('\x1b[90m' + '-'.repeat(process.stdout.columns || 50) + '\x1b[0m\n');
|
|
33
|
+
throw new Error(`Validate step "${step.label}" failed.`);
|
|
34
|
+
}
|
|
35
|
+
export async function runValidateEngine(runtime) {
|
|
36
|
+
const validate = runtime.config.validate;
|
|
37
|
+
if (!validate?.steps?.length) {
|
|
38
|
+
throw new Error('validate.steps is not configured.');
|
|
39
|
+
}
|
|
40
|
+
console.info('\nIniciando validação do monorepo...\n');
|
|
41
|
+
for (const step of validate.steps) {
|
|
42
|
+
await runStepWithStatus(step, runtime);
|
|
43
|
+
}
|
|
44
|
+
if (validate.postSteps?.length) {
|
|
45
|
+
console.info('\nVerificando pós-validação...\n');
|
|
46
|
+
for (const step of validate.postSteps) {
|
|
47
|
+
await runStepWithStatus(step, runtime);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
console.info('\n\x1b[32m✔ Validação concluida!\x1b[0m\n');
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WebToolkitCliConfig, WorkspaceTargetConfig } from './config.js';
|
|
2
|
+
type Runtime = {
|
|
3
|
+
cwd: string;
|
|
4
|
+
config: WebToolkitCliConfig;
|
|
5
|
+
};
|
|
6
|
+
type WorkspaceResult = {
|
|
7
|
+
target: WorkspaceTargetConfig;
|
|
8
|
+
command: string;
|
|
9
|
+
duration: string;
|
|
10
|
+
exitCode: number;
|
|
11
|
+
outputBuffer: string;
|
|
12
|
+
failed: boolean;
|
|
13
|
+
failedFiles: number;
|
|
14
|
+
failedTests: number;
|
|
15
|
+
failedFilesDetected: boolean;
|
|
16
|
+
failedTestsDetected: boolean;
|
|
17
|
+
skipped?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function formatFailureSummary(summary: Pick<WorkspaceResult, 'failedFiles' | 'failedTests' | 'failedTestsDetected'>): string;
|
|
20
|
+
type WorkspaceTestStatusLineOptions = {
|
|
21
|
+
failed: true;
|
|
22
|
+
duration: string;
|
|
23
|
+
summary: Pick<WorkspaceResult, 'failedFiles' | 'failedTests' | 'failedTestsDetected'>;
|
|
24
|
+
} | {
|
|
25
|
+
failed: false;
|
|
26
|
+
duration: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function formatWorkspaceTestStatusLine(options: WorkspaceTestStatusLineOptions): string;
|
|
29
|
+
export declare function progressBlockHasFailure(index: number, width: number, total: number, results: boolean[]): boolean;
|
|
30
|
+
export declare function runWorkspaceTests(runtime: Runtime, rawArgs: string[]): Promise<void>;
|
|
31
|
+
export declare function runWorkspaceCoverage(runtime: Runtime, rawArgs: string[]): Promise<void>;
|
|
32
|
+
export declare function runWorkspaceTestTask(runtime: Runtime, taskName: string, extraArgs: string[]): void;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { buildPackageManagerCommand } from './process.js';
|
|
5
|
+
const defaultTestFilePattern = '\\.(test|spec)\\.(ts|tsx|js|jsx)$';
|
|
6
|
+
const defaultIgnoreDirNames = ['node_modules', 'dist', '.git'];
|
|
7
|
+
function getWorkspaceConfig(config) {
|
|
8
|
+
if (!config.workspaceTests?.workspaces?.length) {
|
|
9
|
+
throw new Error('workspaceTests.workspaces is not configured.');
|
|
10
|
+
}
|
|
11
|
+
return config.workspaceTests;
|
|
12
|
+
}
|
|
13
|
+
function countTestFiles(dirPath, pattern, ignoreDirNames) {
|
|
14
|
+
let count = 0;
|
|
15
|
+
if (!fs.existsSync(dirPath))
|
|
16
|
+
return 0;
|
|
17
|
+
for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
|
|
18
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
if (!ignoreDirNames.has(entry.name)) {
|
|
21
|
+
count += countTestFiles(fullPath, pattern, ignoreDirNames);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (entry.isFile() && pattern.test(entry.name)) {
|
|
25
|
+
count += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return count;
|
|
29
|
+
}
|
|
30
|
+
function stripAnsi(value) {
|
|
31
|
+
return value.replace(/\x1B\[[0-9;?]*[ -/]*[@-~]/g, '');
|
|
32
|
+
}
|
|
33
|
+
function normalizeReportLine(line) {
|
|
34
|
+
return line.replace(/^(?:@[\w.-]+\/)?[\w.-]+:[\w:-]+:\s*/, '').trimEnd();
|
|
35
|
+
}
|
|
36
|
+
function findFailedTestsSectionStartIndex(lines) {
|
|
37
|
+
return lines.findIndex((line) => /-+\s*Failed Tests\b/i.test(line.trim()) || /⎯+\s*Failed Tests\b/i.test(line.trim()));
|
|
38
|
+
}
|
|
39
|
+
function isFailureStartLine(line) {
|
|
40
|
+
return /\bFailed Tests\s+\d+/i.test(line) || /^\s*FAIL\s+/.test(line) || /^\s*[×✕✖]\s+/.test(line);
|
|
41
|
+
}
|
|
42
|
+
function isFailureSummaryLine(line) {
|
|
43
|
+
return /^\s*Test Files\s+/i.test(line) ||
|
|
44
|
+
/^\s*Tests\s+/i.test(line) ||
|
|
45
|
+
/^\s*Start at\s+/i.test(line) ||
|
|
46
|
+
/^\s*Duration\s+/i.test(line) ||
|
|
47
|
+
/^\s*\[ELIFECYCLE\]/i.test(line) ||
|
|
48
|
+
/\bERROR\b.*(?:exited|run failed)/i.test(line);
|
|
49
|
+
}
|
|
50
|
+
function isFailureLogNoiseLine(line) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed)
|
|
53
|
+
return false;
|
|
54
|
+
return /^✓\s+/.test(trimmed) ||
|
|
55
|
+
/^✔\s+/.test(trimmed) ||
|
|
56
|
+
/^◇\s+injected env/i.test(trimmed) ||
|
|
57
|
+
/^Packages in scope:/i.test(trimmed) ||
|
|
58
|
+
/^Running test in /i.test(trimmed) ||
|
|
59
|
+
/^Remote caching disabled/i.test(trimmed) ||
|
|
60
|
+
/^cache (hit|miss)/i.test(trimmed) ||
|
|
61
|
+
/^replaying logs/i.test(trimmed) ||
|
|
62
|
+
/^executing /i.test(trimmed) ||
|
|
63
|
+
/^\$\s+/.test(trimmed) ||
|
|
64
|
+
/^Could not parse CSS stylesheet$/i.test(trimmed) ||
|
|
65
|
+
/was not wrapped in act\(\.\.\.\)/i.test(trimmed);
|
|
66
|
+
}
|
|
67
|
+
function clampFailureExcerptLines(lines, maxLines) {
|
|
68
|
+
if (lines.length <= maxLines)
|
|
69
|
+
return lines;
|
|
70
|
+
const headCount = Math.floor(maxLines * 0.65);
|
|
71
|
+
const tailCount = maxLines - headCount - 1;
|
|
72
|
+
return [
|
|
73
|
+
...lines.slice(0, headCount),
|
|
74
|
+
`... omitted ${lines.length - headCount - tailCount} noisy/verbose failure lines ...`,
|
|
75
|
+
...lines.slice(-tailCount),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function extractFailureExcerpt(outputBuffer, maxLines) {
|
|
79
|
+
const lines = stripAnsi(outputBuffer).split(/\r?\n/).map(normalizeReportLine);
|
|
80
|
+
const failedTestsSectionStartIndex = findFailedTestsSectionStartIndex(lines);
|
|
81
|
+
if (failedTestsSectionStartIndex >= 0) {
|
|
82
|
+
return clampFailureExcerptLines(lines.slice(failedTestsSectionStartIndex).filter((line) => !isFailureLogNoiseLine(line)), maxLines);
|
|
83
|
+
}
|
|
84
|
+
const excerpt = [];
|
|
85
|
+
let capturingFailure = false;
|
|
86
|
+
let summaryTailLines = null;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (isFailureStartLine(trimmed) || trimmed.includes('[runner-error]')) {
|
|
90
|
+
capturingFailure = true;
|
|
91
|
+
summaryTailLines = null;
|
|
92
|
+
}
|
|
93
|
+
if ((capturingFailure || isFailureSummaryLine(trimmed)) && !isFailureLogNoiseLine(trimmed)) {
|
|
94
|
+
excerpt.push(line);
|
|
95
|
+
}
|
|
96
|
+
if (capturingFailure && /^\s*Test Files\s+/i.test(trimmed)) {
|
|
97
|
+
summaryTailLines = 8;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (summaryTailLines !== null) {
|
|
101
|
+
summaryTailLines -= 1;
|
|
102
|
+
if (summaryTailLines <= 0) {
|
|
103
|
+
capturingFailure = false;
|
|
104
|
+
summaryTailLines = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return clampFailureExcerptLines(excerpt.length > 0 ? excerpt : lines.filter((line) => !isFailureLogNoiseLine(line)).slice(-80), maxLines);
|
|
109
|
+
}
|
|
110
|
+
function parseFailureSummary(outputBuffer, code, hasFailure) {
|
|
111
|
+
const cleanOutput = stripAnsi(outputBuffer);
|
|
112
|
+
const summary = {
|
|
113
|
+
failedFiles: 0,
|
|
114
|
+
failedTests: 0,
|
|
115
|
+
failedFilesDetected: false,
|
|
116
|
+
failedTestsDetected: false,
|
|
117
|
+
};
|
|
118
|
+
for (const line of cleanOutput.split(/\r?\n/)) {
|
|
119
|
+
const testFilesMatch = line.match(/Test Files\s+([^\n\r]+)/i);
|
|
120
|
+
if (testFilesMatch) {
|
|
121
|
+
const failedMatch = testFilesMatch[1].match(/(\d+)\s+failed/i);
|
|
122
|
+
if (failedMatch) {
|
|
123
|
+
summary.failedFiles = Number.parseInt(failedMatch[1], 10);
|
|
124
|
+
summary.failedFilesDetected = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const testsMatch = line.match(/(?:^|:\s*)Tests\s+([^\n\r]+)/i);
|
|
128
|
+
if (testsMatch) {
|
|
129
|
+
const failedMatch = testsMatch[1].match(/(\d+)\s+failed/i);
|
|
130
|
+
if (failedMatch) {
|
|
131
|
+
summary.failedTests = Number.parseInt(failedMatch[1], 10);
|
|
132
|
+
summary.failedTestsDetected = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if ((code !== 0 || hasFailure) && !summary.failedFilesDetected) {
|
|
137
|
+
summary.failedFiles = 1;
|
|
138
|
+
}
|
|
139
|
+
return summary;
|
|
140
|
+
}
|
|
141
|
+
export function formatFailureSummary(summary) {
|
|
142
|
+
const fileLabel = summary.failedFiles === 1 ? '1 arquivo' : `${summary.failedFiles} arquivos`;
|
|
143
|
+
if (!summary.failedTestsDetected) {
|
|
144
|
+
return `falhas nao detectadas em ${fileLabel}`;
|
|
145
|
+
}
|
|
146
|
+
const failureLabel = summary.failedTests === 1 ? '1 falha' : `${summary.failedTests} falhas`;
|
|
147
|
+
return `${failureLabel} em ${fileLabel}`;
|
|
148
|
+
}
|
|
149
|
+
export function formatWorkspaceTestStatusLine(options) {
|
|
150
|
+
if (options.failed) {
|
|
151
|
+
return `\x1b[31mERRO\x1b[0m - ${formatFailureSummary(options.summary)} (${options.duration}s)`;
|
|
152
|
+
}
|
|
153
|
+
return `\x1b[32mOK\x1b[0m (${options.duration}s)`;
|
|
154
|
+
}
|
|
155
|
+
export function progressBlockHasFailure(index, width, total, results) {
|
|
156
|
+
const safeTotal = Math.max(total, 1);
|
|
157
|
+
const start = Math.floor((index * safeTotal) / width);
|
|
158
|
+
const end = Math.ceil(((index + 1) * safeTotal) / width);
|
|
159
|
+
for (let resultIndex = start; resultIndex < end && resultIndex < results.length; resultIndex += 1) {
|
|
160
|
+
if (results[resultIndex] === false)
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
function drawProgressBar(label, name, completed, total, results, coverage = null) {
|
|
166
|
+
const safeTotal = Math.max(total, 1);
|
|
167
|
+
const safeCompleted = Math.min(Math.max(completed, 0), safeTotal);
|
|
168
|
+
const width = label === 'Coverage' ? 40 : 60;
|
|
169
|
+
const percentage = Math.min(Math.floor((safeCompleted / safeTotal) * 100), 100);
|
|
170
|
+
const filledChars = Math.min(Math.floor((safeCompleted / safeTotal) * width), width);
|
|
171
|
+
let barStr = '';
|
|
172
|
+
for (let index = 0; index < width; index += 1) {
|
|
173
|
+
if (index < filledChars) {
|
|
174
|
+
const failed = progressBlockHasFailure(index, width, safeTotal, results);
|
|
175
|
+
barStr += `${failed ? '\x1b[31m' : '\x1b[32m'}█\x1b[0m`;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
barStr += '\x1b[90m░\x1b[0m';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const coverageText = coverage === null
|
|
182
|
+
? ''
|
|
183
|
+
: ` | Coverage: ${(coverage >= 80 ? '\x1b[32m' : coverage >= 50 ? '\x1b[33m' : '\x1b[31m')}${coverage.toFixed(1)}%\x1b[0m`;
|
|
184
|
+
process.stdout.write(`\r- ${label} \x1b[1m${name.padEnd(10)}\x1b[0m [${barStr}] ${percentage}% (${safeCompleted}/${total})${coverageText} `);
|
|
185
|
+
}
|
|
186
|
+
function parseTestFileLine(line) {
|
|
187
|
+
const cleanLine = stripAnsi(line);
|
|
188
|
+
const match = cleanLine.match(/(✓|×|✕|✖|FAIL|PASS|failed|passed)\s+(.+?(\.test\.|\.spec\.)[a-zA-Z]+)/i);
|
|
189
|
+
if (!match)
|
|
190
|
+
return null;
|
|
191
|
+
const status = match[1].toUpperCase();
|
|
192
|
+
return {
|
|
193
|
+
filePath: match[2].trim(),
|
|
194
|
+
isSuccess: status === '✓' || status.includes('PASS'),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function resolveTargets(runtime, rawArgs) {
|
|
198
|
+
const testConfig = getWorkspaceConfig(runtime.config);
|
|
199
|
+
let targets = testConfig.workspaces;
|
|
200
|
+
const filterValue = getFilterValue(rawArgs);
|
|
201
|
+
if (filterValue) {
|
|
202
|
+
targets = targets.filter((target) => target.package === filterValue || target.name.toLowerCase() === filterValue.toLowerCase());
|
|
203
|
+
}
|
|
204
|
+
else if (process.env.INIT_CWD) {
|
|
205
|
+
const initCwd = path.resolve(process.env.INIT_CWD);
|
|
206
|
+
const matched = targets.find((target) => path.resolve(runtime.cwd, target.path) === initCwd);
|
|
207
|
+
if (matched)
|
|
208
|
+
targets = [matched];
|
|
209
|
+
}
|
|
210
|
+
return targets;
|
|
211
|
+
}
|
|
212
|
+
async function runWorkspaceTest(target, runtime) {
|
|
213
|
+
const testConfig = getWorkspaceConfig(runtime.config);
|
|
214
|
+
const pattern = new RegExp(testConfig.testFilePattern ?? defaultTestFilePattern);
|
|
215
|
+
const ignoreDirNames = new Set(testConfig.ignoreDirNames ?? defaultIgnoreDirNames);
|
|
216
|
+
const absPath = path.join(runtime.cwd, target.path);
|
|
217
|
+
const totalFiles = countTestFiles(absPath, pattern, ignoreDirNames);
|
|
218
|
+
process.stdout.write(`- Testing \x1b[1m${target.name.padEnd(10)}\x1b[0m Preparando...`);
|
|
219
|
+
if (totalFiles === 0) {
|
|
220
|
+
console.info(`\r- Testing \x1b[1m${target.name.padEnd(10)}\x1b[0m \x1b[33mSKIPPED (No tests found)\x1b[0m `);
|
|
221
|
+
return {
|
|
222
|
+
target,
|
|
223
|
+
skipped: true,
|
|
224
|
+
command: '',
|
|
225
|
+
duration: '0.0',
|
|
226
|
+
exitCode: 0,
|
|
227
|
+
outputBuffer: '',
|
|
228
|
+
failed: false,
|
|
229
|
+
failedFiles: 0,
|
|
230
|
+
failedTests: 0,
|
|
231
|
+
failedFilesDetected: true,
|
|
232
|
+
failedTestsDetected: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const results = [];
|
|
236
|
+
const processedFiles = new Set();
|
|
237
|
+
let hasFailure = false;
|
|
238
|
+
let outputBuffer = '';
|
|
239
|
+
const startedAt = Date.now();
|
|
240
|
+
const commandSpec = buildPackageManagerCommand(runtime.config.packageManager, ['turbo', 'run', 'test', '--filter', target.package, '--', '--reporter=verbose']);
|
|
241
|
+
const commandText = `${commandSpec.command} ${(commandSpec.args ?? []).join(' ')}`;
|
|
242
|
+
drawProgressBar('Testing', target.name, 0, totalFiles, results);
|
|
243
|
+
return await new Promise((resolve) => {
|
|
244
|
+
let finished = false;
|
|
245
|
+
const finish = (code) => {
|
|
246
|
+
if (finished)
|
|
247
|
+
return;
|
|
248
|
+
finished = true;
|
|
249
|
+
const duration = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
250
|
+
const exitCode = code ?? 1;
|
|
251
|
+
const summary = parseFailureSummary(outputBuffer, exitCode, hasFailure);
|
|
252
|
+
const failed = exitCode !== 0 || hasFailure;
|
|
253
|
+
if (failed && !results.includes(false)) {
|
|
254
|
+
results[Math.max(0, Math.min(totalFiles, Math.max(results.length, 1)) - 1)] = false;
|
|
255
|
+
}
|
|
256
|
+
drawProgressBar('Testing', target.name, totalFiles, totalFiles, results);
|
|
257
|
+
console.info(formatWorkspaceTestStatusLine(failed
|
|
258
|
+
? { failed, duration, summary }
|
|
259
|
+
: { failed, duration }));
|
|
260
|
+
resolve({ target, command: commandText, duration, exitCode, outputBuffer, failed, ...summary });
|
|
261
|
+
};
|
|
262
|
+
const child = spawn(commandSpec.command, commandSpec.args ?? [], {
|
|
263
|
+
cwd: runtime.cwd,
|
|
264
|
+
env: { ...process.env, FORCE_COLOR: '1', CI: '1', NODE_OPTIONS: '--no-deprecation' },
|
|
265
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
266
|
+
shell: false,
|
|
267
|
+
});
|
|
268
|
+
child.stdout.on('data', (data) => {
|
|
269
|
+
const text = data.toString();
|
|
270
|
+
outputBuffer += text;
|
|
271
|
+
for (const line of text.split(/\r?\n/)) {
|
|
272
|
+
const parsedLine = parseTestFileLine(line);
|
|
273
|
+
if (!parsedLine || processedFiles.has(parsedLine.filePath))
|
|
274
|
+
continue;
|
|
275
|
+
processedFiles.add(parsedLine.filePath);
|
|
276
|
+
if (!parsedLine.isSuccess)
|
|
277
|
+
hasFailure = true;
|
|
278
|
+
results.push(parsedLine.isSuccess);
|
|
279
|
+
drawProgressBar('Testing', target.name, processedFiles.size, totalFiles, results);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
child.stderr.on('data', (data) => {
|
|
283
|
+
outputBuffer += data.toString();
|
|
284
|
+
});
|
|
285
|
+
child.on('error', (error) => {
|
|
286
|
+
hasFailure = true;
|
|
287
|
+
outputBuffer += `\n[runner-error] ${error.stack || error.message}\n`;
|
|
288
|
+
finish(1);
|
|
289
|
+
});
|
|
290
|
+
child.on('close', finish);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function buildErrorLog(results, runtime) {
|
|
294
|
+
const failedResults = results.filter((result) => result.failed);
|
|
295
|
+
if (failedResults.length === 0)
|
|
296
|
+
return '';
|
|
297
|
+
const testConfig = getWorkspaceConfig(runtime.config);
|
|
298
|
+
const maxLines = testConfig.maxFailureExcerptLines ?? 280;
|
|
299
|
+
const sections = [
|
|
300
|
+
'Workspace test failure report',
|
|
301
|
+
`Generated at: ${new Date().toISOString()}`,
|
|
302
|
+
`Root: ${runtime.cwd}`,
|
|
303
|
+
`Failed workspaces: ${failedResults.length}`,
|
|
304
|
+
'',
|
|
305
|
+
];
|
|
306
|
+
for (const result of failedResults) {
|
|
307
|
+
sections.push('='.repeat(100));
|
|
308
|
+
sections.push(`Workspace: ${result.target.name} (${result.target.package})`);
|
|
309
|
+
sections.push(`Command: ${result.command}`);
|
|
310
|
+
sections.push(`Exit code: ${result.exitCode}`);
|
|
311
|
+
sections.push(`Duration: ${result.duration}s`);
|
|
312
|
+
sections.push(`Failed files: ${result.failedFiles}${result.failedFilesDetected ? '' : ' (fallback)'}`);
|
|
313
|
+
sections.push(`Failed tests: ${result.failedTestsDetected ? result.failedTests : 'not detected'}`);
|
|
314
|
+
sections.push('-'.repeat(100));
|
|
315
|
+
sections.push('Failure excerpt (noise-filtered):');
|
|
316
|
+
sections.push(...extractFailureExcerpt(result.outputBuffer, maxLines));
|
|
317
|
+
sections.push('');
|
|
318
|
+
}
|
|
319
|
+
return `${sections.join('\n')}\n`;
|
|
320
|
+
}
|
|
321
|
+
export async function runWorkspaceTests(runtime, rawArgs) {
|
|
322
|
+
const testConfig = getWorkspaceConfig(runtime.config);
|
|
323
|
+
const { testFiles, extraArgs } = splitArgs(rawArgs);
|
|
324
|
+
if (testFiles.length > 0) {
|
|
325
|
+
runMultipleWorkspaceTestFiles(runtime, testFiles, extraArgs);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
console.info('\nIniciando bateria completa de testes...\n');
|
|
329
|
+
const errorLogPath = path.join(runtime.cwd, testConfig.errorLogFile ?? 'tests_output_errors.log');
|
|
330
|
+
fs.rmSync(errorLogPath, { force: true });
|
|
331
|
+
const results = [];
|
|
332
|
+
for (const target of resolveTargets(runtime, rawArgs)) {
|
|
333
|
+
results.push(await runWorkspaceTest(target, runtime));
|
|
334
|
+
}
|
|
335
|
+
const failedResults = results.filter((result) => result.failed);
|
|
336
|
+
if (failedResults.length > 0) {
|
|
337
|
+
fs.writeFileSync(errorLogPath, buildErrorLog(results, runtime), 'utf8');
|
|
338
|
+
console.info('\n\x1b[31mResumo de falhas:\x1b[0m');
|
|
339
|
+
for (const result of failedResults) {
|
|
340
|
+
console.info(`- ${result.target.name}: ${formatFailureSummary(result)}`);
|
|
341
|
+
}
|
|
342
|
+
console.info(`\nDetalhes consolidados em ${path.relative(runtime.cwd, errorLogPath)}`);
|
|
343
|
+
throw new Error('Workspace tests failed.');
|
|
344
|
+
}
|
|
345
|
+
console.info('\n\x1b[32m✔ Todos os testes passaram com sucesso!\x1b[0m');
|
|
346
|
+
}
|
|
347
|
+
export async function runWorkspaceCoverage(runtime, rawArgs) {
|
|
348
|
+
const testConfig = getWorkspaceConfig(runtime.config);
|
|
349
|
+
const pattern = new RegExp(testConfig.testFilePattern ?? defaultTestFilePattern);
|
|
350
|
+
const ignoreDirNames = new Set(testConfig.ignoreDirNames ?? defaultIgnoreDirNames);
|
|
351
|
+
console.info('\nIniciando bateria completa de cobertura de testes...\n');
|
|
352
|
+
for (const target of resolveTargets(runtime, rawArgs)) {
|
|
353
|
+
const totalFiles = countTestFiles(path.join(runtime.cwd, target.path), pattern, ignoreDirNames);
|
|
354
|
+
process.stdout.write(`- Coverage \x1b[1m${target.name.padEnd(10)}\x1b[0m Preparando...`);
|
|
355
|
+
if (totalFiles === 0) {
|
|
356
|
+
console.info(`\r- Coverage \x1b[1m${target.name.padEnd(10)}\x1b[0m \x1b[33mSKIPPED (No tests found)\x1b[0m `);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const startedAt = Date.now();
|
|
360
|
+
const commandSpec = buildPackageManagerCommand(runtime.config.packageManager, ['turbo', 'run', 'test:coverage', '--filter', target.package]);
|
|
361
|
+
let processedFiles = 0;
|
|
362
|
+
let totalCoverage = null;
|
|
363
|
+
let outputBuffer = '';
|
|
364
|
+
const results = [];
|
|
365
|
+
drawProgressBar('Coverage', target.name, 0, totalFiles, results, totalCoverage);
|
|
366
|
+
const code = await new Promise((resolve) => {
|
|
367
|
+
const child = spawn(commandSpec.command, commandSpec.args ?? [], {
|
|
368
|
+
cwd: runtime.cwd,
|
|
369
|
+
env: { ...process.env, FORCE_COLOR: '1', CI: '1', NODE_OPTIONS: '--no-deprecation' },
|
|
370
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
371
|
+
shell: false,
|
|
372
|
+
});
|
|
373
|
+
child.stdout.on('data', (data) => {
|
|
374
|
+
const text = data.toString();
|
|
375
|
+
outputBuffer += text;
|
|
376
|
+
for (const line of text.split(/\r?\n/)) {
|
|
377
|
+
const cleanLine = stripAnsi(line).trim();
|
|
378
|
+
if (parseTestFileLine(cleanLine)) {
|
|
379
|
+
processedFiles += 1;
|
|
380
|
+
results.push(true);
|
|
381
|
+
drawProgressBar('Coverage', target.name, Math.min(processedFiles, totalFiles), totalFiles, results, totalCoverage);
|
|
382
|
+
}
|
|
383
|
+
const covMatch = cleanLine.match(/All files\s+\|\s+([\d.]+)\s+\|\s+([\d.]+)\s+\|\s+([\d.]+)\s+\|\s+([\d.]+)/);
|
|
384
|
+
if (covMatch) {
|
|
385
|
+
totalCoverage = Number.parseFloat(covMatch[4]);
|
|
386
|
+
drawProgressBar('Coverage', target.name, totalFiles, totalFiles, results, totalCoverage);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
child.stderr.on('data', (data) => {
|
|
391
|
+
outputBuffer += data.toString();
|
|
392
|
+
});
|
|
393
|
+
child.on('close', (exitCode) => resolve(exitCode ?? 1));
|
|
394
|
+
child.on('error', () => resolve(1));
|
|
395
|
+
});
|
|
396
|
+
const duration = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
397
|
+
drawProgressBar('Coverage', target.name, totalFiles, totalFiles, results, totalCoverage);
|
|
398
|
+
if (code !== 0) {
|
|
399
|
+
console.info(`\x1b[31m FALHA\x1b[0m (${duration}s)`);
|
|
400
|
+
console.info(outputBuffer);
|
|
401
|
+
throw new Error(`Coverage failed for ${target.name}.`);
|
|
402
|
+
}
|
|
403
|
+
console.info(`\x1b[32m OK\x1b[0m (${duration}s)`);
|
|
404
|
+
}
|
|
405
|
+
console.info('\n\x1b[32m✔ Relatórios de cobertura gerados com sucesso!\x1b[0m');
|
|
406
|
+
}
|
|
407
|
+
export function runWorkspaceTestTask(runtime, taskName, extraArgs) {
|
|
408
|
+
const supportedTasks = new Set(['test', 'test:coverage']);
|
|
409
|
+
if (!supportedTasks.has(taskName)) {
|
|
410
|
+
throw new Error(`Unsupported workspace test task: ${taskName}`);
|
|
411
|
+
}
|
|
412
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
413
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
414
|
+
throw new Error(`Could not find package.json in ${process.cwd()}`);
|
|
415
|
+
}
|
|
416
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
417
|
+
if (!packageJson.name) {
|
|
418
|
+
throw new Error(`Could not resolve package name from ${packageJsonPath}`);
|
|
419
|
+
}
|
|
420
|
+
const hasTurboContext = [
|
|
421
|
+
process.env.TURBO_HASH,
|
|
422
|
+
process.env.TURBO_TASK_ID,
|
|
423
|
+
process.env.TURBO_TASK,
|
|
424
|
+
process.env.TURBO_PACKAGE_NAME,
|
|
425
|
+
process.env.TURBO_INVOCATION_DIR,
|
|
426
|
+
].some((value) => typeof value === 'string' && value.length > 0);
|
|
427
|
+
const packageDir = process.cwd();
|
|
428
|
+
const initCwd = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : null;
|
|
429
|
+
const isInvokedFromOutsidePackage = initCwd !== null && !isSameOrInsidePath(initCwd, packageDir);
|
|
430
|
+
const isRunningInsideTurbo = process.env.WEBTOOLKIT_WORKSPACE_TEST_TURBO === '1' || hasTurboContext || isInvokedFromOutsidePackage;
|
|
431
|
+
const commandSpec = isRunningInsideTurbo
|
|
432
|
+
? buildPackageManagerCommand(runtime.config.packageManager, taskName === 'test:coverage'
|
|
433
|
+
? ['exec', 'vitest', 'run', '--coverage', ...extraArgs]
|
|
434
|
+
: ['exec', 'vitest', 'run', ...extraArgs])
|
|
435
|
+
: buildPackageManagerCommand(runtime.config.packageManager, extraArgs.length > 0
|
|
436
|
+
? ['turbo', 'run', taskName, `--filter=${packageJson.name}`, '--', ...extraArgs]
|
|
437
|
+
: ['turbo', 'run', taskName, `--filter=${packageJson.name}`]);
|
|
438
|
+
const result = spawnSync(commandSpec.command, commandSpec.args ?? [], {
|
|
439
|
+
cwd: isRunningInsideTurbo ? packageDir : runtime.cwd,
|
|
440
|
+
env: {
|
|
441
|
+
...process.env,
|
|
442
|
+
FORCE_COLOR: process.env.FORCE_COLOR || '1',
|
|
443
|
+
WEBTOOLKIT_WORKSPACE_TEST_TURBO: isRunningInsideTurbo ? process.env.WEBTOOLKIT_WORKSPACE_TEST_TURBO : '1',
|
|
444
|
+
},
|
|
445
|
+
stdio: 'inherit',
|
|
446
|
+
shell: false,
|
|
447
|
+
});
|
|
448
|
+
if (result.error)
|
|
449
|
+
throw result.error;
|
|
450
|
+
process.exit(result.status ?? 1);
|
|
451
|
+
}
|
|
452
|
+
function isSameOrInsidePath(childPath, parentPath) {
|
|
453
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
454
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
455
|
+
}
|
|
456
|
+
const flagsWithValue = new Set([
|
|
457
|
+
'--config',
|
|
458
|
+
'--configLoader',
|
|
459
|
+
'--filter',
|
|
460
|
+
'--hookTimeout',
|
|
461
|
+
'--maxWorkers',
|
|
462
|
+
'--minWorkers',
|
|
463
|
+
'--pool',
|
|
464
|
+
'--reporter',
|
|
465
|
+
'--sequence.seed',
|
|
466
|
+
'--sequence.shuffle',
|
|
467
|
+
'--testNamePattern',
|
|
468
|
+
'--testTimeout',
|
|
469
|
+
'-t',
|
|
470
|
+
]);
|
|
471
|
+
function splitArgs(rawArgs) {
|
|
472
|
+
const testFiles = [];
|
|
473
|
+
const extraArgs = [];
|
|
474
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
475
|
+
const arg = rawArgs[index];
|
|
476
|
+
if (arg.startsWith('-')) {
|
|
477
|
+
extraArgs.push(arg);
|
|
478
|
+
if (flagsWithValue.has(arg) && rawArgs[index + 1] && !rawArgs[index + 1].startsWith('-')) {
|
|
479
|
+
extraArgs.push(rawArgs[index + 1]);
|
|
480
|
+
index += 1;
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
testFiles.push(arg);
|
|
485
|
+
}
|
|
486
|
+
return { testFiles, extraArgs };
|
|
487
|
+
}
|
|
488
|
+
function getFilterValue(rawArgs) {
|
|
489
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
490
|
+
const arg = rawArgs[index];
|
|
491
|
+
if (arg === '--filter' && rawArgs[index + 1])
|
|
492
|
+
return rawArgs[index + 1];
|
|
493
|
+
if (arg.startsWith('--filter='))
|
|
494
|
+
return arg.slice('--filter='.length);
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
function runMultipleWorkspaceTestFiles(runtime, testFiles, extraArgs) {
|
|
499
|
+
const testConfig = getWorkspaceConfig(runtime.config);
|
|
500
|
+
const byWorkspace = new Map();
|
|
501
|
+
for (const testFile of testFiles) {
|
|
502
|
+
const absPath = path.resolve(process.cwd(), testFile);
|
|
503
|
+
const target = testConfig.workspaces.find((workspace) => isSameOrInsidePath(absPath, path.resolve(runtime.cwd, workspace.path)));
|
|
504
|
+
if (!target) {
|
|
505
|
+
throw new Error(`O arquivo ${testFile} nao pertence a nenhum workspace conhecido.`);
|
|
506
|
+
}
|
|
507
|
+
const current = byWorkspace.get(target.package) ?? {
|
|
508
|
+
absPkgPath: path.resolve(runtime.cwd, target.path),
|
|
509
|
+
filePaths: [],
|
|
510
|
+
};
|
|
511
|
+
current.filePaths.push(absPath);
|
|
512
|
+
byWorkspace.set(target.package, current);
|
|
513
|
+
}
|
|
514
|
+
const flagArgs = extraArgs.filter((arg) => arg.startsWith('-'));
|
|
515
|
+
for (const [targetPackage, workspaceData] of byWorkspace.entries()) {
|
|
516
|
+
console.info(`\nExecutando testes em \x1b[1m${targetPackage}\x1b[0m...\n`);
|
|
517
|
+
const fileArgs = workspaceData.filePaths.map((filePath) => path.relative(workspaceData.absPkgPath, filePath).split(path.sep).join('/'));
|
|
518
|
+
const commandSpec = buildPackageManagerCommand(runtime.config.packageManager, ['--filter', targetPackage, 'run', 'test', ...fileArgs, ...flagArgs]);
|
|
519
|
+
const result = spawnSync(commandSpec.command, commandSpec.args ?? [], {
|
|
520
|
+
cwd: runtime.cwd,
|
|
521
|
+
stdio: 'inherit',
|
|
522
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
523
|
+
});
|
|
524
|
+
if (result.error)
|
|
525
|
+
throw result.error;
|
|
526
|
+
if ((result.status ?? 1) !== 0)
|
|
527
|
+
process.exit(result.status ?? 1);
|
|
528
|
+
}
|
|
529
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@titannio/webtoolkit-cli",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Portable CLI tools for TypeScript monorepos and web projects.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cli",
|
|
8
|
+
"tooling",
|
|
9
|
+
"typescript",
|
|
10
|
+
"monorepo",
|
|
11
|
+
"maintenance"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"webtoolkit": "dist/bin.js",
|
|
17
|
+
"webtoolkit-cli": "dist/bin.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/**/*",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=26.0.0 <27"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
32
|
+
"type-check": "tsc --noEmit",
|
|
33
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true }); require('fs').rmSync('.tsbuildinfo', { force: true })\"",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:coverage": "vitest run --coverage",
|
|
36
|
+
"verify": "npm run type-check && npm run test:coverage && npm run build && npm run npm:pack",
|
|
37
|
+
"release:check": "npm run verify",
|
|
38
|
+
"prepack": "npm run test:coverage && npm run build",
|
|
39
|
+
"prepublishOnly": "npm run verify",
|
|
40
|
+
"npm:pack": "npm pack --dry-run",
|
|
41
|
+
"deps:update": "npm update",
|
|
42
|
+
"npm:release": "npm publish --access public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"ajv": "^8.20.0",
|
|
46
|
+
"dependency-cruiser": "^18.0.0",
|
|
47
|
+
"semver": "^7.8.3",
|
|
48
|
+
"ts-morph": "^28.0.0",
|
|
49
|
+
"typescript": "^6.0.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^26.1.0",
|
|
53
|
+
"@types/semver": "^7.7.1",
|
|
54
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
55
|
+
"vitest": "^4.1.9"
|
|
56
|
+
}
|
|
57
|
+
}
|