Skip to content

Commit ae4a7ee

Browse files
committed
chore(scripts): align CDN validator with socket-cli implementation
Synchronize validate-no-cdn-refs.mjs with the latest version from socket-cli for consistency across Socket repositories. Changes: - Simplify CDN pattern matching to use array of regexes - Add shebang line for direct execution - Update logger usage from @socketsecurity/lib/logger - Refine CDN domain list to focus on most common domains
1 parent a9ed86b commit ae4a7ee

File tree

1 file changed

+101
-133
lines changed

1 file changed

+101
-133
lines changed

scripts/validate-no-cdn-refs.mjs

Lines changed: 101 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,35 @@
11
#!/usr/bin/env node
22
/**
3-
* @fileoverview Validates that no files contain CDN references.
4-
* CDN usage is prohibited - use npm packages and bundle instead.
3+
* @fileoverview Validates that there are no CDN references in the codebase.
54
*
6-
* Checks for:
7-
* - bundle.run
8-
* - cdnjs.cloudflare.com
9-
* - denopkg.com
10-
* - esm.run
11-
* - esm.sh
12-
* - jsdelivr.net (cdn.jsdelivr.net, fastly.jsdelivr.net)
13-
* - jspm.io/jspm.dev
14-
* - jsr.io
15-
* - Pika/Snowpack CDN
16-
* - skypack.dev
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:
179
* - unpkg.com
10+
* - cdn.jsdelivr.net
11+
* - esm.sh
12+
* - cdn.skypack.dev
13+
* - ga.jspm.io
1814
*/
1915

2016
import { promises as fs } from 'node:fs'
2117
import path from 'node:path'
2218
import { fileURLToPath } from 'node:url'
19+
import loggerPkg from '@socketsecurity/lib/logger'
20+
21+
const logger = loggerPkg.getDefaultLogger()
2322

2423
const __dirname = path.dirname(fileURLToPath(import.meta.url))
2524
const rootPath = path.join(__dirname, '..')
2625

27-
// CDN patterns to detect
26+
// CDN domains to block
2827
const CDN_PATTERNS = [
29-
{
30-
pattern: /bundle\.run/gi,
31-
name: 'bundle.run',
32-
},
33-
{
34-
pattern: /cdnjs\.cloudflare\.com/gi,
35-
name: 'cdnjs',
36-
},
37-
{
38-
pattern: /denopkg\.com/gi,
39-
name: 'denopkg',
40-
},
41-
{
42-
pattern: /esm\.run/gi,
43-
name: 'esm.run',
44-
},
45-
{
46-
pattern: /esm\.sh/gi,
47-
name: 'esm.sh',
48-
},
49-
{
50-
pattern: /cdn\.jsdelivr\.net|jsdelivr\.net|fastly\.jsdelivr\.net/gi,
51-
name: 'jsDelivr',
52-
},
53-
{
54-
pattern: /ga\.jspm\.io|jspm\.dev/gi,
55-
name: 'JSPM',
56-
},
57-
{
58-
pattern: /jsr\.io/gi,
59-
name: 'JSR',
60-
},
61-
{
62-
pattern: /cdn\.pika\.dev|cdn\.snowpack\.dev/gi,
63-
name: 'Pika/Snowpack CDN',
64-
},
65-
{
66-
pattern: /skypack\.dev|cdn\.skypack\.dev/gi,
67-
name: 'Skypack',
68-
},
69-
{
70-
pattern: /unpkg\.com/gi,
71-
name: 'unpkg',
72-
},
28+
/unpkg\.com/i,
29+
/cdn\.jsdelivr\.net/i,
30+
/esm\.sh/i,
31+
/cdn\.skypack\.dev/i,
32+
/ga\.jspm\.io/i,
7333
]
7434

7535
// Directories to skip
@@ -83,48 +43,63 @@ const SKIP_DIRS = new Set([
8343
'.next',
8444
'.nuxt',
8545
'.output',
46+
'.turbo',
47+
'.type-coverage',
48+
'.yarn',
8649
])
8750

