@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,184 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { createServer } from 'node:net';
3
+ import { buildPackageManagerCommand, resolveSpawnSpec } from './process.js';
4
+ const isWindows = process.platform === 'win32';
5
+ export function resolveDevWatchSpawnSpec(command, commandArgs) {
6
+ const resolved = resolveSpawnSpec(command, commandArgs);
7
+ return { ...resolved, detached: !isWindows };
8
+ }
9
+ function spawnCommand(command, commandArgs, options = {}) {
10
+ const resolved = resolveDevWatchSpawnSpec(command, commandArgs);
11
+ return spawn(resolved.command, resolved.args, { ...options, detached: resolved.detached });
12
+ }
13
+ function getArgValue(args, name) {
14
+ const prefix = `--${name}=`;
15
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length) ?? null;
16
+ }
17
+ function getSelectedApps(runtime, args) {
18
+ const appsArg = getArgValue(args, 'apps');
19
+ if (!appsArg)
20
+ return [...(runtime.config.devWatch?.defaultApps ?? [])];
21
+ return appsArg.split(',').map((app) => app.trim()).filter(Boolean);
22
+ }
23
+ function checkPortAvailability(port, host) {
24
+ return new Promise((resolve) => {
25
+ const server = createServer();
26
+ server.unref();
27
+ server.once('error', (error) => resolve({ available: false, error, host, port }));
28
+ server.listen({ host, port }, () => {
29
+ server.close((error) => resolve({ available: !error, error: error ?? undefined, host, port }));
30
+ });
31
+ });
32
+ }
33
+ function getPortFromEndpoint(endpoint) {
34
+ const bracketedIpv6Match = endpoint.match(/\]:(\d+)$/u);
35
+ if (bracketedIpv6Match)
36
+ return Number(bracketedIpv6Match[1]);
37
+ const separatorIndex = endpoint.lastIndexOf(':');
38
+ if (separatorIndex === -1)
39
+ return null;
40
+ const port = Number(endpoint.slice(separatorIndex + 1));
41
+ return Number.isInteger(port) ? port : null;
42
+ }
43
+ export function parseWindowsNetstatListeningPids(output, port) {
44
+ const pids = new Set();
45
+ for (const line of String(output ?? '').split(/\r?\n/u)) {
46
+ const parts = line.trim().split(/\s+/u);
47
+ if (parts.length < 4 || parts[0] !== 'TCP')
48
+ continue;
49
+ const state = parts[parts.length - 2];
50
+ const pid = Number(parts[parts.length - 1]);
51
+ if (state === 'LISTENING' && Number.isInteger(pid) && getPortFromEndpoint(parts[1]) === port) {
52
+ pids.add(pid);
53
+ }
54
+ }
55
+ return [...pids].sort((left, right) => left - right);
56
+ }
57
+ function listListeningPidsByPort(port) {
58
+ if (isWindows) {
59
+ for (const command of ['netstat.exe', 'netstat']) {
60
+ const result = spawnSync(command, ['-ano', '-p', 'tcp'], { encoding: 'utf8', windowsHide: true });
61
+ if (!result.error && result.status === 0)
62
+ return parseWindowsNetstatListeningPids(result.stdout, port);
63
+ }
64
+ return [];
65
+ }
66
+ const result = spawnSync('lsof', ['-ti', `TCP:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8', windowsHide: true });
67
+ if (result.error || result.status !== 0)
68
+ return [];
69
+ return result.stdout.split(/\r?\n/u).map((value) => Number(value.trim())).filter((pid) => Number.isInteger(pid));
70
+ }
71
+ function stopProcessTree(pid) {
72
+ if (isWindows) {
73
+ const result = spawnSync('taskkill.exe', ['/pid', String(pid), '/t', '/f'], { stdio: 'pipe', windowsHide: true });
74
+ return !result.error && result.status === 0;
75
+ }
76
+ try {
77
+ process.kill(pid, 'SIGTERM');
78
+ return true;
79
+ }
80
+ catch (error) {
81
+ return error.code === 'ESRCH';
82
+ }
83
+ }
84
+ async function stopExistingListenersOnPort(port, displayName, graceMs) {
85
+ const pids = listListeningPidsByPort(port);
86
+ if (pids.length === 0)
87
+ return [];
88
+ console.warn(`Stopping existing ${displayName} listener(s) on port ${port}: PID ${pids.join(', ')}`);
89
+ for (const pid of pids)
90
+ stopProcessTree(pid);
91
+ await new Promise((resolve) => setTimeout(resolve, graceMs));
92
+ return listListeningPidsByPort(port);
93
+ }
94
+ async function ensurePortsAvailable(apps, host) {
95
+ const results = await Promise.all(apps.map(async ({ key, definition }) => ({
96
+ key,
97
+ definition,
98
+ probe: await checkPortAvailability(definition.port, host),
99
+ })));
100
+ const blockedApps = results.filter(({ probe }) => !probe.available);
101
+ if (blockedApps.length === 0)
102
+ return true;
103
+ console.error('DEV port preflight failed.\n');
104
+ for (const { definition, probe } of blockedApps) {
105
+ const reason = probe.error?.code ?? probe.error?.message ?? 'unknown error';
106
+ console.error(`- ${definition.displayName}: ${probe.host}:${probe.port} (${reason})`);
107
+ }
108
+ return false;
109
+ }
110
+ function stopChildTree(child) {
111
+ if (child.exitCode !== null || child.signalCode !== null || typeof child.pid !== 'number')
112
+ return;
113
+ if (isWindows) {
114
+ spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
115
+ return;
116
+ }
117
+ try {
118
+ process.kill(-child.pid, 'SIGTERM');
119
+ }
120
+ catch {
121
+ child.kill('SIGTERM');
122
+ }
123
+ }
124
+ function runWatch(runtime, selectedApps, silent) {
125
+ const children = [];
126
+ let shuttingDown = false;
127
+ function shutdown(exitCode) {
128
+ if (shuttingDown)
129
+ return;
130
+ shuttingDown = true;
131
+ for (const child of children)
132
+ stopChildTree(child);
133
+ process.exitCode = exitCode;
134
+ }
135
+ for (const { key, definition } of selectedApps) {
136
+ if (!definition.filter)
137
+ throw new Error(`devWatch app "${key}" must define filter to run watch mode.`);
138
+ const extraArgs = silent ? ['--', '--logLevel', 'warn'] : [];
139
+ const commandSpec = buildPackageManagerCommand(runtime.config.packageManager, ['--filter', definition.filter, 'run', 'dev:skip-check', ...extraArgs]);
140
+ const child = spawnCommand(commandSpec.command, commandSpec.args ?? [], { stdio: 'inherit', cwd: runtime.cwd });
141
+ children.push(child);
142
+ child.on('error', (error) => {
143
+ console.error(`[${key.toUpperCase()}] ${error.message}`);
144
+ shutdown(1);
145
+ });
146
+ child.on('exit', (code, signal) => {
147
+ if (shuttingDown)
148
+ return;
149
+ const exitCode = code ?? (signal ? 1 : 0);
150
+ if (exitCode !== 0)
151
+ console.error(`[${key.toUpperCase()}] exited with ${signal ?? code}`);
152
+ shutdown(exitCode);
153
+ });
154
+ }
155
+ process.on('SIGINT', () => shutdown(130));
156
+ process.on('SIGTERM', () => shutdown(143));
157
+ }
158
+ export async function runDevWatch(runtime, rawArgs) {
159
+ const config = runtime.config.devWatch;
160
+ if (!config?.apps || !config.defaultApps?.length)
161
+ throw new Error('devWatch.apps and devWatch.defaultApps are not configured.');
162
+ const silent = rawArgs.includes('--silent');
163
+ const checkOnly = rawArgs.includes('--check-only');
164
+ const includeBackend = rawArgs.includes('--include-backend');
165
+ const backendApp = config.backendApp ?? 'backend';
166
+ const selectedKeys = [...(includeBackend && checkOnly ? [backendApp] : []), ...getSelectedApps(runtime, rawArgs)];
167
+ const unknownApps = selectedKeys.filter((app) => !config.apps[app]);
168
+ if (unknownApps.length > 0)
169
+ throw new Error(`Unknown dev app(s): ${unknownApps.join(', ')}`);
170
+ const selectedApps = selectedKeys.map((key) => ({ key, definition: config.apps[key] }));
171
+ if (checkOnly && selectedKeys.includes(backendApp)) {
172
+ const backend = config.apps[backendApp];
173
+ const remainingPids = await stopExistingListenersOnPort(backend.port, backend.displayName, config.backendPortCleanupGraceMs ?? 1500);
174
+ if (remainingPids.length > 0) {
175
+ throw new Error(`Could not stop existing ${backend.displayName} listener(s) on port ${backend.port}: PID ${remainingPids.join(', ')}`);
176
+ }
177
+ }
178
+ const portsAvailable = await ensurePortsAvailable(selectedApps, config.host ?? '127.0.0.1');
179
+ if (!portsAvailable)
180
+ process.exit(1);
181
+ if (checkOnly)
182
+ return;
183
+ runWatch(runtime, selectedApps.filter(({ key }) => key !== backendApp), silent);
184
+ }
@@ -0,0 +1,10 @@
1
+ import type { WebToolkitCliConfig } from './config.js';
2
+ type Runtime = {
3
+ cwd: string;
4
+ config: WebToolkitCliConfig;
5
+ };
6
+ export declare function readRequiredPnpmVersion(repoRoot: string): string;
7
+ export declare function prepareCorepackPnpm(runtime: Runtime, repoRoot: string, version: string): void;
8
+ export declare function runEnvBootstrap(runtime: Runtime): void;
9
+ export declare function runEnvDoctor(runtime: Runtime): void;
10
+ export {};
@@ -0,0 +1,172 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ function findRepoRoot(startDir) {
5
+ let currentDir = startDir;
6
+ while (true) {
7
+ if (fs.existsSync(path.join(currentDir, 'package.json')) && fs.existsSync(path.join(currentDir, 'pnpm-workspace.yaml')))
8
+ return currentDir;
9
+ const parentDir = path.dirname(currentDir);
10
+ if (parentDir === currentDir)
11
+ throw new Error('Could not find repo root from the current working directory.');
12
+ currentDir = parentDir;
13
+ }
14
+ }
15
+ export function readRequiredPnpmVersion(repoRoot) {
16
+ const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
17
+ const packageManager = String(packageJson.packageManager || '');
18
+ if (!packageManager.startsWith('pnpm@'))
19
+ throw new Error(`Expected packageManager to start with pnpm@ but found ${JSON.stringify(packageManager)}.`);
20
+ const version = packageManager.slice('pnpm@'.length).trim();
21
+ if (!version || version.includes(' ') || version === 'latest' || version === '11') {
22
+ throw new Error(`packageManager must pin an exact pnpm version, but found ${JSON.stringify(packageManager)}.`);
23
+ }
24
+ return version;
25
+ }
26
+ function resolveNodeSiblingBinary(baseName) {
27
+ const binaryName = process.platform === 'win32' ? `${baseName}.cmd` : baseName;
28
+ const candidate = path.join(path.dirname(process.execPath), binaryName);
29
+ return fs.existsSync(candidate) ? candidate : binaryName;
30
+ }
31
+ function buildEnv(repoRoot, runtime) {
32
+ return {
33
+ ...process.env,
34
+ COREPACK_HOME: path.join(repoRoot, runtime.config.environment?.corepackHome ?? '.corepack'),
35
+ };
36
+ }
37
+ function spawnTool(command, args, repoRoot, runtime, options = {}) {
38
+ const useShell = process.platform === 'win32';
39
+ const direct = command === process.execPath || command.toLowerCase().endsWith('.exe');
40
+ if (!useShell || direct) {
41
+ return spawnSync(command, args, { cwd: repoRoot, env: buildEnv(repoRoot, runtime), shell: false, ...options });
42
+ }
43
+ return spawnSync([command, ...args].map(quoteForShell).join(' '), { cwd: repoRoot, env: buildEnv(repoRoot, runtime), shell: true, ...options });
44
+ }
45
+ function quoteForShell(value) {
46
+ if (/^[A-Za-z0-9_./:=+-]+$/u.test(value))
47
+ return value;
48
+ return `"${String(value).replace(/"/gu, '""')}"`;
49
+ }
50
+ function captureCommand(command, args, repoRoot, runtime) {
51
+ const result = spawnTool(command, args, repoRoot, runtime, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
52
+ if (result.status !== 0) {
53
+ const detail = String(result.stderr || result.stdout || '').trim();
54
+ throw new Error(`Command failed: ${command} ${args.join(' ')}${detail ? `\n${detail}` : ''}`);
55
+ }
56
+ return String(result.stdout).trim();
57
+ }
58
+ function runCommand(command, args, repoRoot, runtime) {
59
+ const result = spawnTool(command, args, repoRoot, runtime, { stdio: 'inherit' });
60
+ if (result.status !== 0)
61
+ throw new Error(`Command failed: ${command} ${args.join(' ')}`);
62
+ }
63
+ function assertNodeMajor(runtime) {
64
+ const required = runtime.config.environment?.requiredNodeMajor;
65
+ if (!required)
66
+ return;
67
+ const major = Number(process.versions.node.split('.')[0]);
68
+ if (major !== required)
69
+ throw new Error(`Expected Node ${required}.x but found ${process.versions.node}.`);
70
+ }
71
+ function walkRepo(currentDir, onFile) {
72
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
73
+ if (['.git', 'node_modules', '.pnpm-store', '.turbo', 'dist'].includes(entry.name))
74
+ continue;
75
+ const entryPath = path.join(currentDir, entry.name);
76
+ if (entry.isDirectory())
77
+ walkRepo(entryPath, onFile);
78
+ else if (entry.isFile())
79
+ onFile(entryPath);
80
+ }
81
+ }
82
+ function readTrimmedFile(filePath) {
83
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').trim() : '';
84
+ }
85
+ export function prepareCorepackPnpm(runtime, repoRoot, version) {
86
+ if (!version || version.includes(' ') || version === 'latest' || version === '11') {
87
+ throw new Error(`packageManager must pin an exact pnpm version, but found ${JSON.stringify(`pnpm@${version}`)}.`);
88
+ }
89
+ const corepack = resolveNodeSiblingBinary('corepack');
90
+ runCommand(corepack, ['enable'], repoRoot, runtime);
91
+ runCommand(corepack, ['prepare', `pnpm@${version}`, '--activate'], repoRoot, runtime);
92
+ }
93
+ export function runEnvBootstrap(runtime) {
94
+ const repoRoot = findRepoRoot(runtime.cwd);
95
+ const requiredPnpmVersion = readRequiredPnpmVersion(repoRoot);
96
+ const nodeInstallDir = path.dirname(process.execPath);
97
+ const npmCliPath = path.join(nodeInstallDir, 'node_modules', 'npm', 'bin', 'npm-cli.js');
98
+ assertNodeMajor(runtime);
99
+ if (!fs.existsSync(npmCliPath))
100
+ throw new Error(`npm CLI was not found under the active Node installation: ${npmCliPath}`);
101
+ const corepack = resolveNodeSiblingBinary('corepack');
102
+ if (!fs.existsSync(corepack)) {
103
+ console.info('Corepack was not found in the active Node installation. Installing it via npm...');
104
+ runCommand(process.execPath, [npmCliPath, 'install', '--global', '--force', 'corepack'], repoRoot, runtime);
105
+ }
106
+ prepareCorepackPnpm(runtime, repoRoot, requiredPnpmVersion);
107
+ console.info(`Node: ${process.versions.node}`);
108
+ console.info(`npm: ${captureCommand(process.execPath, [npmCliPath, '--version'], repoRoot, runtime)}`);
109
+ console.info(`pnpm: ${captureCommand(resolveNodeSiblingBinary('pnpm'), ['--version'], repoRoot, runtime)}`);
110
+ console.info(`node path: ${process.execPath}`);
111
+ console.info(`npm path: ${npmCliPath}`);
112
+ console.info(`pnpm path: ${resolveNodeSiblingBinary('pnpm')}`);
113
+ console.info(`repo root: ${repoRoot}`);
114
+ }
115
+ export function runEnvDoctor(runtime) {
116
+ const repoRoot = findRepoRoot(runtime.cwd);
117
+ const requiredNodeMajor = runtime.config.environment?.requiredNodeMajor;
118
+ const requiredPnpmVersion = readRequiredPnpmVersion(repoRoot);
119
+ const failures = [];
120
+ if (requiredNodeMajor && Number(process.versions.node.split('.')[0]) !== requiredNodeMajor) {
121
+ failures.push(`Expected Node ${requiredNodeMajor}.x but found ${process.versions.node}.`);
122
+ }
123
+ if (requiredNodeMajor && readTrimmedFile(path.join(repoRoot, '.nvmrc')) !== String(requiredNodeMajor)) {
124
+ failures.push(`.nvmrc must be ${requiredNodeMajor}.`);
125
+ }
126
+ if (requiredNodeMajor && readTrimmedFile(path.join(repoRoot, '.node-version')) !== String(requiredNodeMajor)) {
127
+ failures.push(`.node-version must be ${requiredNodeMajor}.`);
128
+ }
129
+ try {
130
+ const pnpmVersion = captureCommand(resolveNodeSiblingBinary('pnpm'), ['--version'], repoRoot, runtime);
131
+ if (pnpmVersion !== requiredPnpmVersion)
132
+ failures.push(`Expected pnpm ${requiredPnpmVersion} but found ${pnpmVersion}.`);
133
+ }
134
+ catch (error) {
135
+ failures.push(error.message);
136
+ }
137
+ const packageLockfiles = [];
138
+ const yarnLockfiles = [];
139
+ const extraPnpmLockfiles = [];
140
+ walkRepo(repoRoot, (filePath) => {
141
+ const relativePath = path.relative(repoRoot, filePath).split(path.sep).join('/');
142
+ const basename = path.basename(filePath);
143
+ if (basename === 'package-lock.json')
144
+ packageLockfiles.push(relativePath);
145
+ if (basename === 'yarn.lock')
146
+ yarnLockfiles.push(relativePath);
147
+ if (basename === 'pnpm-lock.yaml' && relativePath !== 'pnpm-lock.yaml')
148
+ extraPnpmLockfiles.push(relativePath);
149
+ });
150
+ if (packageLockfiles.length > 0)
151
+ failures.push(`Unexpected package-lock.json files: ${packageLockfiles.join(', ')}`);
152
+ if (yarnLockfiles.length > 0)
153
+ failures.push(`Unexpected yarn.lock files: ${yarnLockfiles.join(', ')}`);
154
+ if (extraPnpmLockfiles.length > 0)
155
+ failures.push(`Unexpected nested pnpm-lock.yaml files: ${extraPnpmLockfiles.join(', ')}`);
156
+ let currentDir = path.dirname(repoRoot);
157
+ while (true) {
158
+ const parentNodeModules = path.join(currentDir, 'node_modules');
159
+ if (fs.existsSync(parentNodeModules))
160
+ failures.push(`Parent node_modules detected outside repo root: ${parentNodeModules}`);
161
+ const nextDir = path.dirname(currentDir);
162
+ if (nextDir === currentDir)
163
+ break;
164
+ currentDir = nextDir;
165
+ }
166
+ if (failures.length > 0) {
167
+ for (const failure of failures)
168
+ console.error(`- ${failure}`);
169
+ process.exit(1);
170
+ }
171
+ console.info('Environment doctor passed.');
172
+ }
@@ -0,0 +1 @@
1
+ export declare const builtinGuards: Readonly<Record<string, string>>;
@@ -0,0 +1,17 @@
1
+ export const builtinGuards = {
2
+ any: 'any-guard.js',
3
+ 'assert-no-tests-in-dist': 'assert-no-tests-in-dist.js',
4
+ 'code-pattern': 'code-pattern-guard.js',
5
+ 'dal-service-repository': 'dal-service-repository-check.js',
6
+ 'dependency-cruiser': 'dependency-cruiser-guard.js',
7
+ documentation: 'documentation-guard.js',
8
+ 'internal-link': 'internal-link-guard.js',
9
+ mojibake: 'check-mojibake.js',
10
+ 'package-surface': 'package-surface-guard.js',
11
+ 'rebuild-preflight': 'rebuild-preflight.js',
12
+ 'repository-hygiene': 'repository-hygiene-guard.js',
13
+ schema: 'schema-guard.js',
14
+ 'singleton-deps': 'singleton-deps-guard.js',
15
+ tsconfig: 'tsconfig-guard.js',
16
+ 'workspace-manifest': 'workspace-manifest-guard.js',
17
+ };
@@ -0,0 +1,4 @@
1
+ export { builtinGuards } from './guard-registry.js';
2
+ export declare function printGuardHelp(): void;
3
+ export declare function executeBuiltinGuard(name: string, args: string[], cwd: string): number;
4
+ export declare function runBuiltinGuard(name: string, args: string[], cwd: string): void;
@@ -0,0 +1,36 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { builtinGuards } from './guard-registry.js';
5
+ export { builtinGuards } from './guard-registry.js';
6
+ function getGuardsDir() {
7
+ return path.join(path.dirname(fileURLToPath(import.meta.url)), 'guards');
8
+ }
9
+ export function printGuardHelp() {
10
+ console.info('Usage: webtoolkit guard <name> [args]');
11
+ console.info('');
12
+ console.info('Builtin guards:');
13
+ for (const name of Object.keys(builtinGuards).sort()) {
14
+ console.info(` ${name}`);
15
+ }
16
+ }
17
+ export function executeBuiltinGuard(name, args, cwd) {
18
+ const guardFile = Object.hasOwn(builtinGuards, name) ? builtinGuards[name] : undefined;
19
+ if (!guardFile) {
20
+ throw new Error(`Unknown builtin guard "${name}". Available guards: ${Object.keys(builtinGuards).sort().join(', ')}.`);
21
+ }
22
+ const result = spawnSync(process.execPath, [path.join(getGuardsDir(), guardFile), ...args], {
23
+ cwd,
24
+ env: {
25
+ ...process.env,
26
+ FORCE_COLOR: '1',
27
+ },
28
+ stdio: 'inherit',
29
+ });
30
+ if (result.error)
31
+ throw result.error;
32
+ return result.status ?? 1;
33
+ }
34
+ export function runBuiltinGuard(name, args, cwd) {
35
+ process.exit(executeBuiltinGuard(name, args, cwd));
36
+ }
@@ -0,0 +1,16 @@
1
+ import { Project } from 'ts-morph';
2
+ import type { AnyGuardConfig } from '../config.js';
3
+ export type AnyOccurrence = {
4
+ line: number;
5
+ column: number;
6
+ context: string;
7
+ };
8
+ export type AnyFileReport = {
9
+ filePath: string;
10
+ occurrences: AnyOccurrence[];
11
+ };
12
+ export declare function findAnyOccurrences(filePath: string, project: Project): AnyOccurrence[];
13
+ export declare function runAnyGuard(options?: {
14
+ rootDir?: string;
15
+ config?: AnyGuardConfig;
16
+ }): Promise<number>;
@@ -0,0 +1,121 @@
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
+ blue: '\x1b[34m',
11
+ cyan: '\x1b[36m',
12
+ gray: '\x1b[90m',
13
+ };
14
+ function normalizedRelativePath(rootDir, filePath) {
15
+ return path.relative(rootDir, filePath).replaceAll('\\', '/');
16
+ }
17
+ function isExcluded(rootDir, filePath, patterns) {
18
+ const relativePath = normalizedRelativePath(rootDir, filePath);
19
+ return patterns.some((pattern) => pattern.test(relativePath));
20
+ }
21
+ function collectFiles(rootDir, config, excludePatterns) {
22
+ const files = [];
23
+ function walk(directory) {
24
+ if (!fs.existsSync(directory) || !fs.statSync(directory).isDirectory())
25
+ return;
26
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
27
+ const fullPath = path.join(directory, entry.name);
28
+ if (entry.isDirectory()) {
29
+ if (!isExcluded(rootDir, fullPath, excludePatterns))
30
+ walk(fullPath);
31
+ }
32
+ else if (entry.isFile() &&
33
+ hasExtension(fullPath, BASE_SOURCE_EXTENSIONS) &&
34
+ !isExcluded(rootDir, fullPath, excludePatterns)) {
35
+ files.push(fullPath);
36
+ }
37
+ }
38
+ }
39
+ for (const includePath of config.includePaths) {
40
+ walk(resolveProjectPath(rootDir, includePath));
41
+ }
42
+ return files.sort();
43
+ }
44
+ function isInsideJsDoc(node) {
45
+ return node.getAncestors().some((ancestor) => ancestor.getKindName().startsWith('JSDoc'));
46
+ }
47
+ function isAllowed(node) {
48
+ for (const ancestor of node.getAncestors()) {
49
+ if (Node.isJSDocable(ancestor)) {
50
+ return ancestor.getJsDocs().some((doc) => doc.getText().includes('@anyAllowed'));
51
+ }
52
+ }
53
+ /* v8 ignore next -- parsed nodes always reach a JSDocable source ancestor */
54
+ return false;
55
+ }
56
+ export function findAnyOccurrences(filePath, project) {
57
+ const sourceFile = project.addSourceFileAtPath(filePath);
58
+ const seenPositions = new Set();
59
+ const occurrences = [];
60
+ for (const node of sourceFile.getDescendantsOfKind(SyntaxKind.AnyKeyword)) {
61
+ const position = node.getStart();
62
+ if (seenPositions.has(position) || isInsideJsDoc(node) || isAllowed(node))
63
+ continue;
64
+ seenPositions.add(position);
65
+ const location = sourceFile.getLineAndColumnAtPos(position);
66
+ occurrences.push({
67
+ line: location.line,
68
+ column: location.column,
69
+ context: node.getParentOrThrow().getText().replace(/\s+/gu, ' ').slice(0, 120),
70
+ });
71
+ }
72
+ return occurrences;
73
+ }
74
+ export async function runAnyGuard(options = {}) {
75
+ const rootDir = options.rootDir ?? process.cwd();
76
+ const config = options.config ?? await loadGuardConfig('any', rootDir);
77
+ const excludePatterns = compilePatterns(config.excludePatterns, BASE_SOURCE_EXCLUDE_PATTERNS);
78
+ const files = collectFiles(rootDir, config, excludePatterns);
79
+ assertConfiguredScanScope({
80
+ root: rootDir,
81
+ guardName: 'any',
82
+ configPath: 'guards.any.includePaths',
83
+ configuredPaths: config.includePaths,
84
+ eligibleFiles: files,
85
+ });
86
+ console.info(`${colors.bright}${colors.blue}🔍 Running TypeScript any guard...${colors.reset}`);
87
+ console.info(`${colors.cyan} • Paths: ${config.includePaths.join(', ')}${colors.reset}`);
88
+ const project = new Project({ skipAddingFilesFromTsConfig: true });
89
+ const reports = [];
90
+ for (const filePath of files) {
91
+ const occurrences = findAnyOccurrences(filePath, project);
92
+ if (occurrences.length > 0) {
93
+ reports.push({
94
+ filePath: normalizedRelativePath(rootDir, filePath),
95
+ occurrences,
96
+ });
97
+ }
98
+ }
99
+ if (reports.length === 0) {
100
+ console.info(`${colors.green}${colors.bright}✨ Success! No forbidden 'any' usage found outside excluded files.${colors.reset}`);
101
+ return 0;
102
+ }
103
+ const total = reports.reduce((sum, report) => sum + report.occurrences.length, 0);
104
+ console.error(`${colors.bright}${colors.red}Forbidden 'any' usage found (${total} total):${colors.reset}`);
105
+ for (const report of reports) {
106
+ for (const occurrence of report.occurrences) {
107
+ console.error(`${colors.gray}${report.filePath}:${occurrence.line}:${occurrence.column}${colors.reset} ${occurrence.context}`);
108
+ }
109
+ }
110
+ return 1;
111
+ }
112
+ /* v8 ignore start -- executable adapter */
113
+ if (isMainModule(import.meta.url)) {
114
+ runAnyGuard().then((code) => {
115
+ process.exitCode = code;
116
+ }).catch((error) => {
117
+ console.error(error.message);
118
+ process.exitCode = 1;
119
+ });
120
+ }
121
+ /* v8 ignore stop */
@@ -0,0 +1 @@
1
+ export declare function runNoTestsInDist(inputDirs?: string[], rootDir?: string): Promise<number>;
@@ -0,0 +1,56 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join, relative, resolve } from 'node:path';
3
+ import { isMainModule } from './guard-config.js';
4
+ const forbiddenPattern = /(^|\/)__tests__(\/|$)|\.(test|spec)\.(cjs|mjs|js|jsx|ts|tsx|cts|mts|d\.ts)$/i;
5
+ async function collectFilesRecursively(dir, rootDir) {
6
+ const entries = await readdir(dir, { withFileTypes: true });
7
+ const files = [];
8
+ for (const entry of entries) {
9
+ const absolutePath = join(dir, entry.name);
10
+ if (entry.isDirectory()) {
11
+ files.push(...(await collectFilesRecursively(absolutePath, rootDir)));
12
+ continue;
13
+ }
14
+ /* v8 ignore next -- non-file directory entries are intentionally ignored */
15
+ if (!entry.isFile())
16
+ continue;
17
+ const rel = relative(rootDir, absolutePath).replaceAll('\\', '/');
18
+ files.push(rel);
19
+ }
20
+ return files;
21
+ }
22
+ export async function runNoTestsInDist(inputDirs = [], rootDir = process.cwd()) {
23
+ const outputDirs = inputDirs.length > 0 ? inputDirs : ['dist'];
24
+ let hasViolations = false;
25
+ for (const outputDir of outputDirs) {
26
+ const absoluteOutputDir = resolve(rootDir, outputDir);
27
+ let files = [];
28
+ try {
29
+ files = await collectFilesRecursively(absoluteOutputDir, absoluteOutputDir);
30
+ }
31
+ catch (error) {
32
+ hasViolations = true;
33
+ console.error(`[dist-check] Could not read ${outputDir}: ${String(error)}`);
34
+ continue;
35
+ }
36
+ const violations = files.filter((file) => forbiddenPattern.test(file));
37
+ if (violations.length === 0)
38
+ continue;
39
+ hasViolations = true;
40
+ console.error(`[dist-check] Found test artifacts in ${outputDir}:`);
41
+ for (const file of violations) {
42
+ console.error(` - ${file}`);
43
+ }
44
+ }
45
+ return hasViolations ? 1 : 0;
46
+ }
47
+ /* v8 ignore start -- executable adapter */
48
+ if (isMainModule(import.meta.url)) {
49
+ runNoTestsInDist(process.argv.slice(2)).then((code) => {
50
+ process.exitCode = code;
51
+ }).catch((error) => {
52
+ console.error(error.message);
53
+ process.exitCode = 1;
54
+ });
55
+ }
56
+ /* v8 ignore stop */