Skip to content

Commit e5d6040

Browse files
committed
feat(scripts): implement CDN reference validation
Implement actual validation logic for validate-no-cdn-refs.mjs as a preventative check to ensure no hardcoded CDN URLs are introduced. The project deliberately avoids CDN dependencies for security and reliability reasons. This validator scans all text files and fails if any CDN domain references are detected. Blocked CDN domains: - unpkg.com - cdn.jsdelivr.net - esm.sh - cdn.skypack.dev - ga.jspm.io
1 parent c7dbc69 commit e5d6040

File tree

1 file changed

+207
-5
lines changed

1 file changed

+207
-5
lines changed

scripts/validate-no-cdn-refs.mjs

Lines changed: 207 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,214 @@
1+
#!/usr/bin/env node
12
/**
23
* @fileoverview Validates that there are no CDN references in the codebase.
3-
* TODO: Implement actual validation logic.
4+
*
5+
* This is a preventative check to ensure no hardcoded CDN URLs are introduced.
6+
* The project deliberately avoids CDN dependencies for security and reliability.
7+
*
8+
* Blocked CDN domains:
9+
* - unpkg.com
10+
* - cdn.jsdelivr.net
11+
* - esm.sh
12+
* - cdn.skypack.dev
13+
* - ga.jspm.io
414
*/
515

6-
import { getDefaultLogger } from '@socketsecurity/lib/logger'
16+
import { promises as fs } from 'node:fs'
17+
import path from 'node:path'
18+
import { fileURLToPath } from 'node:url'
19+
import loggerPkg from '@socketsecurity/lib/logger'
720

8-
const logger = getDefaultLogger()
21+
const logger = loggerPkg.getDefaultLogger()
922

10-
logger.success('No CDN references found')
23+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
24+
const rootPath = path.join(__dirname, '..')
1125

12-
process.exit(0)
26+
// CDN domains to block
27+
const CDN_PATTERNS = [
28+
/unpkg\.com/i,
29+
/cdn\.jsdelivr\.net/i,
30+
/esm\.sh/i,
31+
/cdn\.skypack\.dev/i,
32+
/ga\.jspm\.io/i,
33+
]
34+
35+
// Directories to skip
36+
const SKIP_DIRS = new Set([
37+
'node_modules',
38+
'.git',
39+
'dist',
40+
'build',
41+
'.cache',
42+
'coverage',
43+
'.next',
44+
'.nuxt',
45+
'.output',
46+
'.turbo',
47+
'.type-coverage',
48+
'.yarn',
49+
])
50+
51+
// File extensions to check
52+
const TEXT_EXTENSIONS = new Set([
53+
'.js',
54+
'.mjs',
55+
'.cjs',
56+
'.ts',
57+
'.mts',
58+
'.cts',
59+
'.jsx',
60+
'.tsx',
61+
'.json',
62+
'.md',
63+
'.html',
64+
'.htm',
65+
'.css',
66+
'.yml',
67+
'.yaml',
68+
'.xml',
69+
'.svg',
70+
'.txt',
71+
'.sh',
72+
'.bash',
73+
])
74+
75+
/**
76+
* Check if file should be scanned.
77+
*/
78+
function shouldScanFile(filename) {
79+
const ext = path.extname(filename).toLowerCase()
80+
return TEXT_EXTENSIONS.has(ext)
81+
}
82+
83+
/**
84+
* Recursively find all text files to scan.
85+
*/
86+
async function findTextFiles(dir, files = []) {
87+
try {
88+
const entries = await fs.readdir(dir, { withFileTypes: true })
89+
90+
for (const entry of entries) {
91+
const fullPath = path.join(dir, entry.name)
92+
93+
if (entry.isDirectory()) {
94+
// Skip certain directories and hidden directories (except .github)
95+
if (
96+
!SKIP_DIRS.has(entry.name) &&
97+
(!entry.name.startsWith('.') || entry.name === '.github')
98+
) {
99+
await findTextFiles(fullPath, files)
100+
}
101+
} else if (entry.isFile() && shouldScanFile(entry.name)) {
102+
files.push(fullPath)
103+
}
104+
}
105+
} catch {
106+
// Skip directories we can't read
107+
}
108+
109+
return files
110+
}
111+
112+
/**
113+
* Check file contents for CDN references.
114+
*/
115+
async function checkFileForCdnRefs(filePath) {
116+
// Skip this validator script itself (it mentions CDN domains by necessity)
117+
if (filePath.endsWith('validate-no-cdn-refs.mjs')) {
118+
return []
119+
}
120+
121+
try {
122+
const content = await fs.readFile(filePath, 'utf8')
123+
const lines = content.split('\n')
124+
const violations = []
125+
126+
for (let i = 0; i < lines.length; i++) {
127+
const line = lines[i]
128+
const lineNumber = i + 1
129+
130+
for (const pattern of CDN_PATTERNS) {
131+
if (pattern.test(line)) {
132+
const match = line.match(pattern)
133+
violations.push({
134+
file: path.relative(rootPath, filePath),
135+
line: lineNumber,
136+
content: line.trim(),
137+
cdnDomain: match[0],
138+
})
139+
}
140+
}
141+
}
142+
143+
return violations
144+
} catch (error) {
145+
// Skip files we can't read (likely binary despite extension)
146+
if (error.code === 'EISDIR' || error.message.includes('ENOENT')) {
147+
return []
148+
}
149+
// For other errors, try to continue
150+
return []
151+
}
152+
}
153+
154+
/**
155+
* Validate all files for CDN references.
156+
*/
157+
async function validateNoCdnRefs() {
158+
const files = await findTextFiles(rootPath)
159+
const allViolations = []
160+
161+
for (const file of files) {
162+
const violations = await checkFileForCdnRefs(file)
163+
allViolations.push(...violations)
164+
}
165+
166+
return allViolations
167+
}
168+
169+
async function main() {
170+
try {
171+
const violations = await validateNoCdnRefs()
172+
173+
if (violations.length === 0) {
174+
logger.success('No CDN references found')
175+
process.exitCode = 0
176+
return
177+
}
178+
179+
logger.fail(`Found ${violations.length} CDN reference(s)`)
180+
logger.log('')
181+
logger.log('CDN URLs are not allowed in this codebase for security and')
182+
logger.log('reliability reasons. Please use npm packages instead.')
183+
logger.log('')
184+
logger.log('Blocked CDN domains:')
185+
logger.log(' - unpkg.com')
186+
logger.log(' - cdn.jsdelivr.net')
187+
logger.log(' - esm.sh')
188+
logger.log(' - cdn.skypack.dev')
189+
logger.log(' - ga.jspm.io')
190+
logger.log('')
191+
logger.log('Violations:')
192+
logger.log('')
193+
194+
for (const violation of violations) {
195+
logger.log(` ${violation.file}:${violation.line}`)
196+
logger.log(` Domain: ${violation.cdnDomain}`)
197+
logger.log(` Content: ${violation.content}`)
198+
logger.log('')
199+
}
200+
201+
logger.log('Remove CDN references and use npm dependencies instead.')
202+
logger.log('')
203+
204+
process.exitCode = 1
205+
} catch (error) {
206+
logger.fail(`Validation failed: ${error.message}`)
207+
process.exitCode = 1
208+
}
209+
}
210+
211+
main().catch(error => {
212+
logger.fail(`Unexpected error: ${error.message}`)
213+
process.exitCode = 1
214+
})

0 commit comments

Comments
 (0)