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
1916import { promises as fs } from 'node:fs'
2017import path from 'node:path'
2118import { fileURLToPath } from 'node:url'
19+ import loggerPkg from '@socketsecurity/lib/logger'
20+
21+ const logger = loggerPkg . getDefaultLogger ( )
2222
2323const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) )
2424const rootPath = path . join ( __dirname , '..' )
2525
26- // CDN patterns to detect
26+ // CDN domains to block
2727const CDN_PATTERNS = [
28- {
29- pattern : / b u n d l e \. r u n / gi,
30- name : 'bundle.run' ,
31- } ,
32- {
33- pattern : / c d n j s \. c l o u d f l a r e \. c o m / gi,
34- name : 'cdnjs' ,
35- } ,
36- {
37- pattern : / d e n o p k g \. c o m / gi,
38- name : 'denopkg' ,
39- } ,
40- {
41- pattern : / e s m \. r u n / gi,
42- name : 'esm.run' ,
43- } ,
44- {
45- pattern : / e s m \. s h / gi,
46- name : 'esm.sh' ,
47- } ,
48- {
49- pattern : / c d n \. j s d e l i v r \. n e t | j s d e l i v r \. n e t | f a s t l y \. j s d e l i v r \. n e t / gi,
50- name : 'jsDelivr' ,
51- } ,
52- {
53- pattern : / g a \. j s p m \. i o | j s p m \. d e v / gi,
54- name : 'JSPM' ,
55- } ,
56- {
57- pattern : / j s r \. i o / gi,
58- name : 'JSR' ,
59- } ,
60- {
61- pattern : / c d n \. p i k a \. d e v | c d n \. s n o w p a c k \. d e v / gi,
62- name : 'Pika/Snowpack CDN' ,
63- } ,
64- {
65- pattern : / s k y p a c k \. d e v | c d n \. s k y p a c k \. d e v / gi,
66- name : 'Skypack' ,
67- } ,
68- {
69- pattern : / u n p k g \. c o m / gi,
70- name : 'unpkg' ,
71- } ,
28+ / u n p k g \. c o m / i,
29+ / c d n \. j s d e l i v r \. n e t / i,
30+ / e s m \. s h / i,
31+ / c d n \. s k y p a c k \. d e v / i,
32+ / g a \. j s p m \. i o / 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 */
184157async 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
242211main ( ) . catch ( error => {
243- console . error ( 'Validation failed:' , error )
212+ logger . fail ( `Unexpected error: ${ error . message } ` )
244213 process . exitCode = 1
245214} )
0 commit comments