Skip to content

Commit 815aa13

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 e93bc77 commit 815aa13

File tree

1 file changed

+102
-133
lines changed

1 file changed

+102
-133
lines changed

scripts/validate-no-cdn-refs.mjs

Lines changed: 102 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,35 @@
1+
#!/usr/bin/env node
12
/**
2-
* @fileoverview Validates that no files contain CDN references.
3-
* CDN usage is prohibited - use npm packages and bundle instead.
3+
* @fileoverview Validates that there are no CDN references in the codebase.
44
*
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
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:
169
* - unpkg.com
10+
* - cdn.jsdelivr.net
11+
* - esm.sh
12+
* - cdn.skypack.dev
13+
* - ga.jspm.io
1714
*/
1815

1916
import { promises as fs } from 'node:fs'
2017
import path from 'node:path'
2118
import { fileURLToPath } from 'node:url'
19+
import loggerPkg from '@socketsecurity/lib/logger'
20+
21+
const logger = loggerPkg.getDefaultLogger()
2222

2323
const __dirname = path.dirname(fileURLToPath(import.meta.url))
2424
const rootPath = path.join(__dirname, '..')
2525

26-
// CDN patterns to detect
26+
// CDN domains to block
2727
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-
},
28+
/unpkg\.com/i,
29+
/cdn\.jsdelivr\.net/i,
30+
/esm\.sh/i,
31+
/cdn\.skypack\.dev/i,
32+
/ga\.jspm\.io/i,
7233
]
7334

7435
// Directories to skip
@@ -82,48 +43,63 @@ const SKIP_DIRS = new Set([
8243
'.next',
8344
'.nuxt',
8445
'.output',
46+
'.turbo',
47+
'.type-coverage',
48+
'.yarn',
8549
])
8650

8751
// File extensions to check
88-
const CHECK_EXTENSIONS = new Set([
52+
const TEXT_EXTENSIONS = new Set([
8953
'.js',
9054
'.mjs',
9155
'.cjs',
9256
'.ts',
9357
'.mts',
9458
'.cts',
95-
'.tsx',
9659
'.jsx',
60+
'.tsx',
9761
'.json',
9862
'.md',
9963
'.html',
10064
'.htm',
10165
'.css',
102-
'.scss',
103-
'.yaml',
10466
'.yml',
105-
'.toml',
67+
'.yaml',
68+
'.xml',
69+
'.svg',
70+
'.txt',
71+
'.sh',
72+
'.bash',
10673
])
10774

10875
/**
109-
* Recursively find all files to check.
76+
* Check if file should be scanned.
11077
*/
111-
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 = []) {
11287
try {
11388
const entries = await fs.readdir(dir, { withFileTypes: true })
11489

11590
for (const entry of entries) {
11691
const fullPath = path.join(dir, entry.name)
11792

11893
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)
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)
126100
}
101+
} else if (entry.isFile() && shouldScanFile(entry.name)) {
102+
files.push(fullPath)
127103
}
128104
}
129105
} catch {
@@ -134,59 +110,56 @@ async function findFiles(dir, files = []) {
134110
}
135111

136112
/**
137-
* Check a file for CDN references.
113+
* Check file contents for CDN references.
138114
*/
139-
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+
140121
try {
141122
const content = await fs.readFile(filePath, 'utf8')
123+
const lines = content.split('\n')
142124
const violations = []
143125

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
155-
while ((match = pattern.exec(content)) !== 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-
})
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+
}
171140
}
172141
}
173142

174143
return violations
175-
} catch {
176-
// 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
177150
return []
178151
}
179152
}
180153

181154
/**
182-
* Validate no CDN references exist.
155+
* Validate all files for CDN references.
183156
*/
184157
async function validateNoCdnRefs() {
185-
const files = await findFiles(rootPath)
158+
const files = await findTextFiles(rootPath)
186159
const allViolations = []
187160

188161
for (const file of files) {
189-
const violations = await checkFile(file)
162+
const violations = await checkFileForCdnRefs(file)
190163
allViolations.push(...violations)
191164
}
192165

@@ -198,48 +171,44 @@ async function main() {
198171
const violations = await validateNoCdnRefs()
199172

200173
if (violations.length === 0) {
201-
console.log('✓ No CDN references found')
174+
logger.success('No CDN references found')
202175
process.exitCode = 0
203176
return
204177
}
205178

206-
console.error('❌ CDN references found (prohibited)\n')
207-
console.error(
208-
'Public CDNs (cdnjs, unpkg, jsDelivr, esm.sh, JSR, etc.) are not allowed.\n',
209-
)
210-
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('')
211193

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

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

235204
process.exitCode = 1
236205
} catch (error) {
237-
console.error('Validation failed:', error.message)
206+
logger.fail(`Validation failed: ${error.message}`)
238207
process.exitCode = 1
239208
}
240209
}
241210

242211
main().catch(error => {
243-
console.error('Validation failed:', error)
212+
logger.fail(`Unexpected error: ${error.message}`)
244213
process.exitCode = 1
245214
})

0 commit comments

Comments
 (0)