@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +268 -0
  5. package/dist/bundle-audit.d.ts +7 -0
  6. package/dist/bundle-audit.js +111 -0
  7. package/dist/cleaner.d.ts +15 -0
  8. package/dist/cleaner.js +359 -0
  9. package/dist/config-reference.d.ts +8 -0
  10. package/dist/config-reference.js +805 -0
  11. package/dist/config.d.ts +306 -0
  12. package/dist/config.js +173 -0
  13. package/dist/dev-grid.d.ts +7 -0
  14. package/dist/dev-grid.js +181 -0
  15. package/dist/dev-watch.d.ts +13 -0
  16. package/dist/dev-watch.js +184 -0
  17. package/dist/environment.d.ts +10 -0
  18. package/dist/environment.js +172 -0
  19. package/dist/guard-registry.d.ts +1 -0
  20. package/dist/guard-registry.js +17 -0
  21. package/dist/guard-runner.d.ts +4 -0
  22. package/dist/guard-runner.js +36 -0
  23. package/dist/guards/any-guard.d.ts +16 -0
  24. package/dist/guards/any-guard.js +121 -0
  25. package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
  26. package/dist/guards/assert-no-tests-in-dist.js +56 -0
  27. package/dist/guards/check-mojibake.d.ts +52 -0
  28. package/dist/guards/check-mojibake.js +378 -0
  29. package/dist/guards/code-pattern-guard.d.ts +71 -0
  30. package/dist/guards/code-pattern-guard.js +654 -0
  31. package/dist/guards/dal-service-repository-check.d.ts +13 -0
  32. package/dist/guards/dal-service-repository-check.js +264 -0
  33. package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
  34. package/dist/guards/dependency-cruiser-guard.js +69 -0
  35. package/dist/guards/documentation-guard.d.ts +3 -0
  36. package/dist/guards/documentation-guard.js +370 -0
  37. package/dist/guards/guard-config.d.ts +19 -0
  38. package/dist/guards/guard-config.js +87 -0
  39. package/dist/guards/internal-link-guard.d.ts +12 -0
  40. package/dist/guards/internal-link-guard.js +272 -0
  41. package/dist/guards/package-surface-guard.d.ts +37 -0
  42. package/dist/guards/package-surface-guard.js +234 -0
  43. package/dist/guards/pnpm-workspace-config.d.ts +2 -0
  44. package/dist/guards/pnpm-workspace-config.js +40 -0
  45. package/dist/guards/rebuild-preflight.d.ts +29 -0
  46. package/dist/guards/rebuild-preflight.js +137 -0
  47. package/dist/guards/repository-hygiene-guard.d.ts +12 -0
  48. package/dist/guards/repository-hygiene-guard.js +70 -0
  49. package/dist/guards/schema-guard.d.ts +10 -0
  50. package/dist/guards/schema-guard.js +160 -0
  51. package/dist/guards/singleton-deps-guard.d.ts +21 -0
  52. package/dist/guards/singleton-deps-guard.js +183 -0
  53. package/dist/guards/tsconfig-guard.d.ts +5 -0
  54. package/dist/guards/tsconfig-guard.js +105 -0
  55. package/dist/guards/workspace-manifest-guard.d.ts +21 -0
  56. package/dist/guards/workspace-manifest-guard.js +210 -0
  57. package/dist/jsdoc-report.d.ts +7 -0
  58. package/dist/jsdoc-report.js +456 -0
  59. package/dist/process.d.ts +20 -0
  60. package/dist/process.js +86 -0
  61. package/dist/ready-service.d.ts +7 -0
  62. package/dist/ready-service.js +135 -0
  63. package/dist/release-gate.d.ts +7 -0
  64. package/dist/release-gate.js +35 -0
  65. package/dist/repo-check.d.ts +7 -0
  66. package/dist/repo-check.js +121 -0
  67. package/dist/tasks.d.ts +17 -0
  68. package/dist/tasks.js +195 -0
  69. package/dist/upgrade.d.ts +10 -0
  70. package/dist/upgrade.js +674 -0
  71. package/dist/validate.d.ts +7 -0
  72. package/dist/validate.js +51 -0
  73. package/dist/workspace-tests.d.ts +33 -0
  74. package/dist/workspace-tests.js +529 -0
  75. package/package.json +57 -0
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * DAL + Service + Repository Compliance Report
4
+ *
5
+ * Analisa a camada backend e identifica violacoes de fronteira arquitetural
6
+ * entre Controllers, Services, Repositories, Routes, Middlewares e o agregador
7
+ * central de DAL.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import ts from 'typescript';
12
+ import { BASE_LAYER_EXCLUDE_PATTERNS, BASE_TYPESCRIPT_EXTENSIONS, assertConfiguredScanScope, compilePatterns, hasExtension, isMainModule, loadGuardConfig, resolveProjectPath } from './guard-config.js';
13
+ const colors = {
14
+ reset: '\x1b[0m',
15
+ bright: '\x1b[1m',
16
+ red: '\x1b[31m',
17
+ green: '\x1b[32m',
18
+ yellow: '\x1b[33m',
19
+ blue: '\x1b[34m',
20
+ cyan: '\x1b[36m',
21
+ gray: '\x1b[90m',
22
+ };
23
+ function normalizeFilePath(filePath) {
24
+ return filePath.replace(/\\/g, '/');
25
+ }
26
+ function isAllowedFile(filePath, excludePatterns) {
27
+ if (!hasExtension(filePath, BASE_TYPESCRIPT_EXTENSIONS))
28
+ return false;
29
+ return !excludePatterns.some((pattern) => pattern.test(filePath));
30
+ }
31
+ function collectFiles(rootDir, excludePatterns) {
32
+ const files = [];
33
+ const walk = (dir) => {
34
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory())
35
+ return;
36
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37
+ const fullPath = path.join(dir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ if (!excludePatterns.some((pattern) => pattern.test(fullPath))) {
40
+ walk(fullPath);
41
+ }
42
+ continue;
43
+ }
44
+ if (entry.isFile() && isAllowedFile(fullPath, excludePatterns)) {
45
+ files.push(fullPath);
46
+ }
47
+ }
48
+ };
49
+ walk(rootDir);
50
+ return files;
51
+ }
52
+ function readCompilerOptions(tsconfigPath) {
53
+ let configResult;
54
+ try {
55
+ configResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
56
+ }
57
+ catch (error) {
58
+ throw new Error(`Failed to read backend tsconfig: ${error.message}`);
59
+ }
60
+ if (configResult.error) {
61
+ const message = ts.flattenDiagnosticMessageText(configResult.error.messageText, '\n');
62
+ throw new Error(`Failed to read backend tsconfig: ${message}`);
63
+ }
64
+ const parsed = ts.parseJsonConfigFileContent(configResult.config, ts.sys, path.dirname(tsconfigPath));
65
+ return parsed.options;
66
+ }
67
+ function matchesPath(relativePath, configuredPath) {
68
+ const normalized = normalizeFilePath(configuredPath).replace(/^\/|\/$/g, '');
69
+ return relativePath === normalized || relativePath.startsWith(`${normalized}/`);
70
+ }
71
+ function classifyLayer(filePath, sourceDirectory, layers) {
72
+ const normalized = normalizeFilePath(path.resolve(filePath));
73
+ const relative = normalizeFilePath(path.relative(sourceDirectory, normalized));
74
+ for (const layer of layers) {
75
+ if ((layer.exclude ?? []).some((excluded) => matchesPath(relative, excluded)))
76
+ continue;
77
+ if (layer.paths.some((candidate) => matchesPath(relative, candidate)))
78
+ return layer.name;
79
+ }
80
+ return 'other';
81
+ }
82
+ function isInternalSourceFile(filePath, sourceDirectory) {
83
+ const relativePath = path.relative(sourceDirectory, path.resolve(filePath));
84
+ return relativePath !== '..' && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath);
85
+ }
86
+ function resolveImportTarget(sourceFileName, moduleSpecifier, compilerOptions) {
87
+ const resolved = ts.resolveModuleName(moduleSpecifier, sourceFileName, compilerOptions, ts.sys);
88
+ return resolved.resolvedModule?.resolvedFileName ?? null;
89
+ }
90
+ function isViolation(sourceLayer, targetLayer, forbiddenDependencies) {
91
+ return (forbiddenDependencies[sourceLayer] ?? []).includes(targetLayer);
92
+ }
93
+ function buildViolationMessage(sourceLayer, targetLayer) {
94
+ return `${sourceLayer} cannot depend directly on ${targetLayer}`;
95
+ }
96
+ function formatPercent(value) {
97
+ return `${value.toFixed(1)}%`;
98
+ }
99
+ function printHeader() {
100
+ console.log(`${colors.bright}${colors.blue}╔═══════════════════════════════════════════════════════════════╗${colors.reset}`);
101
+ console.log(`${colors.bright}${colors.blue}║ DAL + Service + Repository Compliance Report ║${colors.reset}`);
102
+ console.log(`${colors.bright}${colors.blue}╚═══════════════════════════════════════════════════════════════╝${colors.reset}`);
103
+ console.log();
104
+ }
105
+ function getLineNumber(sourceFile, position) {
106
+ const lineAndChar = sourceFile.getLineAndCharacterOfPosition(position);
107
+ return lineAndChar.line + 1;
108
+ }
109
+ export async function runDalServiceRepositoryGuard(options = {}) {
110
+ const rootDir = options.rootDir ?? process.cwd();
111
+ const config = options.config ?? await loadGuardConfig('dalServiceRepository', rootDir);
112
+ /* v8 ignore next -- every invalid operand and the valid fallthrough are asserted */
113
+ if (!config.sourceDirectory || !config.tsconfig || config.layers.length === 0) {
114
+ throw new Error('guards.dalServiceRepository requires sourceDirectory, tsconfig and non-empty layers.');
115
+ }
116
+ const sourceDirectory = resolveProjectPath(rootDir, config.sourceDirectory);
117
+ const tsconfigPath = resolveProjectPath(rootDir, config.tsconfig);
118
+ const excludePatterns = compilePatterns(config.excludePatterns, BASE_LAYER_EXCLUDE_PATTERNS);
119
+ const evaluatedLayers = new Set(config.layers.map((layer) => layer.name));
120
+ printHeader();
121
+ console.log(`${colors.cyan}Scope:${colors.reset} ${normalizeFilePath(path.relative(rootDir, sourceDirectory))}`);
122
+ console.log(`${colors.cyan}Rules:${colors.reset}`);
123
+ for (const [layer, forbidden] of Object.entries(config.forbiddenDependencies)) {
124
+ console.log(` - ${layer} cannot import ${forbidden.join(', ')}`);
125
+ }
126
+ console.log();
127
+ const compilerOptions = readCompilerOptions(tsconfigPath);
128
+ const files = collectFiles(sourceDirectory, excludePatterns);
129
+ assertConfiguredScanScope({
130
+ root: rootDir,
131
+ guardName: 'dal-service-repository',
132
+ configPath: 'guards.dalServiceRepository.sourceDirectory',
133
+ configuredPaths: [config.sourceDirectory],
134
+ eligibleFiles: files,
135
+ });
136
+ const reports = [];
137
+ const ruleCounts = new Map();
138
+ const layerCounts = new Map();
139
+ let evaluatedFiles = 0;
140
+ let compliantFiles = 0;
141
+ let totalViolations = 0;
142
+ for (const filePath of files) {
143
+ const content = fs.readFileSync(filePath, 'utf8');
144
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
145
+ const layer = classifyLayer(filePath, sourceDirectory, config.layers);
146
+ const evaluated = evaluatedLayers.has(layer);
147
+ const violations = [];
148
+ if (evaluated)
149
+ evaluatedFiles++;
150
+ if (evaluated) {
151
+ layerCounts.set(layer, (layerCounts.get(layer) ?? 0) + 1);
152
+ }
153
+ for (const statement of sourceFile.statements) {
154
+ if (!ts.isImportDeclaration(statement))
155
+ continue;
156
+ if (!ts.isStringLiteralLike(statement.moduleSpecifier))
157
+ continue;
158
+ const moduleSpecifier = statement.moduleSpecifier.text;
159
+ const resolvedFileName = resolveImportTarget(filePath, moduleSpecifier, compilerOptions);
160
+ if (!resolvedFileName)
161
+ continue;
162
+ if (!isInternalSourceFile(resolvedFileName, sourceDirectory))
163
+ continue;
164
+ const targetLayer = classifyLayer(resolvedFileName, sourceDirectory, config.layers);
165
+ if (targetLayer === 'other')
166
+ continue;
167
+ if (!evaluated)
168
+ continue;
169
+ if (!isViolation(layer, targetLayer, config.forbiddenDependencies))
170
+ continue;
171
+ const line = getLineNumber(sourceFile, statement.getStart(sourceFile));
172
+ const ruleId = `${layer}-forbidden-${targetLayer}`;
173
+ violations.push({
174
+ ruleId,
175
+ message: buildViolationMessage(layer, targetLayer),
176
+ line,
177
+ importPath: normalizeFilePath(path.relative(rootDir, resolvedFileName)),
178
+ targetLayer,
179
+ });
180
+ }
181
+ if (evaluated) {
182
+ if (violations.length === 0) {
183
+ compliantFiles++;
184
+ }
185
+ else {
186
+ totalViolations += violations.length;
187
+ for (const violation of violations) {
188
+ ruleCounts.set(violation.ruleId, (ruleCounts.get(violation.ruleId) ?? 0) + 1);
189
+ }
190
+ }
191
+ }
192
+ reports.push({
193
+ filePath: normalizeFilePath(path.relative(rootDir, filePath)),
194
+ layer,
195
+ evaluated,
196
+ violations,
197
+ });
198
+ }
199
+ const evaluatedReports = reports.filter((report) => report.evaluated);
200
+ const violatingReports = evaluatedReports.filter((report) => report.violations.length > 0);
201
+ const complianceRate = evaluatedFiles === 0 ? 100 : (compliantFiles / evaluatedFiles) * 100;
202
+ console.log(`${colors.bright}${colors.blue}Summary${colors.reset}`);
203
+ console.log(`${colors.gray}────────${colors.reset}`);
204
+ console.log(`Evaluated files: ${colors.bright}${evaluatedFiles}${colors.reset}`);
205
+ console.log(`Compliant files: ${colors.bright}${colors.green}${compliantFiles}${colors.reset}`);
206
+ console.log(`Files with violations: ${colors.bright}${colors.red}${violatingReports.length}${colors.reset}`);
207
+ console.log(`Total violations: ${colors.bright}${colors.red}${totalViolations}${colors.reset}`);
208
+ console.log(`Compliance rate: ${colors.bright}${complianceRate >= 90 ? colors.green : complianceRate >= 70 ? colors.yellow : colors.red}${formatPercent(complianceRate)}${colors.reset}`);
209
+ console.log();
210
+ if (layerCounts.size > 0) {
211
+ console.log(`${colors.bright}${colors.blue}Layer coverage${colors.reset}`);
212
+ console.log(`${colors.gray}────────────────${colors.reset}`);
213
+ for (const [layer, count] of Array.from(layerCounts.entries()).sort((a, b) => a[0].localeCompare(b[0]))) {
214
+ console.log(`- ${layer}: ${count}`);
215
+ }
216
+ console.log();
217
+ }
218
+ if (ruleCounts.size > 0) {
219
+ console.log(`${colors.bright}${colors.blue}Violation groups${colors.reset}`);
220
+ console.log(`${colors.gray}────────────────${colors.reset}`);
221
+ for (const [ruleId, count] of Array.from(ruleCounts.entries()).sort((a, b) => b[1] - a[1])) {
222
+ console.log(`- ${ruleId}: ${count}`);
223
+ }
224
+ console.log();
225
+ }
226
+ if (violatingReports.length > 0) {
227
+ console.log(`${colors.bright}${colors.red}Files with violations${colors.reset}`);
228
+ console.log(`${colors.gray}─────────────────────${colors.reset}`);
229
+ const sortedReports = violatingReports.toSorted((a, b) => b.violations.length - a.violations.length);
230
+ for (const report of sortedReports.slice(0, 20)) {
231
+ console.log(`${colors.bright}${report.filePath}${colors.reset} [${report.layer}] (${report.violations.length})`);
232
+ for (const violation of report.violations.slice(0, 5)) {
233
+ console.log(` ${colors.gray}L${violation.line}${colors.reset} ${violation.ruleId} -> ${violation.message} ${colors.gray}[${violation.importPath}]${colors.reset}`);
234
+ }
235
+ if (report.violations.length > 5) {
236
+ console.log(` ${colors.gray}... and ${report.violations.length - 5} more violation(s)${colors.reset}`);
237
+ }
238
+ console.log();
239
+ }
240
+ if (sortedReports.length > 20) {
241
+ console.log(`${colors.gray}... and ${sortedReports.length - 20} more file(s) with violations${colors.reset}`);
242
+ console.log();
243
+ }
244
+ }
245
+ else {
246
+ console.log(`${colors.green}${colors.bright}No DAL/Service/Repository boundary violations found.${colors.reset}`);
247
+ console.log();
248
+ }
249
+ console.log(`${colors.cyan}Notes:${colors.reset}`);
250
+ console.log(`- Only files classified by the configured layers are evaluated`);
251
+ console.log(`- This report is deterministic and exits with code 1 when violations exist`);
252
+ console.log();
253
+ return totalViolations > 0 ? 1 : 0;
254
+ }
255
+ /* v8 ignore start -- executable adapter */
256
+ if (isMainModule(import.meta.url)) {
257
+ runDalServiceRepositoryGuard().then((code) => {
258
+ process.exitCode = code;
259
+ }).catch((error) => {
260
+ console.error(`${colors.bright}${colors.red}Failed to generate DAL compliance report:${colors.reset}`, error);
261
+ process.exitCode = 1;
262
+ });
263
+ }
264
+ /* v8 ignore stop */
@@ -0,0 +1,14 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ type DependencyCruiserGuardOptions = {
3
+ cwd?: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ execPath?: string;
6
+ searchStart?: string;
7
+ exists?: (target: string) => boolean;
8
+ readFile?: (target: string, encoding: BufferEncoding) => string;
9
+ spawn?: typeof spawnSync;
10
+ error?: (message?: unknown) => void;
11
+ };
12
+ export declare function resolveDependencyCruiserBin(options?: DependencyCruiserGuardOptions): string;
13
+ export declare function runDependencyCruiserGuard(args: string[], options?: DependencyCruiserGuardOptions): number;
14
+ export {};
@@ -0,0 +1,69 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ function findDependencyCruiserPackageRoot(searchStart, exists, readFile) {
6
+ let current = searchStart;
7
+ while (true) {
8
+ if (path.basename(current) !== 'node_modules') {
9
+ const manifestPath = path.join(current, 'node_modules', 'dependency-cruiser', 'package.json');
10
+ if (exists(manifestPath)) {
11
+ const manifest = JSON.parse(readFile(manifestPath, 'utf8'));
12
+ if (manifest.name === 'dependency-cruiser') {
13
+ return path.dirname(manifestPath);
14
+ }
15
+ }
16
+ }
17
+ const directManifestPath = path.join(current, 'package.json');
18
+ if (exists(directManifestPath)) {
19
+ const manifest = JSON.parse(readFile(directManifestPath, 'utf8'));
20
+ if (manifest.name === 'dependency-cruiser') {
21
+ return current;
22
+ }
23
+ }
24
+ const parent = path.dirname(current);
25
+ if (parent === current) {
26
+ throw new Error(`Could not find dependency-cruiser package root from ${searchStart}.`);
27
+ }
28
+ current = parent;
29
+ }
30
+ }
31
+ export function resolveDependencyCruiserBin(options = {}) {
32
+ const exists = options.exists ?? existsSync;
33
+ const readFile = options.readFile ?? readFileSync;
34
+ const searchStart = options.searchStart ?? path.dirname(fileURLToPath(import.meta.url));
35
+ const packageRoot = findDependencyCruiserPackageRoot(searchStart, exists, readFile);
36
+ const binPath = path.join(packageRoot, 'bin', 'dependency-cruise.mjs');
37
+ if (!exists(binPath)) {
38
+ throw new Error(`dependency-cruiser binary not found at ${binPath}.`);
39
+ }
40
+ return binPath;
41
+ }
42
+ export function runDependencyCruiserGuard(args, options = {}) {
43
+ const writeError = options.error ?? console.error;
44
+ let binPath;
45
+ try {
46
+ binPath = resolveDependencyCruiserBin(options);
47
+ }
48
+ catch (error) {
49
+ writeError(`Dependency Cruiser guard is unavailable: ${error.message}`);
50
+ writeError('The dependency-cruiser runtime dependency must be resolvable from @titannio/webtoolkit-cli.');
51
+ return 1;
52
+ }
53
+ const result = (options.spawn ?? spawnSync)(options.execPath ?? process.execPath, [binPath, ...args], {
54
+ cwd: options.cwd ?? process.cwd(),
55
+ env: {
56
+ ...(options.env ?? process.env),
57
+ FORCE_COLOR: '1',
58
+ },
59
+ stdio: 'inherit',
60
+ });
61
+ if (result.error)
62
+ throw result.error;
63
+ return result.status ?? 1;
64
+ }
65
+ /* v8 ignore start -- executable adapter */
66
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
67
+ process.exit(runDependencyCruiserGuard(process.argv.slice(2)));
68
+ }
69
+ /* v8 ignore stop */
@@ -0,0 +1,3 @@
1
+ import { type DocumentationConfig } from '../config.js';
2
+ export declare function validateDocumentationConfig(config: DocumentationConfig): void;
3
+ export declare function checkDocumentation(root: string, config: DocumentationConfig): string[];