@webpieces/nx-webpieces-rules 0.4.487 → 0.4.489
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/package.json +6 -6
- package/src/executors/generate/executor.js +42 -19
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-api-relations/executor.js +6 -1
- package/src/executors/validate-api-relations/executor.js.map +1 -1
- package/src/executors/validate-architecture-unchanged/executor.js +65 -15
- package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
- package/src/executors/validate-eslint-sync/executor.js +3 -2
- package/src/executors/validate-eslint-sync/executor.js.map +1 -1
- package/src/executors/validate-packagejson/executor.js +4 -2
- package/src/executors/validate-packagejson/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +3 -1
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/executors/validate-versions-locked/executor.js +3 -2
- package/src/executors/validate-versions-locked/executor.js.map +1 -1
- package/src/lib/api-usage/api-ast.d.ts +71 -0
- package/src/lib/api-usage/api-ast.js +250 -0
- package/src/lib/api-usage/api-ast.js.map +1 -0
- package/src/lib/api-usage/api-relations.d.ts +72 -3
- package/src/lib/api-usage/api-relations.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +43 -4
- package/src/lib/api-usage/api-scanner.js +121 -103
- package/src/lib/api-usage/api-scanner.js.map +1 -1
- package/src/lib/graph-loader.d.ts +21 -2
- package/src/lib/graph-loader.js +38 -4
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/rule-gate.d.ts +8 -5
- package/src/lib/rule-gate.js +8 -5
- package/src/lib/rule-gate.js.map +1 -1
- package/src/lib/runtime-config.d.ts +7 -1
- package/src/lib/runtime-config.js +7 -1
- package/src/lib/runtime-config.js.map +1 -1
- package/src/lib/runtime-graph-io.d.ts +17 -0
- package/src/lib/runtime-graph-io.js +59 -0
- package/src/lib/runtime-graph-io.js.map +1 -0
- package/src/lib/runtime-graph-model.d.ts +121 -0
- package/src/lib/runtime-graph-model.js +14 -0
- package/src/lib/runtime-graph-model.js.map +1 -0
- package/src/lib/runtime-graph.d.ts +6 -75
- package/src/lib/runtime-graph.js +163 -51
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-visualizer.d.ts +5 -0
- package/src/lib/runtime-visualizer.js +63 -6
- package/src/lib/runtime-visualizer.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-versions-locked/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Versions Locked Executor\n *\n * Validates that package.json versions are:\n * 1. LOCKED (exact versions, no semver ranges like ^, ~, *)\n * 2. CONSISTENT across all package.json files (no version conflicts)\n *\n * Why locked versions matter:\n * - Micro bugs ARE introduced via patch versions (1.4.5 → 1.4.6)\n * - git bisect fails when software changes OUTSIDE of git\n * - Library upgrades must be explicit via PR/commit, not implicit drift\n *\n * Usage:\n * nx run architecture:validate-versions-locked\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { RuleGate } from '../../lib/rule-gate';\nimport { toError } from '../../toError';\n\nexport interface ValidateVersionsLockedOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n// webpieces-disable max-lines-new-methods -- Existing method from renamed validate-versions file\n// Find all package.json files except node_modules, dist, .nx, .angular\nexport function findPackageJsonFiles(dir: string, basePath = ''): string[] {\n const files: string[] = [];\n const items = fs.readdirSync(dir);\n\n for (const item of items) {\n const fullPath = path.join(dir, item);\n const relativePath = path.join(basePath, item);\n\n // Skip these directories\n if (\n ['node_modules', 'dist', '.nx', '.angular', 'tmp', '.git'].includes(\n item,\n )\n ) {\n continue;\n }\n\n // Skip platform-specific node_modules backups (node_modules_mac, node_modules_linux, etc.)\n if (item.startsWith('node_modules_')) {\n continue;\n }\n\n // Skip all hidden directories (starting with .)\n if (item.startsWith('.')) {\n continue;\n }\n\n const stat = fs.statSync(fullPath);\n if (stat.isDirectory()) {\n // Don't descend into a nested git repository (e.g. a clone under repositories/): it is a\n // separate project with its own version policy, NOT part of this monorepo's version lock.\n // The presence of a `.git` entry (dir for a real clone, file for a worktree/submodule)\n // marks a repo boundary — the same signal the AI guards use to scope to the current repo.\n if (fs.existsSync(path.join(fullPath, '.git'))) {\n continue;\n }\n files.push(...findPackageJsonFiles(fullPath, relativePath));\n } else if (item === 'package.json') {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n// Check if a version string uses semver ranges\nfunction hasSemverRange(version: string): boolean {\n // Allow workspace protocol\n if (version.startsWith('workspace:')) {\n return false;\n }\n\n // Allow file: protocol (for local packages)\n if (version.startsWith('file:')) {\n return false;\n }\n\n // Check for common semver range patterns\n const semverPatterns = [\n /^\\^/, // ^1.2.3\n /^~/, // ~1.2.3\n /^\\+/, // +1.2.3\n /^\\*/, // *\n /^>/, // >1.2.3\n /^</, // <1.2.3\n /^>=/, // >=1.2.3\n /^<=/, // <=1.2.3\n /\\|\\|/, // 1.2.3 || 2.x\n / - /, // 1.2.3 - 2.3.4\n /^\\d+\\.x/, // 1.x, 1.2.x\n /^latest$/, // latest\n /^next$/, // next\n ];\n\n return semverPatterns.some((pattern) => pattern.test(version));\n}\n\n// webpieces-disable max-lines-new-methods -- Existing method from renamed validate-versions file\n// Validate a single package.json file for semver ranges\nfunction validatePackageJson(filePath: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n const pkg = JSON.parse(content);\n const errors: string[] = [];\n\n // Check dependencies\n if (pkg.dependencies) {\n for (const [name, version] of Object.entries(pkg.dependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) {\n continue;\n }\n\n if (hasSemverRange(version as string)) {\n errors.push(\n `dependencies.${name}: \"${version}\" uses semver range (must be locked to exact version)`,\n );\n }\n }\n }\n\n // Check devDependencies\n if (pkg.devDependencies) {\n for (const [name, version] of Object.entries(pkg.devDependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) {\n continue;\n }\n\n if (hasSemverRange(version as string)) {\n errors.push(\n `devDependencies.${name}: \"${version}\" uses semver range (must be locked to exact version)`,\n );\n }\n }\n }\n\n // Check peerDependencies (these can have ranges for compatibility)\n // We don't validate peerDependencies for semver ranges since they're meant to be flexible\n\n return errors;\n } catch (err: unknown) {\n const error = toError(err);\n return [`Failed to parse ${filePath}: ${error.message}`];\n }\n}\n\n// Track all dependency versions across the monorepo\ninterface DependencyUsage {\n version: string;\n file: string;\n type: 'dependencies' | 'devDependencies';\n}\n\n// webpieces-disable max-lines-new-methods -- Collecting dependencies from all package.json files\n// Collect all dependency versions from all package.json files\nfunction collectAllDependencies(workspaceRoot: string): Map<string, DependencyUsage[]> {\n const dependencyMap = new Map<string, DependencyUsage[]>();\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n\n for (const filePath of packageFiles) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n const pkg = JSON.parse(content);\n const relativePath = path.relative(workspaceRoot, filePath);\n\n // Collect dependencies\n if (pkg.dependencies) {\n for (const [name, version] of Object.entries(pkg.dependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) continue;\n\n const usage: DependencyUsage = {\n version: version as string,\n file: relativePath,\n type: 'dependencies'\n };\n\n if (!dependencyMap.has(name)) {\n dependencyMap.set(name, []);\n }\n dependencyMap.get(name)!.push(usage);\n }\n }\n\n // Collect devDependencies\n if (pkg.devDependencies) {\n for (const [name, version] of Object.entries(pkg.devDependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) continue;\n\n const usage: DependencyUsage = {\n version: version as string,\n file: relativePath,\n type: 'devDependencies'\n };\n\n if (!dependencyMap.has(name)) {\n dependencyMap.set(name, []);\n }\n dependencyMap.get(name)!.push(usage);\n }\n }\n } catch (err: unknown) {\n // const error = toError(err);\n // Intentionally skip files that can't be parsed - this is expected for some package.json files\n }\n }\n\n return dependencyMap;\n}\n\n// webpieces-disable max-lines-new-methods -- Simple iteration logic, splitting would reduce clarity\n// Check for version conflicts across package.json files\nfunction checkVersionConflicts(workspaceRoot: string): string[] {\n console.log('\\n🔍 Checking for version conflicts across package.json files:');\n\n const dependencyMap = collectAllDependencies(workspaceRoot);\n const conflicts: string[] = [];\n\n for (const [packageName, usages] of dependencyMap.entries()) {\n // Get unique versions (ignoring workspace: and file: protocols)\n const versions = new Set(\n usages\n .map(u => u.version)\n .filter(v => !v.startsWith('workspace:') && !v.startsWith('file:'))\n );\n\n if (versions.size > 1) {\n const conflictDetails = usages\n .filter(u => !u.version.startsWith('workspace:') && !u.version.startsWith('file:'))\n .map(u => ` ${u.file} (${u.type}): ${u.version}`)\n .join('\\n');\n\n conflicts.push(` ❌ ${packageName} has ${versions.size} different versions:\\n${conflictDetails}`);\n }\n }\n\n if (conflicts.length === 0) {\n console.log(' ✅ No version conflicts found');\n } else {\n for (const conflict of conflicts) {\n console.log(conflict);\n }\n }\n\n return conflicts;\n}\n\n/**\n * Prints the educational message explaining why semver ranges are forbidden.\n * This helps developers understand the rationale behind locked versions.\n */\n// webpieces-disable max-lines-new-methods -- Educational message template, splitting reduces clarity\nfunction printSemverRangeEducationalMessage(semverErrors: number): void {\n console.log(`\n❌ SEMVER RANGES DETECTED - BUILD FAILED\n\nFound ${semverErrors} package(s) using semver ranges (^, ~, *, etc.) instead of locked versions.\n\nWHY THIS IS A HARD FAILURE:\n═══════════════════════════════════════════════════════════════════════════════\n\n1. MICRO BUGS ARE REAL\n Thinking that patch versions (1.4.5 → 1.4.6) don't introduce bugs is wrong.\n They do. Sometimes what looks like an \"easy fix\" breaks things in subtle ways.\n\n2. GIT BISECT BECOMES USELESS\n When you run \"git bisect\" to find when a bug was introduced, it fails if\n software changed OUTSIDE of git. You checkout an old commit, but node_modules\n has different versions than when that commit was made. The bug persists even\n in \"known good\" commits because the library versions drifted.\n\n3. THE \"MAGIC BUG\" PROBLEM\n You checkout code from 6 months ago to debug an issue. The bug is still there!\n But it wasn't there 6 months ago... The culprit: a minor version upgrade that\n happened silently without any PR or git commit. Impossible to track down.\n\n4. CHANGES OUTSIDE GIT = BAD\n Every change to your software should be tracked in version control.\n Implicit library upgrades via semver ranges violate this principle.\n\nTHE SOLUTION:\n═══════════════════════════════════════════════════════════════════════════════\n\nUse LOCKED (exact) versions for all dependencies:\n ❌ \"lodash\": \"^4.17.21\" <- BAD: allows 4.17.22, 4.18.0, etc.\n ❌ \"lodash\": \"~4.17.21\" <- BAD: allows 4.17.22, 4.17.23, etc.\n ✅ \"lodash\": \"4.17.21\" <- GOOD: locked to this exact version\n\nTo upgrade libraries, use an explicit process:\n 1. Run: npm update <package-name>\n 2. Test thoroughly\n 3. Commit the package.json AND package-lock.json changes\n 4. Create a PR so the upgrade is reviewed and tracked in git history\n\nThis way, every library change is:\n • Intentional (not accidental)\n • Reviewed (via PR)\n • Tracked (in git history)\n • Bisectable (git bisect works correctly)\n\n`);\n}\n\ntype SemverRangeResult = { errors: number };\n\n// Check semver ranges in all package.json files - FAILS if any found\nfunction checkSemverRanges(workspaceRoot: string): SemverRangeResult {\n console.log('\\n📋 Checking for unlocked versions (semver ranges):');\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n let semverErrors = 0;\n\n for (const filePath of packageFiles) {\n const relativePath = path.relative(workspaceRoot, filePath);\n const errors = validatePackageJson(filePath);\n\n if (errors.length > 0) {\n console.log(` ❌ ${relativePath}:`);\n for (const error of errors) {\n console.log(` ${error}`);\n }\n semverErrors += errors.length;\n } else {\n console.log(` ✅ ${relativePath}`);\n }\n }\n\n return { errors: semverErrors };\n}\n\nexport default async function runExecutor(\n _options: ValidateVersionsLockedOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n // All-or-nothing (honorEpoch = false): there is no blessed baseline to grandfather against.\n if (new RuleGate().isDisabled(workspaceRoot, 'validate-versions-locked', false)) {\n return { success: true };\n }\n\n console.log('\\n🔒 Validating Package Versions are LOCKED and CONSISTENT\\n');\n\n // Step 1: Check for semver ranges (FAILS if any found)\n const semverResult = checkSemverRanges(workspaceRoot);\n const semverErrors = semverResult.errors;\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n\n // Step 2: Check for version conflicts across package.json files\n const versionConflicts = checkVersionConflicts(workspaceRoot);\n\n // Summary\n console.log(`\\n📊 Summary:`);\n console.log(` Files checked: ${packageFiles.length}`);\n console.log(` Unlocked versions (semver ranges): ${semverErrors}`);\n console.log(` Version conflicts: ${versionConflicts.length}`);\n\n // Fail on semver ranges with educational message\n if (semverErrors > 0) {\n printSemverRangeEducationalMessage(semverErrors);\n return { success: false };\n }\n\n // Fail on version conflicts\n if (versionConflicts.length > 0) {\n console.log('\\n❌ VALIDATION FAILED!');\n console.log(' Fix version conflicts - all package.json files must use the same version for each dependency.');\n console.log(' This prevents \"works on my machine\" bugs where different projects use different library versions.\\n');\n return { success: false };\n }\n\n console.log('\\n✅ VALIDATION PASSED! All versions are locked and consistent.');\n return { success: true };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-versions-locked/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate Versions Locked Executor\n *\n * Validates that package.json versions are:\n * 1. LOCKED (exact versions, no semver ranges like ^, ~, *)\n * 2. CONSISTENT across all package.json files (no version conflicts)\n *\n * Why locked versions matter:\n * - Micro bugs ARE introduced via patch versions (1.4.5 → 1.4.6)\n * - git bisect fails when software changes OUTSIDE of git\n * - Library upgrades must be explicit via PR/commit, not implicit drift\n *\n * Usage:\n * nx run architecture:validate-versions-locked\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { RuleGate } from '../../lib/rule-gate';\nimport { toError } from '../../toError';\n\nexport interface ValidateVersionsLockedOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n// webpieces-disable max-lines-new-methods -- Existing method from renamed validate-versions file\n// Find all package.json files except node_modules, dist, .nx, .angular\nexport function findPackageJsonFiles(dir: string, basePath = ''): string[] {\n const files: string[] = [];\n const items = fs.readdirSync(dir);\n\n for (const item of items) {\n const fullPath = path.join(dir, item);\n const relativePath = path.join(basePath, item);\n\n // Skip these directories\n if (\n ['node_modules', 'dist', '.nx', '.angular', 'tmp', '.git'].includes(\n item,\n )\n ) {\n continue;\n }\n\n // Skip platform-specific node_modules backups (node_modules_mac, node_modules_linux, etc.)\n if (item.startsWith('node_modules_')) {\n continue;\n }\n\n // Skip all hidden directories (starting with .)\n if (item.startsWith('.')) {\n continue;\n }\n\n const stat = fs.statSync(fullPath);\n if (stat.isDirectory()) {\n // Don't descend into a nested git repository (e.g. a clone under repositories/): it is a\n // separate project with its own version policy, NOT part of this monorepo's version lock.\n // The presence of a `.git` entry (dir for a real clone, file for a worktree/submodule)\n // marks a repo boundary — the same signal the AI guards use to scope to the current repo.\n if (fs.existsSync(path.join(fullPath, '.git'))) {\n continue;\n }\n files.push(...findPackageJsonFiles(fullPath, relativePath));\n } else if (item === 'package.json') {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n// Check if a version string uses semver ranges\nfunction hasSemverRange(version: string): boolean {\n // Allow workspace protocol\n if (version.startsWith('workspace:')) {\n return false;\n }\n\n // Allow file: protocol (for local packages)\n if (version.startsWith('file:')) {\n return false;\n }\n\n // Check for common semver range patterns\n const semverPatterns = [\n /^\\^/, // ^1.2.3\n /^~/, // ~1.2.3\n /^\\+/, // +1.2.3\n /^\\*/, // *\n /^>/, // >1.2.3\n /^</, // <1.2.3\n /^>=/, // >=1.2.3\n /^<=/, // <=1.2.3\n /\\|\\|/, // 1.2.3 || 2.x\n / - /, // 1.2.3 - 2.3.4\n /^\\d+\\.x/, // 1.x, 1.2.x\n /^latest$/, // latest\n /^next$/, // next\n ];\n\n return semverPatterns.some((pattern) => pattern.test(version));\n}\n\n// webpieces-disable max-lines-new-methods -- Existing method from renamed validate-versions file\n// Validate a single package.json file for semver ranges\nfunction validatePackageJson(filePath: string): string[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n const pkg = JSON.parse(content);\n const errors: string[] = [];\n\n // Check dependencies\n if (pkg.dependencies) {\n for (const [name, version] of Object.entries(pkg.dependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) {\n continue;\n }\n\n if (hasSemverRange(version as string)) {\n errors.push(\n `dependencies.${name}: \"${version}\" uses semver range (must be locked to exact version)`,\n );\n }\n }\n }\n\n // Check devDependencies\n if (pkg.devDependencies) {\n for (const [name, version] of Object.entries(pkg.devDependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) {\n continue;\n }\n\n if (hasSemverRange(version as string)) {\n errors.push(\n `devDependencies.${name}: \"${version}\" uses semver range (must be locked to exact version)`,\n );\n }\n }\n }\n\n // Check peerDependencies (these can have ranges for compatibility)\n // We don't validate peerDependencies for semver ranges since they're meant to be flexible\n\n return errors;\n } catch (err: unknown) {\n const error = toError(err);\n return [`Failed to parse ${filePath}: ${error.message}`];\n }\n}\n\n// Track all dependency versions across the monorepo\ninterface DependencyUsage {\n version: string;\n file: string;\n type: 'dependencies' | 'devDependencies';\n}\n\n// webpieces-disable max-lines-new-methods -- Collecting dependencies from all package.json files\n// Collect all dependency versions from all package.json files\nfunction collectAllDependencies(workspaceRoot: string): Map<string, DependencyUsage[]> {\n const dependencyMap = new Map<string, DependencyUsage[]>();\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n\n for (const filePath of packageFiles) {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf-8');\n const pkg = JSON.parse(content);\n const relativePath = path.relative(workspaceRoot, filePath);\n\n // Collect dependencies\n if (pkg.dependencies) {\n for (const [name, version] of Object.entries(pkg.dependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) continue;\n\n const usage: DependencyUsage = {\n version: version as string,\n file: relativePath,\n type: 'dependencies'\n };\n\n if (!dependencyMap.has(name)) {\n dependencyMap.set(name, []);\n }\n dependencyMap.get(name)!.push(usage);\n }\n }\n\n // Collect devDependencies\n if (pkg.devDependencies) {\n for (const [name, version] of Object.entries(pkg.devDependencies)) {\n // Skip internal workspace packages\n if (name.startsWith('@webpieces/')) continue;\n\n const usage: DependencyUsage = {\n version: version as string,\n file: relativePath,\n type: 'devDependencies'\n };\n\n if (!dependencyMap.has(name)) {\n dependencyMap.set(name, []);\n }\n dependencyMap.get(name)!.push(usage);\n }\n }\n } catch (err: unknown) {\n // const error = toError(err);\n // Intentionally skip files that can't be parsed - this is expected for some package.json files\n }\n }\n\n return dependencyMap;\n}\n\n// webpieces-disable max-lines-new-methods -- Simple iteration logic, splitting would reduce clarity\n// Check for version conflicts across package.json files\nfunction checkVersionConflicts(workspaceRoot: string): string[] {\n console.log('\\n🔍 Checking for version conflicts across package.json files:');\n\n const dependencyMap = collectAllDependencies(workspaceRoot);\n const conflicts: string[] = [];\n\n for (const [packageName, usages] of dependencyMap.entries()) {\n // Get unique versions (ignoring workspace: and file: protocols)\n const versions = new Set(\n usages\n .map(u => u.version)\n .filter(v => !v.startsWith('workspace:') && !v.startsWith('file:'))\n );\n\n if (versions.size > 1) {\n const conflictDetails = usages\n .filter(u => !u.version.startsWith('workspace:') && !u.version.startsWith('file:'))\n .map(u => ` ${u.file} (${u.type}): ${u.version}`)\n .join('\\n');\n\n conflicts.push(` ❌ ${packageName} has ${versions.size} different versions:\\n${conflictDetails}`);\n }\n }\n\n if (conflicts.length === 0) {\n console.log(' ✅ No version conflicts found');\n } else {\n for (const conflict of conflicts) {\n console.log(conflict);\n }\n }\n\n return conflicts;\n}\n\n/**\n * Prints the educational message explaining why semver ranges are forbidden.\n * This helps developers understand the rationale behind locked versions.\n */\n// webpieces-disable max-lines-new-methods -- Educational message template, splitting reduces clarity\nfunction printSemverRangeEducationalMessage(semverErrors: number): void {\n console.log(`\n❌ SEMVER RANGES DETECTED - BUILD FAILED\n\nFound ${semverErrors} package(s) using semver ranges (^, ~, *, etc.) instead of locked versions.\n\nWHY THIS IS A HARD FAILURE:\n═══════════════════════════════════════════════════════════════════════════════\n\n1. MICRO BUGS ARE REAL\n Thinking that patch versions (1.4.5 → 1.4.6) don't introduce bugs is wrong.\n They do. Sometimes what looks like an \"easy fix\" breaks things in subtle ways.\n\n2. GIT BISECT BECOMES USELESS\n When you run \"git bisect\" to find when a bug was introduced, it fails if\n software changed OUTSIDE of git. You checkout an old commit, but node_modules\n has different versions than when that commit was made. The bug persists even\n in \"known good\" commits because the library versions drifted.\n\n3. THE \"MAGIC BUG\" PROBLEM\n You checkout code from 6 months ago to debug an issue. The bug is still there!\n But it wasn't there 6 months ago... The culprit: a minor version upgrade that\n happened silently without any PR or git commit. Impossible to track down.\n\n4. CHANGES OUTSIDE GIT = BAD\n Every change to your software should be tracked in version control.\n Implicit library upgrades via semver ranges violate this principle.\n\nTHE SOLUTION:\n═══════════════════════════════════════════════════════════════════════════════\n\nUse LOCKED (exact) versions for all dependencies:\n ❌ \"lodash\": \"^4.17.21\" <- BAD: allows 4.17.22, 4.18.0, etc.\n ❌ \"lodash\": \"~4.17.21\" <- BAD: allows 4.17.22, 4.17.23, etc.\n ✅ \"lodash\": \"4.17.21\" <- GOOD: locked to this exact version\n\nTo upgrade libraries, use an explicit process:\n 1. Run: npm update <package-name>\n 2. Test thoroughly\n 3. Commit the package.json AND package-lock.json changes\n 4. Create a PR so the upgrade is reviewed and tracked in git history\n\nThis way, every library change is:\n • Intentional (not accidental)\n • Reviewed (via PR)\n • Tracked (in git history)\n • Bisectable (git bisect works correctly)\n\n`);\n}\n\ntype SemverRangeResult = { errors: number };\n\n// Check semver ranges in all package.json files - FAILS if any found\nfunction checkSemverRanges(workspaceRoot: string): SemverRangeResult {\n console.log('\\n📋 Checking for unlocked versions (semver ranges):');\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n let semverErrors = 0;\n\n for (const filePath of packageFiles) {\n const relativePath = path.relative(workspaceRoot, filePath);\n const errors = validatePackageJson(filePath);\n\n if (errors.length > 0) {\n console.log(` ❌ ${relativePath}:`);\n for (const error of errors) {\n console.log(` ${error}`);\n }\n semverErrors += errors.length;\n } else {\n console.log(` ✅ ${relativePath}`);\n }\n }\n\n return { errors: semverErrors };\n}\n\nexport default async function runExecutor(\n _options: ValidateVersionsLockedOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const workspaceRoot = context.root;\n\n // Time-boxable (honorEpoch = true): a schedule is not a baseline — \"do not enforce until <epoch>\n // / off <branch>\" is coherent even with nothing to grandfather. See rule-configs.ts (five-validator block).\n if (new RuleGate().isDisabled(workspaceRoot, 'validate-versions-locked', true)) {\n return { success: true };\n }\n\n console.log('\\n🔒 Validating Package Versions are LOCKED and CONSISTENT\\n');\n\n // Step 1: Check for semver ranges (FAILS if any found)\n const semverResult = checkSemverRanges(workspaceRoot);\n const semverErrors = semverResult.errors;\n const packageFiles = findPackageJsonFiles(workspaceRoot);\n\n // Step 2: Check for version conflicts across package.json files\n const versionConflicts = checkVersionConflicts(workspaceRoot);\n\n // Summary\n console.log(`\\n📊 Summary:`);\n console.log(` Files checked: ${packageFiles.length}`);\n console.log(` Unlocked versions (semver ranges): ${semverErrors}`);\n console.log(` Version conflicts: ${versionConflicts.length}`);\n\n // Fail on semver ranges with educational message\n if (semverErrors > 0) {\n printSemverRangeEducationalMessage(semverErrors);\n return { success: false };\n }\n\n // Fail on version conflicts\n if (versionConflicts.length > 0) {\n console.log('\\n❌ VALIDATION FAILED!');\n console.log(' Fix version conflicts - all package.json files must use the same version for each dependency.');\n console.log(' This prevents \"works on my machine\" bugs where different projects use different library versions.\\n');\n return { success: false };\n }\n\n console.log('\\n✅ VALIDATION PASSED! All versions are locked and consistent.');\n return { success: true };\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API contract AST accessors
|
|
3
|
+
*
|
|
4
|
+
* The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /
|
|
5
|
+
* injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project
|
|
6
|
+
* programs, the source index, relation accumulation) and had grown past the file-size limit.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is parser-level on purpose. Decorators must be read exactly as written, and a
|
|
9
|
+
* plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug
|
|
10
|
+
* api-scanner's source pre-pass exists to guard against.
|
|
11
|
+
*/
|
|
12
|
+
import * as ts from 'typescript';
|
|
13
|
+
import { ApiClassInfo, ApiMethodMeta, ApiTransport } from './api-relations';
|
|
14
|
+
/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
|
|
15
|
+
export declare function apiClassInfoFrom(cls: ts.ClassDeclaration, project: string): ApiClassInfo | null;
|
|
16
|
+
export declare function apiTransport(cls: ts.ClassDeclaration): ApiTransport;
|
|
17
|
+
/**
|
|
18
|
+
* Every `@Endpoint(path, kind)` method on a contract class, in declaration order.
|
|
19
|
+
*
|
|
20
|
+
* `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the
|
|
21
|
+
* source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting
|
|
22
|
+
* would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
|
|
23
|
+
* the blindness the required argument exists to remove.
|
|
24
|
+
*/
|
|
25
|
+
export declare function endpointMethodsOf(cls: ts.ClassDeclaration, api: string): ApiMethodMeta[];
|
|
26
|
+
/** The arguments of a decorator's call expression, or [] when it is a bare `@Foo` reference. */
|
|
27
|
+
export declare function decoratorArgs(decorator: ts.Decorator): ts.NodeArray<ts.Expression> | ts.Expression[];
|
|
28
|
+
/** The named decorator on a class member, or null. */
|
|
29
|
+
export declare function memberDecorator(member: ts.ClassElement, name: string): ts.Decorator | null;
|
|
30
|
+
/** The first argument of a class decorator when it is a string literal (`@ApiPath('/x')`), else null. */
|
|
31
|
+
export declare function decoratorStringArg(cls: ts.ClassDeclaration, name: string): string | null;
|
|
32
|
+
/** The constructor's parameters, or [] when the class declares no constructor. */
|
|
33
|
+
export declare function constructorParamsOf(cls: ts.ClassDeclaration): readonly ts.ParameterDeclaration[];
|
|
34
|
+
/**
|
|
35
|
+
* The bare name of a type reference (`GmailApi`, or `gmail.GmailApi` -> `GmailApi`), else null.
|
|
36
|
+
* Generic wrappers are deliberately NOT unwrapped: `Provider<GmailApi>` hands out the contract
|
|
37
|
+
* lazily, which is still a use, but it is not the shape any of these seams take today and guessing
|
|
38
|
+
* at type arguments would start matching things that merely mention a contract.
|
|
39
|
+
*/
|
|
40
|
+
export declare function typeReferenceName(type: ts.TypeNode | undefined): string | null;
|
|
41
|
+
/** Every type name in the class's `implements` clause — the contracts this class IS, not ones it calls. */
|
|
42
|
+
export declare function implementedTypeNames(cls: ts.ClassDeclaration): Set<string>;
|
|
43
|
+
export declare function isAbstractClass(cls: ts.ClassDeclaration): boolean;
|
|
44
|
+
export declare function hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The service a client-factory call aims at, from its config argument:
|
|
47
|
+
* `createRpcClient(WarmupApi, new ClientConfig('helper-fsdb'))` → `'helper-fsdb'`.
|
|
48
|
+
*
|
|
49
|
+
* Only a `new <Xxx>ClientConfig('<string literal>')` yields a name. A variable, a template string
|
|
50
|
+
* or a computed expression yields null — the target is genuinely unknown at scan time, and the
|
|
51
|
+
* runtime graph must fall back to fan-out (loudly) rather than guess.
|
|
52
|
+
*/
|
|
53
|
+
export declare function targetServiceOf(call: ts.CallExpression): string | null;
|
|
54
|
+
export declare function calleeMethodName(call: ts.CallExpression): string | null;
|
|
55
|
+
export declare function isTestFile(fileName: string): boolean;
|
|
56
|
+
/** {api, owner, type:'rpc'|'pubsub'} for an in-repo contract class, else null. */
|
|
57
|
+
export declare function apiClassInfoFromNode(node: ts.Node, project: string): ApiClassInfo | null;
|
|
58
|
+
/**
|
|
59
|
+
* {api, owner, type:'external'} for a VENDOR contract, else null.
|
|
60
|
+
*
|
|
61
|
+
* A vendor contract cannot be detected the way an in-repo one is. It carries no @ApiPath (there is
|
|
62
|
+
* no route — the call leaves through a vendor SDK), and it is usually a plain `interface` bound to a
|
|
63
|
+
* Symbol token, which is not even a class. So inside a project the workspace has DECLARED external
|
|
64
|
+
* (`runtime-architecture.externalApiPaths`) the signal is structural instead: an exported
|
|
65
|
+
* `interface`/`abstract class` whose name ends in `Api`. That deliberately picks up `GmailApi` and
|
|
66
|
+
* `StorageApi` while leaving their DTOs, `*Config` types and `*Client` implementations alone.
|
|
67
|
+
*/
|
|
68
|
+
export declare function externalApiInfoFrom(node: ts.Node, project: string): ApiClassInfo | null;
|
|
69
|
+
/** True when the declaration carries an `export` modifier. */
|
|
70
|
+
export declare function isExported(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean;
|
|
71
|
+
export declare function collectTsFiles(dir: string): string[];
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API contract AST accessors
|
|
4
|
+
*
|
|
5
|
+
* The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /
|
|
6
|
+
* injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project
|
|
7
|
+
* programs, the source index, relation accumulation) and had grown past the file-size limit.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is parser-level on purpose. Decorators must be read exactly as written, and a
|
|
10
|
+
* plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug
|
|
11
|
+
* api-scanner's source pre-pass exists to guard against.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.apiClassInfoFrom = apiClassInfoFrom;
|
|
15
|
+
exports.apiTransport = apiTransport;
|
|
16
|
+
exports.endpointMethodsOf = endpointMethodsOf;
|
|
17
|
+
exports.decoratorArgs = decoratorArgs;
|
|
18
|
+
exports.memberDecorator = memberDecorator;
|
|
19
|
+
exports.decoratorStringArg = decoratorStringArg;
|
|
20
|
+
exports.constructorParamsOf = constructorParamsOf;
|
|
21
|
+
exports.typeReferenceName = typeReferenceName;
|
|
22
|
+
exports.implementedTypeNames = implementedTypeNames;
|
|
23
|
+
exports.isAbstractClass = isAbstractClass;
|
|
24
|
+
exports.hasClassDecorator = hasClassDecorator;
|
|
25
|
+
exports.targetServiceOf = targetServiceOf;
|
|
26
|
+
exports.calleeMethodName = calleeMethodName;
|
|
27
|
+
exports.isTestFile = isTestFile;
|
|
28
|
+
exports.apiClassInfoFromNode = apiClassInfoFromNode;
|
|
29
|
+
exports.externalApiInfoFrom = externalApiInfoFrom;
|
|
30
|
+
exports.isExported = isExported;
|
|
31
|
+
exports.collectTsFiles = collectTsFiles;
|
|
32
|
+
const tslib_1 = require("tslib");
|
|
33
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
34
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
35
|
+
const path = tslib_1.__importStar(require("path"));
|
|
36
|
+
const bindings_1 = require("../di-graph/bindings");
|
|
37
|
+
/** Legal `@Endpoint(path, kind)` values; anything else is a source error, not a kind we invent. */
|
|
38
|
+
const ENDPOINT_KINDS = ['rpc', 'cloudtasks', 'cron', 'external'];
|
|
39
|
+
/**
|
|
40
|
+
* Name suffix that marks an exported type in an `externalApiPaths` project as a vendor CONTRACT
|
|
41
|
+
* (`GmailApi`, `StorageApi`) rather than one of the DTOs, configs or clients sitting beside it.
|
|
42
|
+
* The same convention the in-repo contracts already follow, applied where no decorator can be read.
|
|
43
|
+
*/
|
|
44
|
+
const EXTERNAL_CONTRACT_SUFFIX = 'Api';
|
|
45
|
+
/**
|
|
46
|
+
* Client-config class-name suffix whose FIRST constructor argument is the target service name —
|
|
47
|
+
* `ClientConfig('helper-fsdb')` (rpc) and `TaskClientConfig('helper-fsdb')` (pubsub) both take
|
|
48
|
+
* `svcName` first, and a consumer's own `XxxClientConfig` follows the same shape.
|
|
49
|
+
*/
|
|
50
|
+
const CLIENT_CONFIG_SUFFIX = 'ClientConfig';
|
|
51
|
+
/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
|
|
52
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
53
|
+
function apiClassInfoFrom(cls, project) {
|
|
54
|
+
if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name)
|
|
55
|
+
return null;
|
|
56
|
+
const api = cls.name.text;
|
|
57
|
+
const info = {
|
|
58
|
+
api,
|
|
59
|
+
owner: project,
|
|
60
|
+
type: apiTransport(cls),
|
|
61
|
+
methods: endpointMethodsOf(cls, api),
|
|
62
|
+
};
|
|
63
|
+
const basePath = decoratorStringArg(cls, 'ApiPath');
|
|
64
|
+
if (basePath !== null)
|
|
65
|
+
info.basePath = basePath;
|
|
66
|
+
return info;
|
|
67
|
+
}
|
|
68
|
+
// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
69
|
+
function apiTransport(cls) {
|
|
70
|
+
return hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every `@Endpoint(path, kind)` method on a contract class, in declaration order.
|
|
74
|
+
*
|
|
75
|
+
* `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the
|
|
76
|
+
* source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting
|
|
77
|
+
* would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
|
|
78
|
+
* the blindness the required argument exists to remove.
|
|
79
|
+
*/
|
|
80
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
81
|
+
function endpointMethodsOf(cls, api) {
|
|
82
|
+
const methods = [];
|
|
83
|
+
for (const member of cls.members) {
|
|
84
|
+
if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name))
|
|
85
|
+
continue;
|
|
86
|
+
const endpoint = memberDecorator(member, 'Endpoint');
|
|
87
|
+
if (endpoint === null)
|
|
88
|
+
continue;
|
|
89
|
+
const args = decoratorArgs(endpoint);
|
|
90
|
+
const path = args[0] !== undefined && ts.isStringLiteral(args[0]) ? args[0].text : null;
|
|
91
|
+
const kind = args[1] !== undefined && ts.isStringLiteral(args[1]) ? args[1].text : null;
|
|
92
|
+
if (path === null || kind === null || !ENDPOINT_KINDS.includes(kind))
|
|
93
|
+
continue;
|
|
94
|
+
const name = member.name.text;
|
|
95
|
+
const override = memberDecorator(member, 'Queue');
|
|
96
|
+
const queueArg = override === null ? undefined : decoratorArgs(override)[0];
|
|
97
|
+
const queueName = queueArg !== undefined && ts.isStringLiteral(queueArg) ? queueArg.text : `${api}-${name}`;
|
|
98
|
+
methods.push({ name, path, kind: kind, queueName });
|
|
99
|
+
}
|
|
100
|
+
return methods;
|
|
101
|
+
}
|
|
102
|
+
/** The arguments of a decorator's call expression, or [] when it is a bare `@Foo` reference. */
|
|
103
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
104
|
+
function decoratorArgs(decorator) {
|
|
105
|
+
return ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];
|
|
106
|
+
}
|
|
107
|
+
/** The named decorator on a class member, or null. */
|
|
108
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
109
|
+
function memberDecorator(member, name) {
|
|
110
|
+
const decorators = ts.getDecorators(member) ?? [];
|
|
111
|
+
return decorators.find((d) => (0, bindings_1.decoratorName)(d) === name) ?? null;
|
|
112
|
+
}
|
|
113
|
+
/** The first argument of a class decorator when it is a string literal (`@ApiPath('/x')`), else null. */
|
|
114
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
115
|
+
function decoratorStringArg(cls, name) {
|
|
116
|
+
const decorator = (0, bindings_1.classDecorators)(cls).find((d) => (0, bindings_1.decoratorName)(d) === name);
|
|
117
|
+
if (decorator === undefined)
|
|
118
|
+
return null;
|
|
119
|
+
const first = decoratorArgs(decorator)[0];
|
|
120
|
+
return first !== undefined && ts.isStringLiteral(first) ? first.text : null;
|
|
121
|
+
}
|
|
122
|
+
/** The constructor's parameters, or [] when the class declares no constructor. */
|
|
123
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
124
|
+
function constructorParamsOf(cls) {
|
|
125
|
+
for (const member of cls.members) {
|
|
126
|
+
if (ts.isConstructorDeclaration(member))
|
|
127
|
+
return member.parameters;
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The bare name of a type reference (`GmailApi`, or `gmail.GmailApi` -> `GmailApi`), else null.
|
|
133
|
+
* Generic wrappers are deliberately NOT unwrapped: `Provider<GmailApi>` hands out the contract
|
|
134
|
+
* lazily, which is still a use, but it is not the shape any of these seams take today and guessing
|
|
135
|
+
* at type arguments would start matching things that merely mention a contract.
|
|
136
|
+
*/
|
|
137
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
138
|
+
function typeReferenceName(type) {
|
|
139
|
+
if (type === undefined || !ts.isTypeReferenceNode(type))
|
|
140
|
+
return null;
|
|
141
|
+
const name = type.typeName;
|
|
142
|
+
if (ts.isIdentifier(name))
|
|
143
|
+
return name.text;
|
|
144
|
+
return ts.isQualifiedName(name) && ts.isIdentifier(name.right) ? name.right.text : null;
|
|
145
|
+
}
|
|
146
|
+
/** Every type name in the class's `implements` clause — the contracts this class IS, not ones it calls. */
|
|
147
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
148
|
+
function implementedTypeNames(cls) {
|
|
149
|
+
const names = new Set();
|
|
150
|
+
for (const clause of cls.heritageClauses ?? []) {
|
|
151
|
+
if (clause.token !== ts.SyntaxKind.ImplementsKeyword)
|
|
152
|
+
continue;
|
|
153
|
+
for (const type of clause.types) {
|
|
154
|
+
if (ts.isIdentifier(type.expression))
|
|
155
|
+
names.add(type.expression.text);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return names;
|
|
159
|
+
}
|
|
160
|
+
// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
161
|
+
function isAbstractClass(cls) {
|
|
162
|
+
return (ts.getModifiers(cls) ?? []).some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
|
|
163
|
+
}
|
|
164
|
+
// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
165
|
+
function hasClassDecorator(cls, name) {
|
|
166
|
+
return (0, bindings_1.classDecorators)(cls).some((d) => (0, bindings_1.decoratorName)(d) === name);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The service a client-factory call aims at, from its config argument:
|
|
170
|
+
* `createRpcClient(WarmupApi, new ClientConfig('helper-fsdb'))` → `'helper-fsdb'`.
|
|
171
|
+
*
|
|
172
|
+
* Only a `new <Xxx>ClientConfig('<string literal>')` yields a name. A variable, a template string
|
|
173
|
+
* or a computed expression yields null — the target is genuinely unknown at scan time, and the
|
|
174
|
+
* runtime graph must fall back to fan-out (loudly) rather than guess.
|
|
175
|
+
*/
|
|
176
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
177
|
+
function targetServiceOf(call) {
|
|
178
|
+
if (call.arguments.length < 2)
|
|
179
|
+
return null;
|
|
180
|
+
const config = call.arguments[1];
|
|
181
|
+
if (!ts.isNewExpression(config) || !ts.isIdentifier(config.expression))
|
|
182
|
+
return null;
|
|
183
|
+
if (!config.expression.text.endsWith(CLIENT_CONFIG_SUFFIX))
|
|
184
|
+
return null;
|
|
185
|
+
const first = config.arguments?.[0];
|
|
186
|
+
if (first === undefined || !ts.isStringLiteral(first))
|
|
187
|
+
return null;
|
|
188
|
+
return first.text.length > 0 ? first.text : null;
|
|
189
|
+
}
|
|
190
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
191
|
+
function calleeMethodName(call) {
|
|
192
|
+
const callee = call.expression;
|
|
193
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
194
|
+
return callee.name.text;
|
|
195
|
+
if (ts.isIdentifier(callee))
|
|
196
|
+
return callee.text;
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
// webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
200
|
+
function isTestFile(fileName) {
|
|
201
|
+
return (fileName.includes('/__tests__/') ||
|
|
202
|
+
fileName.includes('.spec.') ||
|
|
203
|
+
fileName.includes('.test.'));
|
|
204
|
+
}
|
|
205
|
+
/** {api, owner, type:'rpc'|'pubsub'} for an in-repo contract class, else null. */
|
|
206
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
207
|
+
function apiClassInfoFromNode(node, project) {
|
|
208
|
+
return ts.isClassDeclaration(node) ? apiClassInfoFrom(node, project) : null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* {api, owner, type:'external'} for a VENDOR contract, else null.
|
|
212
|
+
*
|
|
213
|
+
* A vendor contract cannot be detected the way an in-repo one is. It carries no @ApiPath (there is
|
|
214
|
+
* no route — the call leaves through a vendor SDK), and it is usually a plain `interface` bound to a
|
|
215
|
+
* Symbol token, which is not even a class. So inside a project the workspace has DECLARED external
|
|
216
|
+
* (`runtime-architecture.externalApiPaths`) the signal is structural instead: an exported
|
|
217
|
+
* `interface`/`abstract class` whose name ends in `Api`. That deliberately picks up `GmailApi` and
|
|
218
|
+
* `StorageApi` while leaving their DTOs, `*Config` types and `*Client` implementations alone.
|
|
219
|
+
*/
|
|
220
|
+
// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
|
|
221
|
+
function externalApiInfoFrom(node, project) {
|
|
222
|
+
const named = ts.isInterfaceDeclaration(node) || (ts.isClassDeclaration(node) && isAbstractClass(node));
|
|
223
|
+
if (!named || !node.name || !isExported(node))
|
|
224
|
+
return null;
|
|
225
|
+
const api = node.name.text;
|
|
226
|
+
if (!api.endsWith(EXTERNAL_CONTRACT_SUFFIX))
|
|
227
|
+
return null;
|
|
228
|
+
return { api, owner: project, type: 'external', methods: [] };
|
|
229
|
+
}
|
|
230
|
+
/** True when the declaration carries an `export` modifier. */
|
|
231
|
+
// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts
|
|
232
|
+
function isExported(node) {
|
|
233
|
+
return (ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
234
|
+
}
|
|
235
|
+
// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here
|
|
236
|
+
function collectTsFiles(dir) {
|
|
237
|
+
const out = [];
|
|
238
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
239
|
+
const full = path.join(dir, entry.name);
|
|
240
|
+
if (entry.isDirectory()) {
|
|
241
|
+
if (entry.name !== 'node_modules')
|
|
242
|
+
out.push(...collectTsFiles(full));
|
|
243
|
+
}
|
|
244
|
+
else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
245
|
+
out.push(full);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=api-ast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-ast.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-ast.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API contract AST accessors\n *\n * The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /\n * injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project\n * programs, the source index, relation accumulation) and had grown past the file-size limit.\n *\n * Everything here is parser-level on purpose. Decorators must be read exactly as written, and a\n * plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug\n * api-scanner's source pre-pass exists to guard against.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { classDecorators, decoratorName } from '../di-graph/bindings';\nimport { ApiClassInfo, ApiMethodMeta, ApiTransport, EndpointKind } from './api-relations';\n\n/** Legal `@Endpoint(path, kind)` values; anything else is a source error, not a kind we invent. */\nconst ENDPOINT_KINDS: readonly EndpointKind[] = ['rpc', 'cloudtasks', 'cron', 'external'];\n\n/**\n * Name suffix that marks an exported type in an `externalApiPaths` project as a vendor CONTRACT\n * (`GmailApi`, `StorageApi`) rather than one of the DTOs, configs or clients sitting beside it.\n * The same convention the in-repo contracts already follow, applied where no decorator can be read.\n */\nconst EXTERNAL_CONTRACT_SUFFIX = 'Api';\n\n/**\n * Client-config class-name suffix whose FIRST constructor argument is the target service name —\n * `ClientConfig('helper-fsdb')` (rpc) and `TaskClientConfig('helper-fsdb')` (pubsub) both take\n * `svcName` first, and a consumer's own `XxxClientConfig` follows the same shape.\n */\nconst CLIENT_CONFIG_SUFFIX = 'ClientConfig';\n\n\n/** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function apiClassInfoFrom(cls: ts.ClassDeclaration, project: string): ApiClassInfo | null {\n if (!isAbstractClass(cls) || !hasClassDecorator(cls, 'ApiPath') || !cls.name) return null;\n const api = cls.name.text;\n const info: ApiClassInfo = {\n api,\n owner: project,\n type: apiTransport(cls),\n methods: endpointMethodsOf(cls, api),\n };\n const basePath = decoratorStringArg(cls, 'ApiPath');\n if (basePath !== null) info.basePath = basePath;\n return info;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function apiTransport(cls: ts.ClassDeclaration): ApiTransport {\n return hasClassDecorator(cls, 'PubSub') ? 'pubsub' : 'rpc';\n}\n\n/**\n * Every `@Endpoint(path, kind)` method on a contract class, in declaration order.\n *\n * `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the\n * source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting\n * would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely\n * the blindness the required argument exists to remove.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function endpointMethodsOf(cls: ts.ClassDeclaration, api: string): ApiMethodMeta[] {\n const methods: ApiMethodMeta[] = [];\n for (const member of cls.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n const endpoint = memberDecorator(member, 'Endpoint');\n if (endpoint === null) continue;\n const args = decoratorArgs(endpoint);\n const path = args[0] !== undefined && ts.isStringLiteral(args[0]) ? args[0].text : null;\n const kind = args[1] !== undefined && ts.isStringLiteral(args[1]) ? args[1].text : null;\n if (path === null || kind === null || !ENDPOINT_KINDS.includes(kind as EndpointKind)) continue;\n const name = member.name.text;\n const override = memberDecorator(member, 'Queue');\n const queueArg = override === null ? undefined : decoratorArgs(override)[0];\n const queueName =\n queueArg !== undefined && ts.isStringLiteral(queueArg) ? queueArg.text : `${api}-${name}`;\n methods.push({ name, path, kind: kind as EndpointKind, queueName });\n }\n return methods;\n}\n\n/** The arguments of a decorator's call expression, or [] when it is a bare `@Foo` reference. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorArgs(decorator: ts.Decorator): ts.NodeArray<ts.Expression> | ts.Expression[] {\n return ts.isCallExpression(decorator.expression) ? decorator.expression.arguments : [];\n}\n\n/** The named decorator on a class member, or null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function memberDecorator(member: ts.ClassElement, name: string): ts.Decorator | null {\n const decorators = ts.getDecorators(member as ts.HasDecorators) ?? [];\n return decorators.find((d: ts.Decorator) => decoratorName(d) === name) ?? null;\n}\n\n/** The first argument of a class decorator when it is a string literal (`@ApiPath('/x')`), else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function decoratorStringArg(cls: ts.ClassDeclaration, name: string): string | null {\n const decorator = classDecorators(cls).find((d: ts.Decorator) => decoratorName(d) === name);\n if (decorator === undefined) return null;\n const first = decoratorArgs(decorator)[0];\n return first !== undefined && ts.isStringLiteral(first) ? first.text : null;\n}\n\n/** The constructor's parameters, or [] when the class declares no constructor. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function constructorParamsOf(cls: ts.ClassDeclaration): readonly ts.ParameterDeclaration[] {\n for (const member of cls.members) {\n if (ts.isConstructorDeclaration(member)) return member.parameters;\n }\n return [];\n}\n\n/**\n * The bare name of a type reference (`GmailApi`, or `gmail.GmailApi` -> `GmailApi`), else null.\n * Generic wrappers are deliberately NOT unwrapped: `Provider<GmailApi>` hands out the contract\n * lazily, which is still a use, but it is not the shape any of these seams take today and guessing\n * at type arguments would start matching things that merely mention a contract.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function typeReferenceName(type: ts.TypeNode | undefined): string | null {\n if (type === undefined || !ts.isTypeReferenceNode(type)) return null;\n const name = type.typeName;\n if (ts.isIdentifier(name)) return name.text;\n return ts.isQualifiedName(name) && ts.isIdentifier(name.right) ? name.right.text : null;\n}\n\n/** Every type name in the class's `implements` clause — the contracts this class IS, not ones it calls. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function implementedTypeNames(cls: ts.ClassDeclaration): Set<string> {\n const names = new Set<string>();\n for (const clause of cls.heritageClauses ?? []) {\n if (clause.token !== ts.SyntaxKind.ImplementsKeyword) continue;\n for (const type of clause.types) {\n if (ts.isIdentifier(type.expression)) names.add(type.expression.text);\n }\n }\n return names;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function isAbstractClass(cls: ts.ClassDeclaration): boolean {\n return (ts.getModifiers(cls) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function hasClassDecorator(cls: ts.ClassDeclaration, name: string): boolean {\n return classDecorators(cls).some((d: ts.Decorator) => decoratorName(d) === name);\n}\n\n/**\n * The service a client-factory call aims at, from its config argument:\n * `createRpcClient(WarmupApi, new ClientConfig('helper-fsdb'))` → `'helper-fsdb'`.\n *\n * Only a `new <Xxx>ClientConfig('<string literal>')` yields a name. A variable, a template string\n * or a computed expression yields null — the target is genuinely unknown at scan time, and the\n * runtime graph must fall back to fan-out (loudly) rather than guess.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function targetServiceOf(call: ts.CallExpression): string | null {\n if (call.arguments.length < 2) return null;\n const config = call.arguments[1];\n if (!ts.isNewExpression(config) || !ts.isIdentifier(config.expression)) return null;\n if (!config.expression.text.endsWith(CLIENT_CONFIG_SUFFIX)) return null;\n const first = config.arguments?.[0];\n if (first === undefined || !ts.isStringLiteral(first)) return null;\n return first.text.length > 0 ? first.text : null;\n}\n\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function calleeMethodName(call: ts.CallExpression): string | null {\n const callee = call.expression;\n if (ts.isPropertyAccessExpression(callee)) return callee.name.text;\n if (ts.isIdentifier(callee)) return callee.text;\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- pure path predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function isTestFile(fileName: string): boolean {\n return (\n fileName.includes('/__tests__/') ||\n fileName.includes('.spec.') ||\n fileName.includes('.test.')\n );\n}\n\n\n/** {api, owner, type:'rpc'|'pubsub'} for an in-repo contract class, else null. */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function apiClassInfoFromNode(node: ts.Node, project: string): ApiClassInfo | null {\n return ts.isClassDeclaration(node) ? apiClassInfoFrom(node, project) : null;\n}\n\n/**\n * {api, owner, type:'external'} for a VENDOR contract, else null.\n *\n * A vendor contract cannot be detected the way an in-repo one is. It carries no @ApiPath (there is\n * no route — the call leaves through a vendor SDK), and it is usually a plain `interface` bound to a\n * Symbol token, which is not even a class. So inside a project the workspace has DECLARED external\n * (`runtime-architecture.externalApiPaths`) the signal is structural instead: an exported\n * `interface`/`abstract class` whose name ends in `Api`. That deliberately picks up `GmailApi` and\n * `StorageApi` while leaving their DTOs, `*Config` types and `*Client` implementations alone.\n */\n// webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts\nexport function externalApiInfoFrom(node: ts.Node, project: string): ApiClassInfo | null {\n const named = ts.isInterfaceDeclaration(node) || (ts.isClassDeclaration(node) && isAbstractClass(node));\n if (!named || !node.name || !isExported(node)) return null;\n const api = node.name.text;\n if (!api.endsWith(EXTERNAL_CONTRACT_SUFFIX)) return null;\n return { api, owner: project, type: 'external', methods: [] };\n}\n\n/** True when the declaration carries an `export` modifier. */\n// webpieces-disable no-function-outside-class -- pure AST predicate, matching the sibling helpers in di-graph/bindings.ts\nexport function isExported(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean {\n return (ts.getModifiers(node) ?? []).some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword);\n}\n\n// webpieces-disable no-function-outside-class -- recursive fs walker, matching the AST-helper style here\nexport function collectTsFiles(dir: string): string[] {\n const out: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name !== 'node_modules') out.push(...collectTsFiles(full));\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n out.push(full);\n }\n }\n return out;\n}\n"]}
|
|
@@ -13,8 +13,17 @@
|
|
|
13
13
|
* `implements`/`uses` are legal interface property names (they are reserved words
|
|
14
14
|
* only as binding identifiers, not as member names).
|
|
15
15
|
*/
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Transport of an API contract:
|
|
18
|
+
* - `rpc` — synchronous request/response over HTTP
|
|
19
|
+
* - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue
|
|
20
|
+
* - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo
|
|
21
|
+
* implements it, so it never becomes a service→service edge; it terminates the graph
|
|
22
|
+
* at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`
|
|
23
|
+
* rather than from a decorator, because a vendor contract is a plain interface bound
|
|
24
|
+
* to a Symbol token, not an `abstract class` carrying @ApiPath.
|
|
25
|
+
*/
|
|
26
|
+
export type ApiTransport = 'rpc' | 'pubsub' | 'external';
|
|
18
27
|
/**
|
|
19
28
|
* How a project relates to ONE api-lib it depends on:
|
|
20
29
|
* - `implements` — it serves the api (a controller extends it)
|
|
@@ -39,6 +48,17 @@ export interface ApiRef {
|
|
|
39
48
|
* falls back to the old fan-out and says so out loud.
|
|
40
49
|
*/
|
|
41
50
|
targetService?: string;
|
|
51
|
+
/**
|
|
52
|
+
* ONLY on a `pubsub` uses ref. True means "this producer was attributed to EVERY cloudtasks
|
|
53
|
+
* method of the contract, not to the methods it actually enqueues".
|
|
54
|
+
*
|
|
55
|
+
* A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)
|
|
56
|
+
* and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely — often after
|
|
57
|
+
* the client has been stored in a DI binding — so WHICH methods it enqueues is not statically
|
|
58
|
+
* recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording
|
|
59
|
+
* the difference keeps a producer-side queue from being read as proof that queue is used.
|
|
60
|
+
*/
|
|
61
|
+
methodsInferred?: boolean;
|
|
42
62
|
}
|
|
43
63
|
/**
|
|
44
64
|
* Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct
|
|
@@ -56,12 +76,61 @@ export interface ApiRelation {
|
|
|
56
76
|
}
|
|
57
77
|
/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */
|
|
58
78
|
export type ProjectApiRelations = Record<string, ApiRelation>;
|
|
59
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union
|
|
81
|
+
* rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency
|
|
82
|
+
* on the framework it inspects (it reads decorators as TEXT, from projects that may be on a
|
|
83
|
+
* different @webpieces version than the tooling itself).
|
|
84
|
+
*/
|
|
85
|
+
export type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';
|
|
86
|
+
/**
|
|
87
|
+
* One method on an API contract, as written in source: what triggers it, where it is mounted, and
|
|
88
|
+
* (for a queued method) which Cloud Tasks queue delivers it.
|
|
89
|
+
*/
|
|
90
|
+
export interface ApiMethodMeta {
|
|
91
|
+
name: string;
|
|
92
|
+
/** The @Endpoint path, relative to the class's @ApiPath basePath. */
|
|
93
|
+
path: string;
|
|
94
|
+
kind: EndpointKind;
|
|
95
|
+
/**
|
|
96
|
+
* `@Queue(...)` override, else `${ApiClassName}-${methodName}`. Present for every method so a
|
|
97
|
+
* `cron` schedule and a `cloudtasks` queue are both nameable; Terraform matches on this string.
|
|
98
|
+
*/
|
|
99
|
+
queueName: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A discovered API contract class: its name, the api-lib project that owns it, its transport, and
|
|
103
|
+
* its per-method trigger table.
|
|
104
|
+
*/
|
|
60
105
|
export interface ApiClassInfo {
|
|
61
106
|
api: string;
|
|
62
107
|
owner: string;
|
|
63
108
|
type: ApiTransport;
|
|
109
|
+
/** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */
|
|
110
|
+
basePath?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Every @Endpoint method, in declaration order. Empty for an external contract (a vendor
|
|
113
|
+
* interface has no endpoints — it is called through a vendor SDK, not mounted).
|
|
114
|
+
*/
|
|
115
|
+
methods: ApiMethodMeta[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The committed, per-contract view written to `architecture/dependencies.json` under `apiContracts`.
|
|
119
|
+
*
|
|
120
|
+
* The runtime graph is derived SOLELY from dependencies.json so generate and validate can never
|
|
121
|
+
* diverge — which means anything the runtime graph needs must be COMMITTED there, not re-scanned.
|
|
122
|
+
* Per-method trigger kinds and queue names are exactly that: without this table the derivation
|
|
123
|
+
* cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.
|
|
124
|
+
*/
|
|
125
|
+
export interface ApiContract {
|
|
126
|
+
owner: string;
|
|
127
|
+
/** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */
|
|
128
|
+
apiKind: ApiTransport;
|
|
129
|
+
basePath?: string;
|
|
130
|
+
methods: ApiMethodMeta[];
|
|
64
131
|
}
|
|
132
|
+
/** apiClassName -> its committed contract. Serialized as the `apiContracts` key. */
|
|
133
|
+
export type ApiContracts = Record<string, ApiContract>;
|
|
65
134
|
/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */
|
|
66
135
|
export declare function deriveApiRelationKind(implementsRefs: ApiRef[], usesRefs: ApiRef[]): ApiRelationKind;
|
|
67
136
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;
|
|
1
|
+
{"version":3,"file":"api-relations.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-relations.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAyDH,8BAEC;AA6ED,sDAIC;AAOD,kCAKC;AApGD;;;GAGG;AACH,+FAA+F;AAC/F,SAAgB,SAAS,CAAC,GAAW;IACjC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,IAAI,EAAE,EAAE,CAAC;AACnD,CAAC;AA2ED,oFAAoF;AACpF,+FAA+F;AAC/F,SAAgB,qBAAqB,CAAC,cAAwB,EAAE,QAAkB;IAC9E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,iBAAiB,CAAC;IAC/E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACnD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,+FAA+F;AAC/F,SAAgB,WAAW,CAAC,IAAc;IACtC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CACjB,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CACrB,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CACjG,CAAC;AACN,CAAC","sourcesContent":["/**\n * API Relations model\n *\n * The typed classification of a compile-time dependency edge P -> apiLib in\n * architecture/dependencies.json. Where the flat `dependsOn` only says \"P depends\n * on apiLib\", `apiRelations[apiLib]` says WHY: which API contracts P IMPLEMENTS\n * (serves, `class Ctrl extends XxxApi`) and which it USES (calls as a client,\n * `factory.createRpcClient(XxxApi, ...)` / `createPubSubClient(...)`), each tagged\n * with its transport.\n *\n * Interfaces + object literals here mirror the sibling runtime-graph.ts model —\n * these are serialization DTOs written verbatim into the committed JSON, and\n * `implements`/`uses` are legal interface property names (they are reserved words\n * only as binding identifiers, not as member names).\n */\n\n/**\n * Transport of an API contract:\n * - `rpc` — synchronous request/response over HTTP\n * - `pubsub` — fire-and-forget, delivered later through a Cloud Tasks queue\n * - `external` — a contract for a system OUTSIDE this repo (firestore, gmail, ...). Nothing in-repo\n * implements it, so it never becomes a service→service edge; it terminates the graph\n * at a dashed vendor node. Detected from `runtime-architecture.externalApiPaths`\n * rather than from a decorator, because a vendor contract is a plain interface bound\n * to a Symbol token, not an `abstract class` carrying @ApiPath.\n */\nexport type ApiTransport = 'rpc' | 'pubsub' | 'external';\n\n/**\n * How a project relates to ONE api-lib it depends on:\n * - `implements` — it serves the api (a controller extends it)\n * - `uses` — it calls the api (generates a client)\n * - `uses-implements` — it does BOTH (implements some of the api-lib's contracts,\n * uses others)\n */\nexport type ApiRelationKind = 'implements' | 'uses' | 'uses-implements';\n\n/** One API class a project implements or uses, with its transport. */\nexport interface ApiRef {\n api: string;\n type: ApiTransport;\n /**\n * ONLY on a `uses` ref: the service the call site aims at, read from the client config literal\n * (`createRpcClient(XxxApi, new ClientConfig('helper-fsdb'))` → `helper-fsdb`). It is matched\n * against a project's DECLARED `serviceName` to pick the ONE runtime edge target, instead of\n * fanning the edge out to every implementer of the api — which is catastrophically wrong for a\n * company-wide contract registered in a shared library and therefore implemented by every server.\n *\n * Absent when the config argument is not a `new <Xxx>ClientConfig('<literal>')` (a variable, a\n * computed name, ...). Absent means \"unknown target\", NOT \"no target\" — the runtime graph then\n * falls back to the old fan-out and says so out loud.\n */\n targetService?: string;\n /**\n * ONLY on a `pubsub` uses ref. True means \"this producer was attributed to EVERY cloudtasks\n * method of the contract, not to the methods it actually enqueues\".\n *\n * A producer builds one client for the whole contract (`createPubSubClient(EmailTaskApi, cfg)`)\n * and enqueues through a proxy (`emailTasks.send(req)`) somewhere else entirely — often after\n * the client has been stored in a DI binding — so WHICH methods it enqueues is not statically\n * recoverable. The consumer side IS exact (addRoutes + the contract's method table). Recording\n * the difference keeps a producer-side queue from being read as proof that queue is used.\n */\n methodsInferred?: boolean;\n}\n\n/**\n * Identity of a ref for de-duplication: an api used twice against DIFFERENT services is two distinct\n * relations (two distinct runtime edges), so the api name alone is not the key.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function apiRefKey(ref: ApiRef): string {\n return `${ref.api} ${ref.targetService ?? ''}`;\n}\n\n/**\n * A project's relationship to ONE api-lib it depends on. Serialized verbatim into\n * architecture/dependencies.json under `apiRelations[apiLibProjectName]`.\n */\nexport interface ApiRelation {\n kind: ApiRelationKind;\n implements: ApiRef[];\n uses: ApiRef[];\n}\n\n/** apiLibProjectName -> relation. Attached to a GraphEntry as `apiRelations`. */\nexport type ProjectApiRelations = Record<string, ApiRelation>;\n\n/**\n * What triggers ONE endpoint, mirroring core-util's `EndpointKind`. Duplicated as a string union\n * rather than imported: nx-webpieces-rules is build tooling and must not take a runtime dependency\n * on the framework it inspects (it reads decorators as TEXT, from projects that may be on a\n * different @webpieces version than the tooling itself).\n */\nexport type EndpointKind = 'rpc' | 'cloudtasks' | 'cron' | 'external';\n\n/**\n * One method on an API contract, as written in source: what triggers it, where it is mounted, and\n * (for a queued method) which Cloud Tasks queue delivers it.\n */\nexport interface ApiMethodMeta {\n name: string;\n /** The @Endpoint path, relative to the class's @ApiPath basePath. */\n path: string;\n kind: EndpointKind;\n /**\n * `@Queue(...)` override, else `${ApiClassName}-${methodName}`. Present for every method so a\n * `cron` schedule and a `cloudtasks` queue are both nameable; Terraform matches on this string.\n */\n queueName: string;\n}\n\n/**\n * A discovered API contract class: its name, the api-lib project that owns it, its transport, and\n * its per-method trigger table.\n */\nexport interface ApiClassInfo {\n api: string;\n owner: string;\n type: ApiTransport;\n /** The class's @ApiPath basePath; absent for an external (vendor) contract, which has no route. */\n basePath?: string;\n /**\n * Every @Endpoint method, in declaration order. Empty for an external contract (a vendor\n * interface has no endpoints — it is called through a vendor SDK, not mounted).\n */\n methods: ApiMethodMeta[];\n}\n\n/**\n * The committed, per-contract view written to `architecture/dependencies.json` under `apiContracts`.\n *\n * The runtime graph is derived SOLELY from dependencies.json so generate and validate can never\n * diverge — which means anything the runtime graph needs must be COMMITTED there, not re-scanned.\n * Per-method trigger kinds and queue names are exactly that: without this table the derivation\n * cannot tell a queued endpoint from a cron sweep, and cannot name the queue between two services.\n */\nexport interface ApiContract {\n owner: string;\n /** 'rpc' | 'pubsub' for an in-repo contract, 'external' for a vendor seam. */\n apiKind: ApiTransport;\n basePath?: string;\n methods: ApiMethodMeta[];\n}\n\n/** apiClassName -> its committed contract. Serialized as the `apiContracts` key. */\nexport type ApiContracts = Record<string, ApiContract>;\n\n/** Derive the relation kind from the (possibly empty) implements/uses ref lists. */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function deriveApiRelationKind(implementsRefs: ApiRef[], usesRefs: ApiRef[]): ApiRelationKind {\n if (implementsRefs.length > 0 && usesRefs.length > 0) return 'uses-implements';\n if (implementsRefs.length > 0) return 'implements';\n return 'uses';\n}\n\n/**\n * Stable-sort a ref list by api name, then by target service, so the committed JSON is\n * deterministic even when one api is used against two different services.\n */\n// webpieces-disable no-function-outside-class -- pure data helper for these serialization DTOs\nexport function sortApiRefs(refs: ApiRef[]): ApiRef[] {\n return [...refs].sort(\n (a: ApiRef, b: ApiRef) =>\n a.api.localeCompare(b.api) || (a.targetService ?? '').localeCompare(b.targetService ?? ''),\n );\n}\n"]}
|