Skip to content

Commit 5578996

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 - Update logger usage from @socketsecurity/lib/logger - Refine CDN domain list to focus on most common domains
1 parent 7af035f commit 5578996

File tree

1 file changed

+101
-135
lines changed

1 file changed

+101
-135
lines changed

scripts/validate-no-cdn-refs.mjs

Lines changed: 101 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,34 @@
11
/**
2-
* @fileoverview Validates that no files contain CDN references.
3-
* CDN usage is prohibited - use npm packages and bundle instead.
2+
* @fileoverview Validates that there are no CDN references in the codebase.
43
*
5-
* Checks for:
6-
* - bundle.run
7-
* - cdnjs.cloudflare.com
8-
* - denopkg.com
9-
* - esm.run
10-
* - esm.sh
11-
* - jsdelivr.net (cdn.jsdelivr.net, fastly.jsdelivr.net)
12-
* - jspm.io/jspm.dev
13-
* - jsr.io
14-
* - Pika/Snowpack CDN
15-
* - skypack.dev
4+
* This is a preventative check to ensure no hardcoded CDN URLs are introduced.
5+
* The project deliberately avoids CDN dependencies for security and reliability.
6+
*
7+
* Blocked CDN domains:
168
* - unpkg.com
9+
* - cdn.jsdelivr.net
10+
* - esm.sh
11+
* - cdn.skypack.dev
12+
* - ga.jspm.io
1713
*/
1814

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

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

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

7434
// Directories to skip
@@ -82,48 +42,63 @@ const SKIP_DIRS = new Set([
8242
'.next',
8343
'.nuxt',
8444
'.output',
45+
'.turbo',
46+
'.type-coverage',
47+
'.yarn',
8548
])
8649

