@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,137 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { loadConfig } from '../config.js';
4
+ import { isMainModule } from './guard-config.js';
5
+ const defaultRepoRoot = process.cwd();
6
+ const colors = {
7
+ reset: '\x1b[0m',
8
+ red: '\x1b[31m',
9
+ yellow: '\x1b[33m',
10
+ };
11
+ function colorize(message, color) {
12
+ return `${color}${message}${colors.reset}`;
13
+ }
14
+ function quoteShellArg(arg) {
15
+ if (/^[\w@%+=:,./\\-]+$/.test(arg)) {
16
+ return arg;
17
+ }
18
+ return `"${arg.replace(/"/g, '\\"')}"`;
19
+ }
20
+ export function getTargetDefinition(target, targetDefinitions) {
21
+ const definition = targetDefinitions[target];
22
+ if (!definition) {
23
+ throw new Error(`Unknown rebuild preflight target: ${target}. Expected one of ${Object.keys(targetDefinitions).join(', ')}`);
24
+ }
25
+ return definition;
26
+ }
27
+ export function parseTurboDryRun(stdout) {
28
+ const trimmed = stdout.trim();
29
+ if (!trimmed) {
30
+ throw new Error('Turbo dry run produced no JSON output');
31
+ }
32
+ return JSON.parse(trimmed);
33
+ }
34
+ export function extractPackagesNeedingRebuild(report, relevantBuildPackages) {
35
+ const relevantPackages = new Set(relevantBuildPackages);
36
+ return report.tasks
37
+ .filter((task) => task.task === 'build')
38
+ .filter((task) => task.command !== '<NONEXISTENT>')
39
+ .filter((task) => relevantPackages.size === 0 || relevantPackages.has(task.package))
40
+ .filter((task) => task.cache?.status !== 'HIT')
41
+ .map((task) => task.package);
42
+ }
43
+ function runTurboBuildDryRun(options) {
44
+ const { repoRoot, turboFilters, packageManager } = options;
45
+ const args = ['turbo', 'run', 'build', '--dry=json'];
46
+ for (const filter of turboFilters) {
47
+ args.push(`--filter=${filter}`);
48
+ }
49
+ const result = process.platform === 'win32'
50
+ ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', [packageManager, ...args].map(quoteShellArg).join(' ')], {
51
+ cwd: repoRoot,
52
+ encoding: 'utf8',
53
+ env: { ...process.env, FORCE_COLOR: '1' },
54
+ })
55
+ : spawnSync(packageManager, args, {
56
+ cwd: repoRoot,
57
+ encoding: 'utf8',
58
+ env: { ...process.env, FORCE_COLOR: '1' },
59
+ });
60
+ if (result.error) {
61
+ throw result.error;
62
+ }
63
+ if (result.status !== 0) {
64
+ const details = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status}`;
65
+ throw new Error(`Turbo dry run failed: ${details}`);
66
+ }
67
+ return parseTurboDryRun(result.stdout);
68
+ }
69
+ export async function getRebuildPreflightReport({ repoRoot = defaultRepoRoot, target, }) {
70
+ const { config } = await loadConfig(repoRoot);
71
+ const targetDefinitions = config.guards?.rebuildPreflight?.targets;
72
+ if (!targetDefinitions || Object.keys(targetDefinitions).length === 0) {
73
+ throw new Error('guards.rebuildPreflight.targets is not configured in .webtoolkit-cli/config.json.');
74
+ }
75
+ const definition = getTargetDefinition(target, targetDefinitions);
76
+ if (definition.relevantBuildPackages.length === 0) {
77
+ return {
78
+ target,
79
+ warningTitle: definition.warningTitle,
80
+ packagesNeedingRebuild: [],
81
+ };
82
+ }
83
+ const turboReport = runTurboBuildDryRun({
84
+ repoRoot,
85
+ turboFilters: definition.turboFilters,
86
+ packageManager: config.packageManager,
87
+ });
88
+ return {
89
+ target,
90
+ warningTitle: definition.warningTitle,
91
+ packagesNeedingRebuild: extractPackagesNeedingRebuild(turboReport, definition.relevantBuildPackages),
92
+ };
93
+ }
94
+ export function formatRebuildPreflightWarning(report) {
95
+ if (report.packagesNeedingRebuild.length === 0) {
96
+ return '';
97
+ }
98
+ const packageNames = report.packagesNeedingRebuild.join(', ');
99
+ return `${colorize(`[DEV PRECHECK] ${report.warningTitle}: ${packageNames}`, colors.red)}\n`;
100
+ }
101
+ export async function printRebuildPreflightWarning(options) {
102
+ const report = await getRebuildPreflightReport(options);
103
+ const warning = formatRebuildPreflightWarning(report);
104
+ if (warning) {
105
+ process.stderr.write(warning);
106
+ }
107
+ return report;
108
+ }
109
+ /* v8 ignore start -- executable adapter */
110
+ function getTargetFromArgv(argv = process.argv) {
111
+ const targetArg = argv.find((arg) => arg.startsWith('--target='));
112
+ return targetArg ? targetArg.slice('--target='.length) : null;
113
+ }
114
+ async function main() {
115
+ const target = getTargetFromArgv();
116
+ if (!target) {
117
+ process.stderr.write(colorize('[DEV PRECHECK] Missing --target=<name>; skipping rebuild warning.\n', colors.yellow));
118
+ process.exit(0);
119
+ }
120
+ try {
121
+ await printRebuildPreflightWarning({ target });
122
+ }
123
+ catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ process.stderr.write(colorize(`[DEV PRECHECK] Warning check failed: ${message}\n`, colors.yellow));
126
+ }
127
+ process.exit(0);
128
+ }
129
+ /* v8 ignore stop */
130
+ /* v8 ignore start -- executable adapter */
131
+ if (isMainModule(import.meta.url)) {
132
+ main().catch((error) => {
133
+ const message = error instanceof Error ? error.message : String(error);
134
+ process.stderr.write(colorize(`[DEV PRECHECK] Warning check failed: ${message}\n`, colors.yellow));
135
+ });
136
+ }
137
+ /* v8 ignore stop */
@@ -0,0 +1,12 @@
1
+ import type { RepositoryHygieneGuardConfig } from '../config.js';
2
+ export type RepositoryHygieneIssue = {
3
+ filePath: string;
4
+ pattern: string;
5
+ };
6
+ export declare function readTrackedFiles(rootDir: string): string[];
7
+ export declare function inspectTrackedPaths(trackedFiles: string[], config: RepositoryHygieneGuardConfig): RepositoryHygieneIssue[];
8
+ export declare function runRepositoryHygieneGuard(options?: {
9
+ rootDir?: string;
10
+ config?: RepositoryHygieneGuardConfig;
11
+ trackedFiles?: string[];
12
+ }): Promise<number>;
@@ -0,0 +1,70 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { compilePatterns, isMainModule, loadGuardConfig } from './guard-config.js';
3
+ function normalizeTrackedPath(filePath) {
4
+ return filePath.replaceAll('\\', '/');
5
+ }
6
+ export function readTrackedFiles(rootDir) {
7
+ const result = spawnSync('git', ['ls-files', '-z'], {
8
+ cwd: rootDir,
9
+ encoding: 'utf8',
10
+ shell: false,
11
+ });
12
+ if (result.error) {
13
+ throw new Error(`repository-hygiene: unable to list Git-tracked files: ${result.error.message}`);
14
+ }
15
+ if (result.status !== 0) {
16
+ const detail = result.stderr.trim();
17
+ /* v8 ignore next -- Git reports command failures on stderr */
18
+ throw new Error(`repository-hygiene: git ls-files failed${detail ? `: ${detail}` : '.'}`);
19
+ }
20
+ const files = result.stdout
21
+ .split('\0')
22
+ .filter(Boolean)
23
+ .map(normalizeTrackedPath);
24
+ if (files.length === 0) {
25
+ throw new Error('repository-hygiene: git ls-files returned no tracked files.');
26
+ }
27
+ return files;
28
+ }
29
+ export function inspectTrackedPaths(trackedFiles, config) {
30
+ const forbidden = compilePatterns(config.forbiddenPathPatterns);
31
+ const allowed = compilePatterns(config.allowedPathPatterns);
32
+ const issues = [];
33
+ for (const filePath of trackedFiles.map(normalizeTrackedPath)) {
34
+ if (allowed.some((pattern) => pattern.test(filePath)))
35
+ continue;
36
+ const index = forbidden.findIndex((pattern) => pattern.test(filePath));
37
+ if (index >= 0) {
38
+ issues.push({ filePath, pattern: config.forbiddenPathPatterns[index] });
39
+ }
40
+ }
41
+ return issues.sort((left, right) => Buffer.compare(Buffer.from(left.filePath), Buffer.from(right.filePath)));
42
+ }
43
+ export async function runRepositoryHygieneGuard(options = {}) {
44
+ const rootDir = options.rootDir ?? process.cwd();
45
+ const config = options.config ?? await loadGuardConfig('repositoryHygiene', rootDir);
46
+ const trackedFiles = options.trackedFiles ?? readTrackedFiles(rootDir);
47
+ if (trackedFiles.length === 0) {
48
+ throw new Error('repository-hygiene: tracked-file inventory must not be empty.');
49
+ }
50
+ const issues = inspectTrackedPaths(trackedFiles, config);
51
+ if (issues.length === 0) {
52
+ console.info(`Repository hygiene is valid (${trackedFiles.length} tracked files).`);
53
+ return 0;
54
+ }
55
+ console.error('Repository hygiene guard failed:');
56
+ for (const issue of issues) {
57
+ console.error(` - ${issue.filePath} [${issue.pattern}]`);
58
+ }
59
+ return 1;
60
+ }
61
+ /* v8 ignore start -- executable adapter */
62
+ if (isMainModule(import.meta.url)) {
63
+ runRepositoryHygieneGuard().then((code) => {
64
+ process.exitCode = code;
65
+ }).catch((error) => {
66
+ console.error(error.message);
67
+ process.exitCode = 1;
68
+ });
69
+ }
70
+ /* v8 ignore stop */
@@ -0,0 +1,10 @@
1
+ import { Project } from 'ts-morph';
2
+ import type { SchemaGuardConfig } from '../config.js';
3
+ export declare function findSchemaDefinitions(filePath: string, project: Project, configuredBuilders: string[]): Array<{
4
+ line: number;
5
+ snippet: string;
6
+ }>;
7
+ export declare function runSchemaGuard(options?: {
8
+ rootDir?: string;
9
+ config?: SchemaGuardConfig;
10
+ }): Promise<number>;
@@ -0,0 +1,160 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Node, Project, SyntaxKind, } from 'ts-morph';
4
+ import { BASE_SOURCE_EXCLUDE_PATTERNS, BASE_SOURCE_EXTENSIONS, assertConfiguredScanScope, compilePatterns, hasExtension, isMainModule, loadGuardConfig, resolveProjectPath, } from './guard-config.js';
5
+ const colors = {
6
+ reset: '\x1b[0m',
7
+ bright: '\x1b[1m',
8
+ red: '\x1b[31m',
9
+ green: '\x1b[32m',
10
+ yellow: '\x1b[33m',
11
+ blue: '\x1b[34m',
12
+ gray: '\x1b[90m',
13
+ };
14
+ function isWithinDirectory(filePath, directory) {
15
+ const relative = path.relative(directory, filePath);
16
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..');
17
+ }
18
+ function normalizedRelativePath(rootDir, filePath) {
19
+ return path.relative(rootDir, filePath).replaceAll('\\', '/');
20
+ }
21
+ function collectFiles(rootDir, config, centralDirectory, excludePatterns) {
22
+ const files = [];
23
+ function walk(directory) {
24
+ if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory())
25
+ return;
26
+ if (isWithinDirectory(directory, centralDirectory))
27
+ return;
28
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
29
+ const fullPath = path.join(directory, entry.name);
30
+ const relativePath = normalizedRelativePath(rootDir, fullPath);
31
+ if (entry.isDirectory()) {
32
+ if (!excludePatterns.some((pattern) => pattern.test(relativePath)))
33
+ walk(fullPath);
34
+ }
35
+ else if (entry.isFile() &&
36
+ hasExtension(fullPath, BASE_SOURCE_EXTENSIONS) &&
37
+ !excludePatterns.some((pattern) => pattern.test(relativePath))) {
38
+ files.push(fullPath);
39
+ }
40
+ }
41
+ }
42
+ for (const includePath of config.includePaths) {
43
+ walk(resolveProjectPath(rootDir, includePath));
44
+ }
45
+ return files.sort();
46
+ }
47
+ function unwrapExpression(expression) {
48
+ if (Node.isParenthesizedExpression(expression) ||
49
+ Node.isAsExpression(expression) ||
50
+ Node.isTypeAssertion(expression) ||
51
+ Node.isSatisfiesExpression(expression) ||
52
+ Node.isNonNullExpression(expression)) {
53
+ return unwrapExpression(expression.getExpression());
54
+ }
55
+ return expression;
56
+ }
57
+ function findRootBuilder(expression, builders) {
58
+ const current = unwrapExpression(expression);
59
+ if (!Node.isCallExpression(current))
60
+ return null;
61
+ const callee = unwrapExpression(current.getExpression());
62
+ if (!Node.isPropertyAccessExpression(callee))
63
+ return null;
64
+ const target = unwrapExpression(callee.getExpression());
65
+ if (Node.isIdentifier(target) && target.getText() === 'z' && builders.has(callee.getName())) {
66
+ return callee.getName();
67
+ }
68
+ return findRootBuilder(target, builders);
69
+ }
70
+ function definitionCandidate(node) {
71
+ if (Node.isVariableDeclaration(node) || Node.isPropertyDeclaration(node) || Node.isPropertyAssignment(node)) {
72
+ const expression = node.getInitializer();
73
+ return expression ? { owner: node, expression } : null;
74
+ }
75
+ if (Node.isExportAssignment(node)) {
76
+ return { owner: node, expression: node.getExpression() };
77
+ }
78
+ if (Node.isBinaryExpression(node) && node.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
79
+ return { owner: node, expression: node.getRight() };
80
+ }
81
+ return null;
82
+ }
83
+ function isNestedInDefinition(candidate, outer) {
84
+ if (candidate.expression.getStart() <= outer.expression.getStart() ||
85
+ candidate.expression.getEnd() > outer.expression.getEnd()) {
86
+ return false;
87
+ }
88
+ for (const ancestor of candidate.expression.getAncestors()) {
89
+ if (ancestor === outer.expression)
90
+ return true;
91
+ if (ancestor !== candidate.owner && Node.isFunctionLikeDeclaration(ancestor))
92
+ return false;
93
+ }
94
+ /* v8 ignore next -- a contained ts-morph descendant always reaches the containing expression */
95
+ return false;
96
+ }
97
+ export function findSchemaDefinitions(filePath, project, configuredBuilders) {
98
+ const sourceFile = project.addSourceFileAtPath(filePath);
99
+ const builders = new Set(configuredBuilders);
100
+ const candidates = [];
101
+ sourceFile.forEachDescendant((node) => {
102
+ const candidate = definitionCandidate(node);
103
+ if (candidate && findRootBuilder(candidate.expression, builders))
104
+ candidates.push(candidate);
105
+ });
106
+ return candidates
107
+ .filter((candidate) => !candidates.some((outer) => (outer !== candidate && isNestedInDefinition(candidate, outer))))
108
+ .map((candidate) => ({
109
+ line: candidate.expression.getStartLineNumber(),
110
+ snippet: candidate.expression.getText().replace(/\s+/gu, ' ').slice(0, 120),
111
+ }));
112
+ }
113
+ export async function runSchemaGuard(options = {}) {
114
+ const rootDir = options.rootDir ?? process.cwd();
115
+ const config = options.config ?? await loadGuardConfig('schema', rootDir);
116
+ /* v8 ignore next -- every invalid operand and the valid fallthrough are asserted */
117
+ if (!config.centralDirectory || config.includePaths.length === 0 || config.builders.length === 0) {
118
+ throw new Error('guards.schema requires centralDirectory, non-empty includePaths, and non-empty builders.');
119
+ }
120
+ const excludePatterns = compilePatterns(config.excludePatterns, BASE_SOURCE_EXCLUDE_PATTERNS);
121
+ const centralDirectory = resolveProjectPath(rootDir, config.centralDirectory);
122
+ const files = collectFiles(rootDir, config, centralDirectory, excludePatterns);
123
+ assertConfiguredScanScope({
124
+ root: rootDir,
125
+ guardName: 'schema',
126
+ configPath: 'guards.schema.includePaths',
127
+ configuredPaths: config.includePaths,
128
+ eligibleFiles: files,
129
+ });
130
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
131
+ const violations = [];
132
+ for (const filePath of files) {
133
+ for (const definition of findSchemaDefinitions(filePath, project, config.builders)) {
134
+ violations.push({
135
+ filePath: normalizedRelativePath(rootDir, filePath),
136
+ ...definition,
137
+ });
138
+ }
139
+ }
140
+ if (violations.length === 0) {
141
+ console.info(`${colors.green}${colors.bright}✅ [OK]${colors.reset} No configured schema builders found outside ${config.centralDirectory}.`);
142
+ return 0;
143
+ }
144
+ console.error(`${colors.bright}${colors.yellow}Configured schemas found outside ${config.centralDirectory}:${colors.reset}`);
145
+ for (const violation of violations) {
146
+ console.error(`${colors.gray}${violation.filePath}:${violation.line}${colors.reset} ${violation.snippet}`);
147
+ }
148
+ console.error(`${colors.red}Move these definitions to ${config.centralDirectory}.${colors.reset}`);
149
+ return 1;
150
+ }
151
+ /* v8 ignore start -- executable adapter */
152
+ if (isMainModule(import.meta.url)) {
153
+ runSchemaGuard().then((code) => {
154
+ process.exitCode = code;
155
+ }).catch((error) => {
156
+ console.error(error.message);
157
+ process.exitCode = 1;
158
+ });
159
+ }
160
+ /* v8 ignore stop */
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env tsx
2
+ export declare const MANIFEST_FIELDS: readonly ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
3
+ export type ManifestDependencies = Partial<Record<(typeof MANIFEST_FIELDS)[number], Record<string, string>>>;
4
+ export type ManifestIssue = {
5
+ filePath: string;
6
+ message: string;
7
+ };
8
+ export type ManifestEntry = {
9
+ filePath: string;
10
+ manifest: ManifestDependencies;
11
+ };
12
+ export type SingletonDependencyValidationInput = {
13
+ lockfileContent?: string | null;
14
+ manifests: ManifestEntry[];
15
+ overrides: Record<string, string>;
16
+ };
17
+ export declare function collectWorkspacePackageJsonFiles(rootDir?: string): string[];
18
+ export declare function isValidSemverRange(value: string): boolean;
19
+ export declare function collectLockfileVersions(lockfileContent: string, packageName: string): string[];
20
+ export declare function validateSingletonDependencyPolicy(input: SingletonDependencyValidationInput, rootDir?: string): ManifestIssue[];
21
+ export declare function runSingletonDepsGuard(rootDir?: string): number;
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env tsx
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { isMainModule } from './guard-config.js';
6
+ import { readPnpmWorkspaceOverrides } from './pnpm-workspace-config.js';
7
+ const require = createRequire(import.meta.url);
8
+ const semver = require('semver');
9
+ const WORKSPACE_ROOTS = ['apps', 'packages'];
10
+ export const MANIFEST_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
11
+ const colors = {
12
+ reset: '\x1b[0m',
13
+ bright: '\x1b[1m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ blue: '\x1b[34m',
18
+ cyan: '\x1b[36m',
19
+ gray: '\x1b[90m',
20
+ };
21
+ function readJsonFile(filePath) {
22
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
23
+ }
24
+ function escapeRegExp(value) {
25
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
+ }
27
+ export function collectWorkspacePackageJsonFiles(rootDir = process.cwd()) {
28
+ const manifests = [];
29
+ for (const root of WORKSPACE_ROOTS) {
30
+ const absoluteRoot = path.resolve(rootDir, root);
31
+ if (!fs.existsSync(absoluteRoot))
32
+ continue;
33
+ for (const entry of fs.readdirSync(absoluteRoot, { withFileTypes: true })) {
34
+ if (!entry.isDirectory())
35
+ continue;
36
+ const manifestPath = path.join(absoluteRoot, entry.name, 'package.json');
37
+ if (fs.existsSync(manifestPath)) {
38
+ manifests.push(manifestPath);
39
+ }
40
+ }
41
+ }
42
+ return manifests.sort((left, right) => left.localeCompare(right));
43
+ }
44
+ function toRelativePath(filePath, rootDir) {
45
+ return path.isAbsolute(filePath) ? path.relative(rootDir, filePath) : filePath;
46
+ }
47
+ export function isValidSemverRange(value) {
48
+ return semver.validRange(value) !== null;
49
+ }
50
+ function acceptsResolvedVersion(versionRange, resolvedVersion) {
51
+ return semver.satisfies(resolvedVersion, versionRange, { includePrerelease: true });
52
+ }
53
+ function collectOverrideIssues(overrides) {
54
+ return Object.entries(overrides)
55
+ .sort((left, right) => left[0].localeCompare(right[0]))
56
+ .flatMap(([packageName, overrideRange]) => isValidSemverRange(overrideRange)
57
+ ? []
58
+ : [
59
+ {
60
+ filePath: 'pnpm-workspace.yaml',
61
+ message: `overrides["${packageName}"] deve ser um semver/range valido; encontrado "${overrideRange}"`,
62
+ },
63
+ ]);
64
+ }
65
+ function buildResolvedVersionsByPackage(lockfileContent, packageNames) {
66
+ const versionsByPackage = new Map();
67
+ for (const packageName of packageNames) {
68
+ versionsByPackage.set(packageName, lockfileContent ? collectLockfileVersions(lockfileContent, packageName) : []);
69
+ }
70
+ return versionsByPackage;
71
+ }
72
+ function collectManifestCompatibilityIssues(manifestEntry, overrides, resolvedVersionsByPackage, rootDir) {
73
+ const relativePath = toRelativePath(manifestEntry.filePath, rootDir);
74
+ const issues = [];
75
+ for (const [packageName, overrideRange] of Object.entries(overrides)) {
76
+ const resolvedVersions = resolvedVersionsByPackage.get(packageName);
77
+ if (resolvedVersions.length !== 1)
78
+ continue;
79
+ const resolvedVersion = resolvedVersions[0];
80
+ for (const field of MANIFEST_FIELDS) {
81
+ const declaredVersion = manifestEntry.manifest[field]?.[packageName];
82
+ if (!declaredVersion)
83
+ continue;
84
+ if (!isValidSemverRange(declaredVersion) || !acceptsResolvedVersion(declaredVersion, resolvedVersion)) {
85
+ issues.push({
86
+ filePath: relativePath,
87
+ message: `"${packageName}" em "${field}" deve aceitar a versao singleton resolvida "${resolvedVersion}" (override: "${overrideRange}"); encontrado "${declaredVersion}"`,
88
+ });
89
+ }
90
+ }
91
+ }
92
+ return issues;
93
+ }
94
+ export function collectLockfileVersions(lockfileContent, packageName) {
95
+ const pattern = new RegExp(`^\\s{2}${escapeRegExp(packageName)}@([^:\\n]+):$`, 'gm');
96
+ const versions = new Set();
97
+ for (const match of lockfileContent.matchAll(pattern)) {
98
+ const normalizedVersion = match[1].split('(')[0].trim();
99
+ if (normalizedVersion.length > 0) {
100
+ versions.add(normalizedVersion);
101
+ }
102
+ }
103
+ return Array.from(versions).sort((left, right) => left.localeCompare(right));
104
+ }
105
+ export function validateSingletonDependencyPolicy(input, rootDir = process.cwd()) {
106
+ const issues = [...collectOverrideIssues(input.overrides)];
107
+ const packageNames = Object.keys(input.overrides).sort((left, right) => left.localeCompare(right));
108
+ const resolvedVersionsByPackage = buildResolvedVersionsByPackage(input.lockfileContent, packageNames);
109
+ for (const packageName of packageNames) {
110
+ const overrideRange = input.overrides[packageName];
111
+ const resolvedVersions = resolvedVersionsByPackage.get(packageName);
112
+ if (resolvedVersions.length > 1) {
113
+ issues.push({
114
+ filePath: 'pnpm-lock.yaml',
115
+ message: `"${packageName}" aparece com multiplas versoes no lockfile: ${resolvedVersions.join(', ')}`,
116
+ });
117
+ continue;
118
+ }
119
+ if (resolvedVersions.length === 1 &&
120
+ isValidSemverRange(overrideRange) &&
121
+ !acceptsResolvedVersion(overrideRange, resolvedVersions[0])) {
122
+ issues.push({
123
+ filePath: 'pnpm-lock.yaml',
124
+ message: `"${packageName}" resolve para "${resolvedVersions[0]}" no lockfile, mas pnpm-workspace.yaml overrides exige compatibilidade com "${overrideRange}"`,
125
+ });
126
+ }
127
+ }
128
+ for (const manifestEntry of input.manifests) {
129
+ issues.push(...collectManifestCompatibilityIssues(manifestEntry, input.overrides, resolvedVersionsByPackage, rootDir));
130
+ }
131
+ return issues;
132
+ }
133
+ function printIssues(issues) {
134
+ for (const issue of issues) {
135
+ console.info(`${colors.gray}${issue.filePath}${colors.reset}`);
136
+ console.info(` ${colors.red}→${colors.reset} ${issue.message}`);
137
+ }
138
+ }
139
+ export function runSingletonDepsGuard(rootDir = process.cwd()) {
140
+ console.info(`${colors.bright}${colors.blue}🔒 Checking singleton dependency compatibility...${colors.reset}`);
141
+ const rootPackageJsonPath = path.resolve(rootDir, 'package.json');
142
+ const lockfilePath = path.resolve(rootDir, 'pnpm-lock.yaml');
143
+ if (!fs.existsSync(rootPackageJsonPath)) {
144
+ console.error(`${colors.red}Root package.json not found.${colors.reset}`);
145
+ return 1;
146
+ }
147
+ const rootManifest = readJsonFile(rootPackageJsonPath);
148
+ const overrides = readPnpmWorkspaceOverrides(path.resolve(rootDir, 'pnpm-workspace.yaml'));
149
+ const overrideEntries = Object.entries(overrides).sort((left, right) => left[0].localeCompare(right[0]));
150
+ if (overrideEntries.length === 0) {
151
+ console.info(`${colors.yellow}No pnpm-workspace.yaml overrides configured. Singleton dependency guard skipped.${colors.reset}`);
152
+ return 0;
153
+ }
154
+ const manifests = [{ filePath: rootPackageJsonPath, manifest: rootManifest }];
155
+ for (const manifestPath of collectWorkspacePackageJsonFiles(rootDir)) {
156
+ const manifest = readJsonFile(manifestPath);
157
+ manifests.push({ filePath: manifestPath, manifest });
158
+ }
159
+ const issues = [];
160
+ if (!fs.existsSync(lockfilePath)) {
161
+ issues.push({
162
+ filePath: 'pnpm-lock.yaml',
163
+ message: 'pnpm-lock.yaml não encontrado',
164
+ });
165
+ }
166
+ else {
167
+ const lockfileContent = fs.readFileSync(lockfilePath, 'utf8');
168
+ issues.push(...validateSingletonDependencyPolicy({ lockfileContent, manifests, overrides }, rootDir));
169
+ }
170
+ if (issues.length > 0) {
171
+ console.info(`${colors.bright}${colors.red}⚠️ SINGLETON DEPENDENCY POLICY VIOLATION${colors.reset}\n`);
172
+ printIssues(issues);
173
+ console.info(`\n${colors.red}Ajuste os manifests/lockfile para manter dependências singleton alinhadas.${colors.reset}`);
174
+ return 1;
175
+ }
176
+ console.info(`${colors.green}${colors.bright}✅ [OK]${colors.reset} Singleton dependencies are compatible with pnpm-workspace.yaml overrides`);
177
+ return 0;
178
+ }
179
+ /* v8 ignore start -- executable adapter */
180
+ if (isMainModule(import.meta.url)) {
181
+ process.exitCode = runSingletonDepsGuard();
182
+ }
183
+ /* v8 ignore stop */
@@ -0,0 +1,5 @@
1
+ import type { TsconfigGuardConfig } from '../config.js';
2
+ export declare function runTsconfigGuard(options?: {
3
+ rootDir?: string;
4
+ config?: TsconfigGuardConfig;
5
+ }): Promise<number>;