Skip to content

Commit 67967c7

Browse files
committed
chore(build): add CDN references validation check
Add validation script to ensure no CDN references in codebase. Checks for: cdnjs, unpkg, jsDelivr, esm.sh, JSR, Skypack, JSPM, bundle.run, esm.run, denopkg, Pika/Snowpack CDN.
1 parent 8c68e83 commit 67967c7

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

scripts/check.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,27 @@ async function main() {
263263
}
264264
}
265265

266+
// Run CDN references validation check
267+
if (runAll) {
268+
if (!quiet) {
269+
logger.progress('Validating no CDN references')
270+
}
271+
exitCode = await runCommandQuiet('node', [
272+
'scripts/validate-no-cdn-refs.mjs',
273+
]).then(r => r.exitCode)
274+
if (exitCode !== 0) {
275+
if (!quiet) {
276+
logger.error('CDN references validation failed')
277+
}
278+
process.exitCode = exitCode
279+
return
280+
}
281+
if (!quiet) {
282+
logger.clearLine().done('No CDN references found')
283+
logger.error('')
284+
}
285+
}
286+
266287
if (!quiet) {
267288
logger.success('All checks passed')
268289
printFooter()

scripts/validate-no-cdn-refs.mjs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/usr/bin/env node
2+
/**
3+
* @fileoverview Validates that no files contain CDN references.
4+
* CDN usage is prohibited - use npm packages and bundle instead.
5+
*
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
17+
* - unpkg.com
18+
*/
19+
20+
import { promises as fs } from 'node:fs'
21+
import path from 'node:path'
22+
import { fileURLToPath } from 'node:url'
23+
24+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
25+
const rootPath = path.join(__dirname, '..')
26+
27+
// CDN patterns to detect
28+
const CDN_PATTERNS = [
29+
{
30+
pattern: /bundle\.run/gi,
31+
name: 'bundle.run',
32+
},
33+
{
34+
pattern: /cdnjs\.cloudflare\.com/gi,
35+
name: 'cdnjs',
36+
},
37+
{
38+
pattern: /denopkg\.com/gi,
39+
name: 'denopkg',
40+
},
41+
{
42+
pattern: /esm\.run/gi,
43+
name: 'esm.run',
44+
},
45+
{
46+
pattern: /esm\.sh/gi,
47+
name: 'esm.sh',
48+
},
49+
{
50+
pattern: /cdn\.jsdelivr\.net|jsdelivr\.net|fastly\.jsdelivr\.net/gi,
51+
name: 'jsDelivr',
52+
},
53+
{
54+
pattern: /ga\.jspm\.io|jspm\.dev/gi,
55+
name: 'JSPM',
56+
},
57+
{
58+
pattern: /jsr\.io/gi,
59+
name: 'JSR',
60+
},
61+
{
62+
pattern: /cdn\.pika\.dev|cdn\.snowpack\.dev/gi,
63+
name: 'Pika/Snowpack CDN',
64+
},
65+
{
66+
pattern: /skypack\.dev|cdn\.skypack\.dev/gi,
67+
name: 'Skypack',
68+
},
69+
{
70+
pattern: /unpkg\.com/gi,
71+
name: 'unpkg',
72+
},
73+
]
74+
75+
// Directories to skip
76+
const SKIP_DIRS = new Set([
77+
'node_modules',
78+
'.git',
79+
'dist',
80+
'build',
81+
'.cache',
82+
'coverage',
83+
'.next',
84+
'.nuxt',
85+
'.output',
86+
])
87+
88+
// File extensions to check
89+
const CHECK_EXTENSIONS = new Set([
90+
'.js',
91+
'.mjs',
92+
'.cjs',
93+
'.ts',
94+
'.mts',
95+
'.cts',
96+
'.tsx',
97+
'.jsx',
98+
'.json',
99+
'.md',
100+
'.html',
101+
'.htm',
102+
'.css',
103+
'.scss',
104+
'.yaml',
105+
'.yml',
106+
'.toml',
107+
])
108+
109+
/**
110+
* Recursively find all files to check.
111+
*/
112+
async function findFiles(dir, files = []) {
113+
try {
114+
const entries = await fs.readdir(dir, { withFileTypes: true })
115+
116+
for (const entry of entries) {
117+
const fullPath = path.join(dir, entry.name)
118+
119+
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)
127+
}
128+
}
129+
}
130+
} catch (error) {
131+
// Skip directories we can't read
132+
}
133+
134+
return files
135+
}
136+
137+
/**
138+
* Check a file for CDN references.
139+
*/
140+
async function checkFile(filePath) {
141+
try {
142+
const content = await fs.readFile(filePath, 'utf8')
143+
const violations = []
144+
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 { pattern, name } 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+
})
172+
}
173+
}
174+
175+
return violations
176+
} catch (error) {
177+
// Skip files we can't read
178+
return []
179+
}
180+
}
181+
182+
/**
183+
* Validate no CDN references exist.
184+
*/
185+
async function validateNoCdnRefs() {
186+
const files = await findFiles(rootPath)
187+
const allViolations = []
188+
189+
for (const file of files) {
190+
const violations = await checkFile(file)
191+
allViolations.push(...violations)
192+
}
193+
194+
return allViolations
195+
}
196+
197+
async function main() {
198+
try {
199+
const violations = await validateNoCdnRefs()
200+
201+
if (violations.length === 0) {
202+
console.log('✓ No CDN references found')
203+
process.exitCode = 0
204+
return
205+
}
206+
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')
212+
213+
// Group by file
214+
const byFile = new Map()
215+
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('')
229+
}
230+
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('')
235+
236+
process.exitCode = 1
237+
} catch (error) {
238+
console.error('Validation failed:', error.message)
239+
process.exitCode = 1
240+
}
241+
}
242+
243+
main().catch(error => {
244+
console.error('Validation failed:', error)
245+
process.exitCode = 1
246+
})

0 commit comments

Comments
 (0)