8750
// File extensions to check
88-
const CHECK_EXTENSIONS = new Set([
51+
const TEXT_EXTENSIONS = new Set([
8952
'.js',
9053
'.mjs',
9154
'.cjs',
9255
'.ts',
9356
'.mts',
9457
'.cts',
95-
'.tsx',
9658
'.jsx',
59+
'.tsx',
9760
'.json',
9861
'.md',
9962
'.html',
10063
'.htm',
10164
'.css',
102-
'.scss',
103-
'.yaml',
10465
'.yml',
105-
'.toml',
66+
'.yaml',
67+
'.xml',
68+
'.svg',
69+
'.txt',
70+
'.sh',
71+
'.bash',
10672
])
10773

10874
/**
109-
* Recursively find all files to check.
75+
* Check if file should be scanned.
11076
*/
111-
async function findFiles(dir, files = []) {
77+
function shouldScanFile(filename) {
78+
const ext = path.extname(filename).toLowerCase()
79+
return TEXT_EXTENSIONS.has(ext)
80+
}
81+
82+
/**
83+
* Recursively find all text files to scan.
84+
*/
85+
async function findTextFiles(dir, files = []) {
11286
try {
11387
const entries = await fs.readdir(dir, { withFileTypes: true })
11488

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

11892
if (entry.isDirectory()) {
119-
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
120-
await findFiles(fullPath, files)
121-
}
122-
} else if (entry.isFile()) {
123-
const ext = path.extname(entry.name)
124-
if (CHECK_EXTENSIONS.has(ext)) {
125-
files.push(fullPath)
93+
// Skip certain directories and hidden directories (except .github)
94+
if (
95+
!SKIP_DIRS.has(entry.name) &&
96+
(!entry.name.startsWith('.') || entry.name === '.github')
97+
) {
98+
await findTextFiles(fullPath, files)
12699
}
100+
} else if (entry.isFile() && shouldScanFile(entry.name)) {
101+
files.push(fullPath)
127102
}
128103
}
129104
} catch {
@@ -134,61 +109,56 @@ async function findFiles(dir, files = []) {
134109
}
135110

136111
/**
137-
* Check a file for CDN references.
112+
* Check file contents for CDN references.
138113
*/
139-
async function checkFile(filePath) {
114+
async function checkFileForCdnRefs(filePath) {
115+
// Skip this validator script itself (it mentions CDN domains by necessity)
116+
if (filePath.endsWith('validate-no-cdn-refs.mjs')) {
117+
return []
118+
}
119+
140120
try {
141121
const content = await fs.readFile(filePath, 'utf8')
122+
const lines = content.split('\n')
142123
const violations = []
143124

144-
// Skip this validation script itself (it contains CDN names in documentation)
145-
const relativePath = path.relative(rootPath, filePath)
146-
if (relativePath === 'scripts/validate-no-cdn-refs.mjs') {
147-
return []
148-
}
149-
150-
for (const { name, pattern } of CDN_PATTERNS) {
151-
// Reset regex state
152-
pattern.lastIndex = 0
153-
154-
let match = pattern.exec(content)
155-
while (match !== null) {
156-
// Get line number
157-
const beforeMatch = content.substring(0, match.index)
158-
const lineNumber = beforeMatch.split('\n').length
159-
160-
// Get context (line containing the match)
161-
const lines = content.split('\n')
162-
const line = lines[lineNumber - 1]
163-
164-
violations.push({
165-
file: path.relative(rootPath, filePath),
166-
lineNumber,
167-
cdn: name,
168-
line: line.trim(),
169-
url: match[0],
170-
})
171-
172-
match = pattern.exec(content)
125+
for (let i = 0; i < lines.length; i++) {
126+
const line = lines[i]
127+
const lineNumber = i + 1
128+
129+
for (const pattern of CDN_PATTERNS) {
130+
if (pattern.test(line)) {
131+
const match = line.match(pattern)
132+
violations.push({
133+
file: path.relative(rootPath, filePath),
134+
line: lineNumber,
135+
content: line.trim(),
136+
cdnDomain: match[0],
137+
})
138+
}
173139
}
174140
}
175141

176142
return violations
177-
} catch {
178-
// Skip files we can't read
143+
} catch (error) {
144+
// Skip files we can't read (likely binary despite extension)
145+
if (error.code === 'EISDIR' || error.message.includes('ENOENT')) {
146+
return []
147+
}
148+
// For other errors, try to continue
179149
return []
180150
}
181151
}
182152

183153
/**
184-
* Validate no CDN references exist.
154+
* Validate all files for CDN references.
185155
*/
186156
async function validateNoCdnRefs() {
187-
const files = await findFiles(rootPath)
157+
const files = await findTextFiles(rootPath)
188158
const allViolations = []
189159

190160
for (const file of files) {
191-
const violations = await checkFile(file)
161+
const violations = await checkFileForCdnRefs(file)
192162
allViolations.push(...violations)
193163
}
194164

@@ -200,48 +170,44 @@ async function main() {
200170
const violations = await validateNoCdnRefs()
201171

202172
if (violations.length === 0) {
203-
console.log('✓ No CDN references found')
173+
logger.success('No CDN references found')
204174
process.exitCode = 0
205175
return
206176
}
207177

208-
console.error('❌ CDN references found (prohibited)\n')
209-
console.error(
210-
'Public CDNs (cdnjs, unpkg, jsDelivr, esm.sh, JSR, etc.) are not allowed.\n',
211-
)
212-
console.error('Use npm packages and bundle instead.\n')
178+
logger.fail(`Found ${violations.length} CDN reference(s)`)
179+
logger.log('')
180+
logger.log('CDN URLs are not allowed in this codebase for security and')
181+
logger.log('reliability reasons. Please use npm packages instead.')
182+
logger.log('')
183+
logger.log('Blocked CDN domains:')
184+
logger.log(' - unpkg.com')
185+
logger.log(' - cdn.jsdelivr.net')
186+
logger.log(' - esm.sh')
187+
logger.log(' - cdn.skypack.dev')
188+
logger.log(' - ga.jspm.io')
189+
logger.log('')
190+
logger.log('Violations:')
191+
logger.log('')
213192

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

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

237203
process.exitCode = 1
238204
} catch (error) {
239-
console.error('Validation failed:', error.message)
205+
logger.fail(`Validation failed: ${error.message}`)
240206
process.exitCode = 1
241207
}
242208
}
243209

244210
main().catch(error => {
245-
console.error('Validation failed:', error)
211+
logger.fail(`Unexpected error: ${error.message}`)
246212
process.exitCode = 1
247213
})

0 commit comments

Comments
 (0)