8851
// File extensions to check
89-
const CHECK_EXTENSIONS = new Set([
52+
const TEXT_EXTENSIONS = new Set([
9053
'.js',
9154
'.mjs',
9255
'.cjs',
9356
'.ts',
9457
'.mts',
9558
'.cts',
96-
'.tsx',
9759
'.jsx',
60+
'.tsx',
9861
'.json',
9962
'.md',
10063
'.html',
10164
'.htm',
10265
'.css',
103-
'.scss',
104-
'.yaml',
10566
'.yml',
106-
'.toml',
67+
'.yaml',
68+
'.xml',
69+
'.svg',
70+
'.txt',
71+
'.sh',
72+
'.bash',
10773
])
10874

10975
/**
110-
* Recursively find all files to check.
76+
* Check if file should be scanned.
11177
*/
112-
async function findFiles(dir, files = []) {
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 = []) {
11387
try {
11488
const entries = await fs.readdir(dir, { withFileTypes: true })
11589

11690
for (const entry of entries) {
11791
const fullPath = path.join(dir, entry.name)
11892

11993
if (entry.isDirectory()) {
120-
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
121-
await findFiles(fullPath, files)
122-
}
123-
} else if (entry.isFile()) {
124-
const ext = path.extname(entry.name)
125-
if (CHECK_EXTENSIONS.has(ext)) {
126-
files.push(fullPath)
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)
127100
}
101+
} else if (entry.isFile() && shouldScanFile(entry.name)) {
102+
files.push(fullPath)
128103
}
129104
}
130105
} catch {
@@ -135,59 +110,56 @@ async function findFiles(dir, files = []) {
135110
}
136111

137112
/**
138-
* Check a file for CDN references.
113+
* Check file contents for CDN references.
139114
*/
140-
async function checkFile(filePath) {
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+
141121
try {
142122
const content = await fs.readFile(filePath, 'utf8')
123+
const lines = content.split('\n')
143124
const violations = []
144125

145-
// Skip this validation script itself (it contains CDN names in documentation)
146-
const relativePath = path.relative(rootPath, filePath)
147-
if (relativePath === 'scripts/validate-no-cdn-refs.mjs') {
148-
return []
149-
}
150-
151-
for (const { name, pattern } of CDN_PATTERNS) {
152-
// Reset regex state
153-
pattern.lastIndex = 0
154-
155-
let match
156-
while ((match = pattern.exec(content)) !== null) {
157-
// Get line number
158-
const beforeMatch = content.substring(0, match.index)
159-
const lineNumber = beforeMatch.split('\n').length
160-
161-
// Get context (line containing the match)
162-
const lines = content.split('\n')
163-
const line = lines[lineNumber - 1]
164-
165-
violations.push({
166-
file: path.relative(rootPath, filePath),
167-
lineNumber,
168-
cdn: name,
169-
line: line.trim(),
170-
url: match[0],
171-
})
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+
}
172140
}
173141
}
174142

175143
return violations
176-
} catch {
177-
// Skip files we can't read
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
178150
return []
179151
}
180152
}
181153

182154
/**
183-
* Validate no CDN references exist.
155+
* Validate all files for CDN references.
184156
*/
185157
async function validateNoCdnRefs() {
186-
const files = await findFiles(rootPath)
158+
const files = await findTextFiles(rootPath)
187159
const allViolations = []
188160

189161
for (const file of files) {
190-
const violations = await checkFile(file)
162+
const violations = await checkFileForCdnRefs(file)
191163
allViolations.push(...violations)
192164
}
193165

@@ -199,48 +171,44 @@ async function main() {
199171
const violations = await validateNoCdnRefs()
200172

201173
if (violations.length === 0) {
202-
console.log('✓ No CDN references found')
174+
logger.success('No CDN references found')
203175
process.exitCode = 0
204176
return
205177
}
206178

207-
console.error('❌ CDN references found (prohibited)\n')
208-
console.error(
209-
'Public CDNs (cdnjs, unpkg, jsDelivr, esm.sh, JSR, etc.) are not allowed.\n',
210-
)
211-
console.error('Use npm packages and bundle instead.\n')
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('')
212193

213-
// Group by file
214-
const byFile = new Map()
215194
for (const violation of violations) {
216-
if (!byFile.has(violation.file)) {
217-
byFile.set(violation.file, [])
218-
}
219-
byFile.get(violation.file).push(violation)
220-
}
221-
222-
for (const [file, fileViolations] of byFile) {
223-
console.error(` ${file}`)
224-
for (const violation of fileViolations) {
225-
console.error(` Line ${violation.lineNumber}: ${violation.cdn}`)
226-
console.error(` ${violation.line}`)
227-
}
228-
console.error('')
195+
logger.log(` ${violation.file}:${violation.line}`)
196+
logger.log(` Domain: ${violation.cdnDomain}`)
197+
logger.log(` Content: ${violation.content}`)
198+
logger.log('')
229199
}
230200

231-
console.error('Replace CDN usage with:')
232-
console.error(' - npm install <package>')
233-
console.error(' - Import and bundle with your build tool')
234-
console.error('')
201+
logger.log('Remove CDN references and use npm dependencies instead.')
202+
logger.log('')
235203

236204
process.exitCode = 1
237205
} catch (error) {
238-
console.error('Validation failed:', error.message)
206+
logger.fail(`Validation failed: ${error.message}`)
239207
process.exitCode = 1
240208
}
241209
}
242210

243211
main().catch(error => {
244-
console.error('Validation failed:', error)
212+
logger.fail(`Unexpected error: ${error.message}`)
245213
process.exitCode = 1
246214
})

0 commit comments

Comments
 (0)