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
1915import { promises as fs } from 'node:fs'
2016import path from 'node:path'
2117import { fileURLToPath } from 'node:url'
18+ import loggerPkg from '@socketsecurity/lib/logger'
19+
20+ const logger = loggerPkg . getDefaultLogger ( )
2221
2322const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) )
2423const rootPath = path . join ( __dirname , '..' )
2524
26- // CDN patterns to detect
25+ // CDN domains to block
2726const 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- } ,
27+ / u n p k g \. c o m / i,
28+ / c d n \. j s d e l i v r \. n e t / i,
29+ / e s m \. s h / i,
30+ / c d n \. s k y p a c k \. d e v / i,
31+ / g a \. j s p m \. i o / 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 */
186156async 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
244210main ( ) . catch ( error => {
245- console . error ( 'Validation failed:' , error )
211+ logger . fail ( `Unexpected error: ${ error . message } ` )
246212 process . exitCode = 1
247213} )
0 commit comments