verify-offline-runtime-assets.mjs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { existsSync } from 'node:fs'
  2. import { readdir, readFile } from 'node:fs/promises'
  3. import { extname, relative, resolve } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. const root = resolve(fileURLToPath(new URL('..', import.meta.url)))
  6. const sourceRoots = [
  7. 'packages/core/src',
  8. 'packages/components/vue3/src',
  9. 'packages/components/vue2.7/src',
  10. 'packages/components/vue2.6/src',
  11. 'packages/components/react/src',
  12. 'packages/components/react-legacy/src',
  13. 'packages/components/web/src',
  14. 'packages/components/jquery/src',
  15. 'packages/components/svelte/src',
  16. 'packages/compat/vue3/src',
  17. 'packages/compat/vue2/src',
  18. 'packages/compat/web/src',
  19. 'apps/viewer-demo/src',
  20. 'apps/component-demo/src',
  21. ].map(path => resolve(root, path))
  22. const allowedExtensions = new Set([
  23. '.css',
  24. '.html',
  25. '.js',
  26. '.jsx',
  27. '.mjs',
  28. '.svelte',
  29. '.ts',
  30. '.tsx',
  31. '.vue',
  32. ])
  33. const ignoredDirectories = new Set([
  34. '.git',
  35. '.turbo',
  36. 'coverage',
  37. 'dist',
  38. 'node_modules',
  39. 'viewer',
  40. ])
  41. const urlPattern = /https?:\/\/[^\s'"`<>)]+/gi
  42. const deniedHostFragments = [
  43. 'cdn.jsdelivr.net',
  44. 'cdnjs.cloudflare.com',
  45. 'esm.sh',
  46. 'googleapis.com',
  47. 'gstatic.com',
  48. 'npm.onmicrosoft.cn',
  49. 'unpkg.com',
  50. 'viewer.diagrams.net',
  51. ]
  52. const allowedUrlPrefixes = [
  53. 'http://localhost/',
  54. 'http://127.0.0.1',
  55. 'http://www.w3.org/',
  56. 'https://www.w3.org/',
  57. 'http://schemas.openxmlformats.org/',
  58. 'http://schemas.microsoft.com/',
  59. 'http://purl.oclc.org/',
  60. 'http://www.idpf.org/',
  61. 'http://www.daisy.org/',
  62. 'http://www.apache.org/licenses/LICENSE-2.0',
  63. 'https://demo.file-viewer.app',
  64. ]
  65. const findFiles = async dir => {
  66. const entries = await readdir(dir, { withFileTypes: true })
  67. const files = []
  68. for (const entry of entries) {
  69. if (ignoredDirectories.has(entry.name)) {
  70. continue
  71. }
  72. const path = resolve(dir, entry.name)
  73. if (entry.isDirectory()) {
  74. files.push(...await findFiles(path))
  75. continue
  76. }
  77. if (entry.isFile() && allowedExtensions.has(extname(entry.name))) {
  78. files.push(path)
  79. }
  80. }
  81. return files
  82. }
  83. const isAllowedUrl = url => {
  84. if (allowedUrlPrefixes.some(prefix => url.startsWith(prefix))) {
  85. return true
  86. }
  87. try {
  88. const parsed = new URL(url)
  89. return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'
  90. } catch {
  91. return false
  92. }
  93. }
  94. const isDeniedUrl = url => {
  95. const normalized = url.toLowerCase()
  96. return deniedHostFragments.some(fragment => normalized.includes(fragment))
  97. }
  98. const violations = []
  99. let scannedFiles = 0
  100. for (const sourceRoot of sourceRoots) {
  101. if (!existsSync(sourceRoot)) {
  102. continue
  103. }
  104. const files = await findFiles(sourceRoot)
  105. for (const file of files) {
  106. scannedFiles += 1
  107. const content = await readFile(file, 'utf8')
  108. const lines = content.split('\n')
  109. lines.forEach((line, lineIndex) => {
  110. const urls = line.match(urlPattern) || []
  111. urls.forEach(url => {
  112. const cleanUrl = url.replace(/[.,;]+$/, '')
  113. if (isAllowedUrl(cleanUrl) && !isDeniedUrl(cleanUrl)) {
  114. return
  115. }
  116. violations.push({
  117. file: relative(root, file),
  118. line: lineIndex + 1,
  119. url: cleanUrl,
  120. })
  121. })
  122. })
  123. }
  124. }
  125. if (violations.length) {
  126. console.error('[file-viewer] Offline runtime asset verification failed.')
  127. for (const violation of violations) {
  128. console.error(`- ${violation.file}:${violation.line} ${violation.url}`)
  129. }
  130. console.error('\nRuntime preview code must not depend on public CDN or third-party online assets.')
  131. process.exit(1)
  132. }
  133. console.log(`[file-viewer] Offline runtime asset verification passed (${scannedFiles} source files scanned).`)