verify-compatibility-api.mjs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { existsSync } from 'node:fs'
  2. import { readdir, readFile } 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 { rootPackage, entries } = await loadEcosystemReleaseContext(sourceRoot)
  9. const expectedWorkspaceRange = `workspace:^${rootPackage.version}`
  10. const entryByName = new Map(entries.map(entry => [entry.packageName, entry]))
  11. const sourceFileExtensions = new Set(['.ts', '.tsx', '.vue', '.js', '.mjs', '.svelte'])
  12. const forbiddenLegacyTokens = [
  13. 'buildViewerSrc',
  14. 'buildFileViewerFrameSrc',
  15. 'createFileViewerFrameControllerHandle',
  16. 'createViewerFrame',
  17. 'createViewerFrameControllerHandle',
  18. 'createViewerFrameFilePostController',
  19. 'createViewerFrame',
  20. 'getIframe',
  21. 'isViewerFrameEvent',
  22. 'mountFileViewerFrame',
  23. 'mountViewerFrame',
  24. 'postFileToViewer',
  25. 'postFileViewerLifecycleEvent',
  26. 'postFileViewerLocationChange',
  27. 'postFileViewerMessageToParent',
  28. 'postFileViewerOperationAvailabilityChange',
  29. 'postFileViewerOperationContextEvent',
  30. 'postFileViewerSearchChange',
  31. 'postFileViewerZoomChange',
  32. 'syncViewerFrame',
  33. 'targetOrigin',
  34. 'toFileViewerFrameOptions',
  35. 'toViewerFrameOptions',
  36. 'viewerUrl',
  37. 'FileViewerFrameComponentProps',
  38. 'FileViewerFrameIframeComponentProps',
  39. 'ViewerFrameComponentProps',
  40. 'ViewerFrameIframeComponentProps',
  41. 'ViewerFrameOptions'
  42. ]
  43. function assert(condition, message) {
  44. if (!condition) {
  45. throw new Error(message)
  46. }
  47. }
  48. function installDependencies(packageJson) {
  49. return {
  50. ...packageJson.dependencies,
  51. ...packageJson.optionalDependencies
  52. }
  53. }
  54. function assertTokens(source, tokens, label) {
  55. for (const token of tokens) {
  56. assert(source.includes(token), `${label} must include ${token}`)
  57. }
  58. }
  59. function assertImportsFrom(source, packageName, label) {
  60. assert(
  61. source.includes(`from '${packageName}'`) || source.includes(`from "${packageName}"`),
  62. `${label} must import from ${packageName}`
  63. )
  64. }
  65. function assertNotImportsFrom(source, packageName, label) {
  66. assert(
  67. !source.includes(`from '${packageName}'`) && !source.includes(`from "${packageName}"`),
  68. `${label} must not import from ${packageName}`
  69. )
  70. }
  71. async function readAllSourceFiles(dir) {
  72. const entries = await readdir(dir, { withFileTypes: true })
  73. const files = []
  74. for (const entry of entries) {
  75. const path = join(dir, entry.name)
  76. if (entry.isDirectory()) {
  77. if (['dist', 'node_modules', 'viewer'].includes(entry.name)) {
  78. continue
  79. }
  80. files.push(...await readAllSourceFiles(path))
  81. continue
  82. }
  83. if (entry.isFile() && sourceFileExtensions.has(extname(entry.name))) {
  84. files.push(path)
  85. }
  86. }
  87. return files
  88. }
  89. async function readSource(entry, relativePath) {
  90. return readFile(join(entry.absoluteDir, relativePath), 'utf8')
  91. }
  92. function requireEntry(packageName) {
  93. const entry = entryByName.get(packageName)
  94. assert(entry, `Missing compatibility package ${packageName}`)
  95. return entry
  96. }
  97. function assertNoLegacyIframeTokens(source, label) {
  98. for (const token of forbiddenLegacyTokens) {
  99. assert(!source.includes(token), `${label} must not contain legacy standalone-page API token ${token}`)
  100. }
  101. }
  102. async function assertNoLegacyIframeTokensInDir(entry, relativeDir) {
  103. const absoluteDir = join(entry.absoluteDir, relativeDir)
  104. if (!existsSync(absoluteDir)) {
  105. return
  106. }
  107. const sourceFiles = await readAllSourceFiles(absoluteDir)
  108. for (const file of sourceFiles) {
  109. const source = await readFile(file, 'utf8')
  110. const label = `${entry.packageName} ${relative(entry.absoluteDir, file)}`
  111. assertNoLegacyIframeTokens(source, label)
  112. }
  113. }
  114. async function verifyWebCompatibility() {
  115. const entry = requireEntry('@flyfish-group/file-viewer-web')
  116. const dependencies = installDependencies(entry.packageJson)
  117. assert(
  118. dependencies['@file-viewer/web'] === expectedWorkspaceRange,
  119. `${entry.packageName} must depend on @file-viewer/web@${expectedWorkspaceRange}`
  120. )
  121. assert(
  122. !dependencies['@file-viewer/core'],
  123. `${entry.packageName} must not depend on @file-viewer/core directly`
  124. )
  125. const indexSource = await readSource(entry, 'src/index.ts')
  126. assertImportsFrom(indexSource, '@file-viewer/web', entry.packageName)
  127. assertTokens(indexSource, [
  128. 'createViewerControllerHandle',
  129. 'mountViewer',
  130. 'ViewerController',
  131. 'ViewerMountOptions'
  132. ], entry.packageName)
  133. assertNoLegacyIframeTokens(indexSource, entry.packageName)
  134. const globalSource = await readSource(entry, 'src/global.ts')
  135. assertTokens(globalSource, ['FlyfishFileViewerWeb', 'createViewerControllerHandle', 'mountViewer'], `${entry.packageName} global bundle`)
  136. assertNoLegacyIframeTokens(globalSource, `${entry.packageName} global bundle`)
  137. }
  138. async function verifyReactCompatibility() {
  139. const entry = requireEntry('@flyfish-group/file-viewer-react')
  140. const dependencies = installDependencies(entry.packageJson)
  141. assert(
  142. dependencies['@file-viewer/react'] === expectedWorkspaceRange,
  143. `${entry.packageName} must depend on @file-viewer/react@${expectedWorkspaceRange}`
  144. )
  145. assert(
  146. !dependencies['@file-viewer/core'] && !dependencies['@file-viewer/web'],
  147. `${entry.packageName} must be a thin React alias instead of depending on core/web directly`
  148. )
  149. const source = await readSource(entry, 'src/index.tsx')
  150. assertImportsFrom(source, '@file-viewer/react', entry.packageName)
  151. assertNotImportsFrom(source, '@file-viewer/core', entry.packageName)
  152. assertNotImportsFrom(source, '@file-viewer/web', entry.packageName)
  153. assertTokens(source, ['FileViewer', 'FileViewerHandle', 'FileViewerProps'], entry.packageName)
  154. assertNoLegacyIframeTokens(source, entry.packageName)
  155. }
  156. async function verifyVue3ScopedCompatibility() {
  157. const entry = requireEntry('@flyfish-group/file-viewer3')
  158. const dependencies = installDependencies(entry.packageJson)
  159. assert(
  160. dependencies['@file-viewer/vue3'] === expectedWorkspaceRange,
  161. `${entry.packageName} must depend on @file-viewer/vue3@${expectedWorkspaceRange}`
  162. )
  163. assert(
  164. !dependencies['@file-viewer/core'],
  165. `${entry.packageName} must be a thin Vue 3 alias and must not depend on core directly`
  166. )
  167. assert(
  168. !existsSync(join(entry.absoluteDir, 'src')),
  169. `${entry.packageName} must not carry duplicate Vue 3 source; it should generate a thin alias dist`
  170. )
  171. const buildScript = await readSource(entry, 'scripts/build.mjs')
  172. assertTokens(buildScript, [
  173. "export { default } from '@file-viewer/vue3';",
  174. "export * from '@file-viewer/vue3';"
  175. ], `${entry.packageName} build script`)
  176. assertNoLegacyIframeTokens(buildScript, `${entry.packageName} build script`)
  177. }
  178. async function verifyVue3UnscopedCompatibility() {
  179. const entry = requireEntry('file-viewer3')
  180. const dependencies = installDependencies(entry.packageJson)
  181. assert(
  182. dependencies['@file-viewer/vue3'] === expectedWorkspaceRange,
  183. `${entry.packageName} must depend on @file-viewer/vue3@${expectedWorkspaceRange}`
  184. )
  185. assert(
  186. !dependencies['@file-viewer/core'] && !dependencies['@flyfish-group/file-viewer3'],
  187. `${entry.packageName} must remain a thin Vue 3 alias and must not depend on core or another compatibility alias`
  188. )
  189. assert(
  190. !existsSync(join(entry.absoluteDir, 'src')),
  191. `${entry.packageName} must not carry duplicate Vue 3 source; it should generate a thin alias dist`
  192. )
  193. const buildScript = await readSource(entry, 'scripts/build.mjs')
  194. assertTokens(buildScript, [
  195. "export { default } from '@file-viewer/vue3';",
  196. "export * from '@file-viewer/vue3';"
  197. ], `${entry.packageName} build script`)
  198. assertNoLegacyIframeTokens(buildScript, `${entry.packageName} build script`)
  199. }
  200. async function verifyVue27ScopedCompatibility() {
  201. const entry = requireEntry('@flyfish-group/file-viewer')
  202. const dependencies = installDependencies(entry.packageJson)
  203. assert(
  204. dependencies['@file-viewer/vue2.7'] === expectedWorkspaceRange,
  205. `${entry.packageName} must depend on @file-viewer/vue2.7@${expectedWorkspaceRange}`
  206. )
  207. assert(
  208. !dependencies['@file-viewer/core'] && !dependencies['@file-viewer/web'],
  209. `${entry.packageName} must remain a thin Vue 2.7 alias instead of depending on core/web directly`
  210. )
  211. assert(
  212. !existsSync(join(entry.absoluteDir, 'src')),
  213. `${entry.packageName} must not carry duplicate Vue 2.7 source; it should generate a thin alias dist`
  214. )
  215. const buildScript = await readSource(entry, 'scripts/build.mjs')
  216. assertTokens(buildScript, [
  217. "export { default } from '@file-viewer/vue2.7';",
  218. "export * from '@file-viewer/vue2.7';"
  219. ], `${entry.packageName} build script`)
  220. assertNoLegacyIframeTokens(buildScript, `${entry.packageName} build script`)
  221. }
  222. await verifyWebCompatibility()
  223. await verifyReactCompatibility()
  224. await verifyVue3ScopedCompatibility()
  225. await verifyVue3UnscopedCompatibility()
  226. await verifyVue27ScopedCompatibility()
  227. console.log('Verified native compatibility aliases, Vue 2.7/Vue 3 scoped packages, and PPTX renderer ownership.')