Skip to content

Commit e41779d

Browse files
committed
feat(validation): add guard against link: dependencies
- Add validate-no-link-deps.mjs script to detect and prevent link: protocol usage - Integrate validation into check script to run automatically - Link dependencies are prohibited: use workspace: for monorepo or catalog: for centralized versions This ensures consistent dependency management and prevents the use of link: which can cause issues with published packages.
1 parent 72e7c0f commit e41779d

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed

scripts/check.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,27 @@ async function main() {
200200
}
201201
}
202202

203+
// Run link: validation check
204+
if (runAll) {
205+
if (!quiet) {
206+
logger.progress('Validating no link: dependencies')
207+
}
208+
exitCode = await runCommandQuiet('node', [
209+
'scripts/validate-no-link-deps.mjs',
210+
]).then(r => r.exitCode)
211+
if (exitCode !== 0) {
212+
if (!quiet) {
213+
logger.error('Validation failed')
214+
}
215+
process.exitCode = exitCode
216+
return
217+
}
218+
if (!quiet) {
219+
logger.clearLine().done('No link: dependencies found')
220+
logger.error('')
221+
}
222+
}
223+
203224
if (!quiet) {
204225
logger.success('All checks passed')
205226
printFooter()

scripts/validate-no-link-deps.mjs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env node
2+
/**
3+
* @fileoverview Validates that no package.json files contain link: dependencies.
4+
* Link dependencies are prohibited - use workspace: or catalog: instead.
5+
*/
6+
7+
import { promises as fs } from 'node:fs'
8+
import path from 'node:path'
9+
import { fileURLToPath } from 'node:url'
10+
11+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
12+
const rootPath = path.join(__dirname, '..')
13+
14+
/**
15+
* Find all package.json files in the repository.
16+
*/
17+
async function findPackageJsonFiles(dir) {
18+
const files = []
19+
const entries = await fs.readdir(dir, { withFileTypes: true })
20+
21+
for (const entry of entries) {
22+
const fullPath = path.join(dir, entry.name)
23+
24+
// Skip node_modules, .git, and build directories.
25+
if (
26+
entry.name === 'node_modules' ||
27+
entry.name === '.git' ||
28+
entry.name === 'build' ||
29+
entry.name === 'dist'
30+
) {
31+
continue
32+
}
33+
34+
if (entry.isDirectory()) {
35+
files.push(...(await findPackageJsonFiles(fullPath)))
36+
} else if (entry.name === 'package.json') {
37+
files.push(fullPath)
38+
}
39+
}
40+
41+
return files
42+
}
43+
44+
/**
45+
* Check if a package.json contains link: dependencies.
46+
*/
47+
async function checkPackageJson(filePath) {
48+
const content = await fs.readFile(filePath, 'utf8')
49+
const pkg = JSON.parse(content)
50+
51+
const violations = []
52+
53+
// Check dependencies.
54+
if (pkg.dependencies) {
55+
for (const [name, version] of Object.entries(pkg.dependencies)) {
56+
if (typeof version === 'string' && version.startsWith('link:')) {
57+
violations.push({
58+
file: filePath,
59+
field: 'dependencies',
60+
package: name,
61+
value: version,
62+
})
63+
}
64+
}
65+
}
66+
67+
// Check devDependencies.
68+
if (pkg.devDependencies) {
69+
for (const [name, version] of Object.entries(pkg.devDependencies)) {
70+
if (typeof version === 'string' && version.startsWith('link:')) {
71+
violations.push({
72+
file: filePath,
73+
field: 'devDependencies',
74+
package: name,
75+
value: version,
76+
})
77+
}
78+
}
79+
}
80+
81+
// Check peerDependencies.
82+
if (pkg.peerDependencies) {
83+
for (const [name, version] of Object.entries(pkg.peerDependencies)) {
84+
if (typeof version === 'string' && version.startsWith('link:')) {
85+
violations.push({
86+
file: filePath,
87+
field: 'peerDependencies',
88+
package: name,
89+
value: version,
90+
})
91+
}
92+
}
93+
}
94+
95+
// Check optionalDependencies.
96+
if (pkg.optionalDependencies) {
97+
for (const [name, version] of Object.entries(pkg.optionalDependencies)) {
98+
if (typeof version === 'string' && version.startsWith('link:')) {
99+
violations.push({
100+
file: filePath,
101+
field: 'optionalDependencies',
102+
package: name,
103+
value: version,
104+
})
105+
}
106+
}
107+
}
108+
109+
return violations
110+
}
111+
112+
async function main() {
113+
const packageJsonFiles = await findPackageJsonFiles(rootPath)
114+
const allViolations = []
115+
116+
for (const file of packageJsonFiles) {
117+
const violations = await checkPackageJson(file)
118+
allViolations.push(...violations)
119+
}
120+
121+
if (allViolations.length > 0) {
122+
console.error('❌ Found link: dependencies (prohibited)')
123+
console.error('')
124+
console.error(
125+
'Use workspace: protocol for monorepo packages or catalog: for centralized versions.',
126+
)
127+
console.error('')
128+
129+
for (const violation of allViolations) {
130+
const relativePath = path.relative(rootPath, violation.file)
131+
console.error(` ${relativePath}`)
132+
console.error(
133+
` ${violation.field}.${violation.package}: "${violation.value}"`,
134+
)
135+
}
136+
137+
console.error('')
138+
console.error('Replace link: with:')
139+
console.error(' - workspace: for monorepo packages')
140+
console.error(' - catalog: for centralized version management')
141+
console.error('')
142+
143+
process.exitCode = 1
144+
} else {
145+
console.log('✓ No link: dependencies found')
146+
}
147+
}
148+
149+
main().catch(error => {
150+
console.error('Validation failed:', error)
151+
process.exitCode = 1
152+
})

0 commit comments

Comments
 (0)