verify-wrapper-api.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import { constants } from 'node:fs'
  2. import { access, readFile, readdir } from 'node:fs/promises'
  3. import { dirname, extname, join, relative, resolve } from 'node:path'
  4. import { fileURLToPath } from 'node:url'
  5. import { loadEcosystemReleaseContext } from './lib/ecosystem-packages.mjs'
  6. const scriptDir = dirname(fileURLToPath(import.meta.url))
  7. const sourceRoot = resolve(scriptDir, '..')
  8. const { wrapperManifest } = await loadEcosystemReleaseContext(sourceRoot)
  9. const sourceFileExtensions = new Set(['.ts', '.tsx', '.vue', '.js', '.mjs', '.svelte'])
  10. const standardWrapperPackageNames = new Set(wrapperManifest.wrappers.map(wrapper => wrapper.packageName))
  11. const forbiddenLegacyTokens = [
  12. 'buildViewerSrc',
  13. 'createViewerFrame',
  14. 'createViewerFrameControllerHandle',
  15. 'createViewerFrameFilePostController',
  16. 'getIframe',
  17. 'isViewerFrameEvent',
  18. 'mountViewerFrame',
  19. 'postFile',
  20. 'postFileToViewer',
  21. 'syncViewerFrame',
  22. 'targetOrigin',
  23. 'toViewerFrameOptions',
  24. 'viewerUrl',
  25. 'ViewerFrameComponentBridgeOptions',
  26. 'ViewerFrameComponentProps',
  27. 'ViewerFrameContainerComponentProps',
  28. 'ViewerFrameControllerAccessor',
  29. 'ViewerFrameHostComponentProps',
  30. 'ViewerFrameIframeComponentProps',
  31. 'ViewerFrameOptions'
  32. ]
  33. const sharedOptionTypeExports = [
  34. 'FileRef',
  35. 'ViewerAiOptions',
  36. 'ViewerArchiveOptions',
  37. 'ViewerCadOptions',
  38. 'ViewerController',
  39. 'ViewerControllerAccessor',
  40. 'ViewerControllerHandle',
  41. 'ViewerDocxOptions',
  42. 'ViewerEvent',
  43. 'ViewerEventHandler',
  44. 'ViewerEventType',
  45. 'ViewerFetchFile',
  46. 'ViewerFetchInput',
  47. 'ViewerMountOptions',
  48. 'ViewerOptions',
  49. 'ViewerPdfOptions',
  50. 'ViewerCoreOptions',
  51. 'ViewerSearchOptions',
  52. 'ViewerSourceInput',
  53. 'ViewerThemeMode',
  54. 'ViewerToolbarOptions',
  55. 'ViewerToolbarPosition',
  56. 'ViewerTypstOptions',
  57. 'ViewerWatermarkOptions'
  58. ]
  59. const webControllerMethods = [
  60. 'load',
  61. 'update',
  62. 'reload',
  63. 'destroy',
  64. 'getApi',
  65. 'downloadOriginalFile',
  66. 'printRenderedHtml',
  67. 'exportRenderedHtml',
  68. 'zoomIn',
  69. 'zoomOut',
  70. 'resetZoom',
  71. 'searchDocument',
  72. 'clearDocumentSearch',
  73. 'nextSearchResult',
  74. 'previousSearchResult',
  75. 'collectDocumentAnchors',
  76. 'scrollToAnchor',
  77. 'scrollToLine',
  78. 'getDocumentTextChunks',
  79. 'getOperationAvailability'
  80. ]
  81. function assert(condition, message) {
  82. if (!condition) {
  83. throw new Error(message)
  84. }
  85. }
  86. async function exists(path) {
  87. try {
  88. await access(path, constants.F_OK)
  89. return true
  90. } catch {
  91. return false
  92. }
  93. }
  94. async function readWrapperSource(wrapper) {
  95. const candidates = [
  96. join(sourceRoot, wrapper.packageDir, 'src', 'index.ts'),
  97. join(sourceRoot, wrapper.packageDir, 'src', 'index.tsx'),
  98. join(sourceRoot, wrapper.packageDir, 'src', 'package', 'index.ts')
  99. ]
  100. for (const candidate of candidates) {
  101. if (await exists(candidate)) {
  102. return {
  103. path: candidate,
  104. content: await readFile(candidate, 'utf8')
  105. }
  106. }
  107. }
  108. throw new Error(`${wrapper.packageName} must provide src/index.ts or src/index.tsx`)
  109. }
  110. async function readAllSourceFiles(dir) {
  111. const entries = await readdir(dir, { withFileTypes: true })
  112. const files = []
  113. for (const entry of entries) {
  114. const path = join(dir, entry.name)
  115. if (entry.isDirectory()) {
  116. if (['dist', 'node_modules', 'viewer'].includes(entry.name)) {
  117. continue
  118. }
  119. files.push(...await readAllSourceFiles(path))
  120. continue
  121. }
  122. if (entry.isFile() && sourceFileExtensions.has(extname(entry.name))) {
  123. files.push(path)
  124. }
  125. }
  126. return files
  127. }
  128. function escapeRegExp(value) {
  129. return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  130. }
  131. function hasToken(source, token) {
  132. return new RegExp(`\\b${escapeRegExp(token)}\\b`).test(source)
  133. }
  134. function assertTokens(source, tokens, label) {
  135. for (const token of tokens) {
  136. assert(hasToken(source, token), `${label} must include ${token}`)
  137. }
  138. }
  139. function assertNoLegacyIframeApi(source, label) {
  140. for (const token of forbiddenLegacyTokens) {
  141. assert(!hasToken(source, token), `${label} must not expose legacy standalone-page API token ${token}`)
  142. }
  143. }
  144. function assertImportsFrom(source, packageName, label) {
  145. assert(
  146. source.includes(`from '${packageName}'`) || source.includes(`from "${packageName}"`),
  147. `${label} must import from ${packageName}`
  148. )
  149. }
  150. function assertReferencesPackage(source, packageName, label) {
  151. assert(
  152. source.includes(`'${packageName}'`) || source.includes(`"${packageName}"`),
  153. `${label} must reference ${packageName}`
  154. )
  155. }
  156. function assertImportsFromAny(source, packageNames, label) {
  157. assert(
  158. packageNames.some(packageName =>
  159. source.includes(`from '${packageName}'`) || source.includes(`from "${packageName}"`)
  160. ),
  161. `${label} must import from ${packageNames.join(' or ')}`
  162. )
  163. }
  164. function assertNotImportsFrom(source, packageName, label) {
  165. assert(
  166. !source.includes(`from '${packageName}'`) && !source.includes(`from "${packageName}"`),
  167. `${label} must not import from ${packageName}`
  168. )
  169. }
  170. function importsFromPackage(source, packageName) {
  171. return source.includes(`from '${packageName}'`) ||
  172. source.includes(`from "${packageName}"`) ||
  173. source.includes(`import('${packageName}')`) ||
  174. source.includes(`import("${packageName}")`)
  175. }
  176. async function assertNoWrapperToWrapperImports(wrapper) {
  177. const sourceDir = join(sourceRoot, wrapper.packageDir, 'src')
  178. if (!(await exists(sourceDir))) {
  179. return
  180. }
  181. const forbiddenPackages = [...standardWrapperPackageNames].filter(packageName => packageName !== wrapper.packageName)
  182. for (const file of await readAllSourceFiles(sourceDir)) {
  183. const source = await readFile(file, 'utf8')
  184. const label = `${wrapper.packageName} ${relative(sourceDir, file)}`
  185. for (const packageName of forbiddenPackages) {
  186. assert(
  187. !importsFromPackage(source, packageName),
  188. `${label} must not import another standard component package ${packageName}`
  189. )
  190. }
  191. }
  192. }
  193. function assertReexportsLocalControllerTypes(source, label) {
  194. const reExportPattern = /export\s+type\s*{[\s\S]*}\s+from\s+['"]\.\/controller(?:\.js)?['"]/
  195. assert(reExportPattern.test(source), `${label} must re-export its local component controller types`)
  196. assertTokens(source, sharedOptionTypeExports, label)
  197. }
  198. function assertConsumesCore(source, label) {
  199. assertImportsFromAny(source, ['./controller', './controller.js'], label)
  200. assertImportsFrom(source, '@file-viewer/core', label)
  201. assertNotImportsFrom(source, '@file-viewer/web', label)
  202. assertTokens(source, [
  203. 'mountViewer',
  204. 'fileViewerCoreRendererRegistry',
  205. 'ViewerMountOptions',
  206. 'ViewerController'
  207. ], label)
  208. }
  209. function verifyWebWrapper(source, label) {
  210. assertConsumesCore(source, label)
  211. assertTokens(source, [
  212. 'ViewerCoreOptions',
  213. 'FlyfishFileViewerWeb',
  214. 'FileViewerElement',
  215. 'defineFileViewerElement',
  216. 'FILE_VIEWER_ELEMENT_TAG'
  217. ], label)
  218. assertReexportsLocalControllerTypes(source, label)
  219. assertNoLegacyIframeApi(source, label)
  220. }
  221. function verifyVue3Wrapper(source, label) {
  222. assertImportsFrom(source, 'vue', label)
  223. assertImportsFromAny(source, ['./controller', './controller.js'], label)
  224. assertNotImportsFrom(source, '@file-viewer/web', label)
  225. assertTokens(source, [
  226. 'FileViewer',
  227. 'createFlyfishFileViewer',
  228. 'mountFlyfishFileViewer',
  229. 'FileViewerVue3PluginOptions',
  230. 'FileViewerVue3Handle',
  231. 'ViewerMountOptions'
  232. ], label)
  233. assertReexportsLocalControllerTypes(source, label)
  234. assertNoLegacyIframeApi(source, label)
  235. }
  236. function verifyVue2Wrapper(source, label) {
  237. assertImportsFrom(source, 'vue', label)
  238. assertConsumesCore(source, label)
  239. assertReexportsLocalControllerTypes(source, label)
  240. assertTokens(source, [
  241. 'FileViewer',
  242. 'install',
  243. 'FileViewerPlugin',
  244. 'componentName',
  245. 'viewer-event',
  246. 'viewerEvent',
  247. 'createViewerControllerHandle',
  248. 'mountViewer',
  249. 'getApi',
  250. 'load',
  251. 'update',
  252. 'reload',
  253. 'destroy'
  254. ], label)
  255. assertNoLegacyIframeApi(source, label)
  256. }
  257. function verifyReactWrapper(source, label) {
  258. assertImportsFrom(source, 'react', label)
  259. assertConsumesCore(source, label)
  260. assertReexportsLocalControllerTypes(source, label)
  261. assertTokens(source, [
  262. 'FileViewerHandle',
  263. 'FileViewerProps',
  264. 'forwardRef',
  265. 'useImperativeHandle',
  266. 'createViewerControllerHandle',
  267. 'mountViewer',
  268. 'ViewerMountOptions',
  269. 'ViewerController'
  270. ], label)
  271. assertNoLegacyIframeApi(source, label)
  272. assert(/export\s+default\s+FileViewer/.test(source), `${label} must default-export FileViewer`)
  273. }
  274. function verifyReactLegacyWrapper(source, label) {
  275. assertImportsFrom(source, 'react', label)
  276. assertConsumesCore(source, label)
  277. assertReexportsLocalControllerTypes(source, label)
  278. assertTokens(source, [
  279. 'FileViewerLegacyHandle',
  280. 'FileViewerLegacyProps',
  281. 'forwardRef',
  282. 'useImperativeHandle',
  283. 'createViewerControllerHandle',
  284. 'mountViewer',
  285. 'ViewerMountOptions',
  286. 'ViewerController'
  287. ], label)
  288. assertNoLegacyIframeApi(source, label)
  289. assert(/export\s+default\s+FileViewerLegacy/.test(source), `${label} must default-export FileViewerLegacy`)
  290. }
  291. function verifyJQueryWrapper(source, label) {
  292. assertConsumesCore(source, label)
  293. assertReexportsLocalControllerTypes(source, label)
  294. assertTokens(source, [
  295. 'JQueryFileViewerMethod',
  296. 'JQueryFileViewerOptions',
  297. 'installJQueryFileViewer',
  298. 'getFileViewerController',
  299. 'destroyFileViewer',
  300. 'fileViewer',
  301. 'mountViewer',
  302. 'load',
  303. 'update',
  304. 'reload',
  305. 'destroy'
  306. ], label)
  307. assertNoLegacyIframeApi(source, label)
  308. assert(/export\s+default\s+installJQueryFileViewer/.test(source), `${label} must default-export installJQueryFileViewer`)
  309. }
  310. function verifySvelteWrapper(source, label) {
  311. assertConsumesCore(source, label)
  312. assertReexportsLocalControllerTypes(source, label)
  313. assertTokens(source, [
  314. 'FileViewerSvelteActionOptions',
  315. 'FileViewerSvelteActionReturn',
  316. 'fileViewer',
  317. 'mountViewer',
  318. 'update',
  319. 'destroy',
  320. 'replace'
  321. ], label)
  322. assertNoLegacyIframeApi(source, label)
  323. assert(/export\s+default\s+fileViewer/.test(source), `${label} must default-export fileViewer action`)
  324. }
  325. function verifyFullWrapper(wrapper, source) {
  326. const label = wrapper.packageName
  327. assert(wrapper.basePackage, `${label} full package must declare basePackage`)
  328. assertReferencesPackage(source, '@file-viewer/preset-all', label)
  329. assertReferencesPackage(source, wrapper.basePackage, label)
  330. assertNoLegacyIframeApi(source, label)
  331. assert(
  332. /export\s+\*\s+from\s+['"][^'"]+['"]/.test(source) ||
  333. /export\s+\{\s*default\s*\}\s+from\s+['"][^'"]+['"]/.test(source),
  334. `${label} full package must re-export the standard component API`
  335. )
  336. }
  337. const verifiers = {
  338. vue3: source => verifyVue3Wrapper(source, '@file-viewer/vue3'),
  339. 'vue2.7': source => verifyVue2Wrapper(source, '@file-viewer/vue2.7'),
  340. 'vue2.6': source => verifyVue2Wrapper(source, '@file-viewer/vue2.6'),
  341. react: source => verifyReactWrapper(source, '@file-viewer/react'),
  342. 'react-legacy': source => verifyReactLegacyWrapper(source, '@file-viewer/react-legacy'),
  343. web: source => verifyWebWrapper(source, '@file-viewer/web'),
  344. jquery: source => verifyJQueryWrapper(source, '@file-viewer/jquery'),
  345. svelte: source => verifySvelteWrapper(source, '@file-viewer/svelte')
  346. }
  347. let checked = 0
  348. for (const wrapper of wrapperManifest.wrappers) {
  349. const { content } = await readWrapperSource(wrapper)
  350. if (wrapper.flavor === 'full') {
  351. verifyFullWrapper(wrapper, content)
  352. checked += 1
  353. continue
  354. }
  355. const verify = verifiers[wrapper.id]
  356. assert(verify, `Missing wrapper API verifier for ${wrapper.id}`)
  357. await assertNoWrapperToWrapperImports(wrapper)
  358. verify(content)
  359. checked += 1
  360. }
  361. console.log(`Verified ${checked} standard component package native core API surfaces.`)