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