sync-public-main.mjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. import { createReadStream, existsSync } from 'node:fs'
  2. import { cp, mkdir, readFile, readdir, rename, rm, stat, utimes, writeFile } from 'node:fs/promises'
  3. import { basename, join, relative, resolve, sep } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { createHash } from 'node:crypto'
  6. import { loadEcosystemReleaseContext, readJson } from './lib/ecosystem-packages.mjs'
  7. import { assertOpenSourceMainRepoLayout } from './lib/public-main.mjs'
  8. const sourceRoot = process.cwd()
  9. const args = process.argv.slice(2)
  10. function readArg(name, fallback) {
  11. const index = args.indexOf(name)
  12. if (index >= 0) {
  13. return args[index + 1]
  14. }
  15. return fallback
  16. }
  17. const publicRepoDir = resolve(
  18. readArg('--public-repo-dir', process.env.FILE_VIEWER_PUBLIC_REPO_DIR || '../file-viewer-public')
  19. )
  20. const skipBuild = args.includes('--skip-build')
  21. const keepOldArtifacts = args.includes('--keep-old-artifacts')
  22. const includeLegacyVue2Tarball =
  23. args.includes('--include-legacy-vue2-tarball') ||
  24. process.env.FILE_VIEWER_INCLUDE_LEGACY_VUE2_TARBALL === '1'
  25. const skipVue2Tarball = args.includes('--skip-vue2-tarball') || !includeLegacyVue2Tarball
  26. const slimArtifacts =
  27. (args.includes('--slim') || process.env.FILE_VIEWER_PUBLIC_SLIM === '1') &&
  28. !args.includes('--expanded-assets')
  29. const keepExpandedAssets = !slimArtifacts
  30. const {
  31. rootPackage: packageJson,
  32. wrapperManifest,
  33. entries: ecosystemPackageEntries
  34. } = await loadEcosystemReleaseContext(sourceRoot)
  35. const branchRoles = await readJson(join(sourceRoot, 'ecosystem', 'branch-roles.json'))
  36. const releaseSourceBranch = branchRoles.currentSourceBranch || currentBranch()
  37. const version = packageJson.version
  38. const vue2Tarball = resolve(
  39. readArg(
  40. '--vue2-tarball',
  41. process.env.FILE_VIEWER_VUE2_TARBALL ||
  42. join(sourceRoot, '..', 'file-viewer', `flyfish-group-file-viewer-${version}.tgz`)
  43. )
  44. )
  45. const releaseDir = join(sourceRoot, '.release', `file-viewer-v2-${version}`)
  46. const demoStagingDir = join(releaseDir, 'demo')
  47. const wrapperDemoStagingDir = join(releaseDir, 'component-demo')
  48. const ecosystemPackDir = join(releaseDir, 'ecosystem')
  49. const legacyStandaloneDemoArtifactSegment = ['adapter', 'demo'].join('-')
  50. const viewerDemoDistDir = join(sourceRoot, 'apps', 'viewer-demo', 'dist')
  51. const viewerDemoExampleDir = join(sourceRoot, 'apps', 'viewer-demo', 'public', 'example')
  52. const vue3LibraryDistDir = join(sourceRoot, 'packages', 'components', 'vue3', 'dist')
  53. const stableArchiveDate = new Date('2020-01-01T00:00:00.000Z')
  54. const publicCommunityRootFiles = [
  55. 'CHANGELOG.md',
  56. 'CODE_OF_CONDUCT.md',
  57. 'CONTRIBUTING.md',
  58. 'RELEASE_TEMPLATE.md',
  59. 'ROADMAP.md',
  60. 'SECURITY.md',
  61. 'SUPPORT.md'
  62. ]
  63. const privateOperationsRootFiles = [
  64. 'CONTENT_DISTRIBUTION_PLAN.md',
  65. 'GITHUB_GROWTH_AUDIT.md',
  66. 'GITHUB_GROWTH_PLAYBOOK.md',
  67. 'GITHUB_SETTINGS_CHECKLIST.md',
  68. 'TRAFFIC_TRACKING.md'
  69. ]
  70. const publicGithubEntries = [
  71. 'FUNDING.yml',
  72. 'PULL_REQUEST_TEMPLATE.md',
  73. 'ISSUE_TEMPLATE',
  74. 'social-preview.svg',
  75. 'social-preview.png'
  76. ]
  77. function run(command, commandArgs, options = {}) {
  78. console.log(`$ ${[command, ...commandArgs].join(' ')}`)
  79. const result = spawnSync(command, commandArgs, {
  80. cwd: options.cwd || sourceRoot,
  81. stdio: options.capture ? 'pipe' : 'inherit',
  82. encoding: 'utf8'
  83. })
  84. if (result.status !== 0) {
  85. const details = result.stderr || result.stdout || ''
  86. throw new Error(`Command failed: ${command} ${commandArgs.join(' ')}\n${details}`)
  87. }
  88. return result.stdout?.trim() || ''
  89. }
  90. async function assertDirectory(path, label) {
  91. if (!existsSync(path)) {
  92. throw new Error(`${label} does not exist: ${path}`)
  93. }
  94. const info = await stat(path)
  95. if (!info.isDirectory()) {
  96. throw new Error(`${label} is not a directory: ${path}`)
  97. }
  98. }
  99. async function assertFile(path, label) {
  100. if (!existsSync(path)) {
  101. throw new Error(`${label} does not exist: ${path}`)
  102. }
  103. const info = await stat(path)
  104. if (!info.isFile()) {
  105. throw new Error(`${label} is not a file: ${path}`)
  106. }
  107. }
  108. async function assertCleanGitRepo(repoDir, label) {
  109. const gitMetadataPath = join(repoDir, '.git')
  110. if (!existsSync(gitMetadataPath)) {
  111. throw new Error(`${label} git metadata does not exist: ${gitMetadataPath}`)
  112. }
  113. const gitMetadata = await stat(gitMetadataPath)
  114. if (!gitMetadata.isDirectory() && !gitMetadata.isFile()) {
  115. throw new Error(`${label} git metadata must be a .git directory or worktree .git file: ${gitMetadataPath}`)
  116. }
  117. const status = run('git', ['status', '--porcelain'], { cwd: repoDir, capture: true })
  118. if (status) {
  119. throw new Error(`${label} has uncommitted changes. Commit or clean it first:\n${status}`)
  120. }
  121. }
  122. function currentBranch() {
  123. return run('git', ['branch', '--show-current'], { capture: true })
  124. }
  125. function currentCommit() {
  126. return run('git', ['rev-parse', 'HEAD'], { capture: true })
  127. }
  128. function remoteHeadForBranch(remoteName, branchName) {
  129. const refs = run('git', ['ls-remote', '--heads', remoteName, branchName], { capture: true })
  130. const line = refs
  131. .split('\n')
  132. .map(value => value.trim())
  133. .find(Boolean)
  134. return line ? line.split(/\s+/)[0] : ''
  135. }
  136. function assertReleaseSourceBranch() {
  137. const sourceRemoteName = branchRoles.sourceRemote?.name || 'origin'
  138. const branch = currentBranch()
  139. if (branch === releaseSourceBranch) {
  140. return
  141. }
  142. const remoteHead = remoteHeadForBranch(sourceRemoteName, releaseSourceBranch)
  143. if (remoteHead && remoteHead === currentCommit()) {
  144. return
  145. }
  146. if (process.env.FILE_VIEWER_ALLOW_NON_SOURCE_BRANCH === '1') {
  147. return
  148. }
  149. throw new Error(
  150. `Open-source main repository releases must be prepared from ${releaseSourceBranch} or an identical HEAD. Current branch ${branch || '(detached)'} does not match ${sourceRemoteName}/${releaseSourceBranch}.`
  151. )
  152. }
  153. async function removePath(path) {
  154. await rm(path, { recursive: true, force: true })
  155. }
  156. async function fileDigest(path) {
  157. return new Promise((resolveDigest, rejectDigest) => {
  158. const hash = createHash('sha256')
  159. const stream = createReadStream(path)
  160. stream.on('data', chunk => hash.update(chunk))
  161. stream.on('error', rejectDigest)
  162. stream.on('end', () => resolveDigest(hash.digest('hex')))
  163. })
  164. }
  165. async function filesEqual(left, right) {
  166. if (!existsSync(left) || !existsSync(right)) {
  167. return false
  168. }
  169. const [leftStat, rightStat] = await Promise.all([stat(left), stat(right)])
  170. if (leftStat.size !== rightStat.size) {
  171. return false
  172. }
  173. const [leftDigest, rightDigest] = await Promise.all([fileDigest(left), fileDigest(right)])
  174. return leftDigest === rightDigest
  175. }
  176. async function replaceFileIfChanged(sourceFile, targetFile, label = targetFile) {
  177. if (await filesEqual(sourceFile, targetFile)) {
  178. await removePath(sourceFile)
  179. console.log(`unchanged ${label}`)
  180. return false
  181. }
  182. await removePath(targetFile)
  183. await rename(sourceFile, targetFile)
  184. return true
  185. }
  186. async function copyFileIfChanged(sourceFile, targetFile, label = targetFile) {
  187. if (await filesEqual(sourceFile, targetFile)) {
  188. console.log(`unchanged ${label}`)
  189. return false
  190. }
  191. await cp(sourceFile, targetFile, { force: true })
  192. return true
  193. }
  194. async function writeTextIfChanged(targetFile, content, label = targetFile) {
  195. if (existsSync(targetFile) && (await readFile(targetFile, 'utf8')) === content) {
  196. console.log(`unchanged ${label}`)
  197. return false
  198. }
  199. await writeFile(targetFile, content, 'utf8')
  200. return true
  201. }
  202. async function copyCleanDir(from, to) {
  203. await removePath(to)
  204. await cp(from, to, {
  205. recursive: true,
  206. force: true,
  207. filter: source => {
  208. const name = basename(source)
  209. return name !== '.DS_Store' && name !== '.git' && name !== '.vercel' && name !== '.gitignore'
  210. }
  211. })
  212. }
  213. async function copySourceDir(from, to, options = {}) {
  214. const excludedRelativePaths = new Set(options.exclude || [])
  215. await removePath(to)
  216. await cp(from, to, {
  217. recursive: true,
  218. force: true,
  219. filter: source => {
  220. const name = basename(source)
  221. const relativePath = relative(from, source).split(sep).join('/')
  222. if (
  223. name === '.DS_Store' ||
  224. name === '.git' ||
  225. name === '.vercel' ||
  226. name === 'node_modules' ||
  227. name === 'dist' ||
  228. name === 'tsconfig.tsbuildinfo'
  229. ) {
  230. return false
  231. }
  232. return !excludedRelativePaths.has(relativePath)
  233. }
  234. })
  235. }
  236. async function walkFiles(dir, callback) {
  237. if (!existsSync(dir)) {
  238. return
  239. }
  240. const entries = await readdir(dir, { withFileTypes: true })
  241. for (const entry of entries) {
  242. const path = join(dir, entry.name)
  243. if (entry.isDirectory()) {
  244. if (entry.name === 'node_modules' || entry.name === 'dist') {
  245. continue
  246. }
  247. await walkFiles(path, callback)
  248. continue
  249. }
  250. await callback(path, entry.name)
  251. }
  252. }
  253. async function normalizeArchiveTimestamps(path) {
  254. if (!existsSync(path)) {
  255. return
  256. }
  257. const info = await stat(path)
  258. if (info.isDirectory()) {
  259. const entries = await readdir(path, { withFileTypes: true })
  260. for (const entry of entries) {
  261. await normalizeArchiveTimestamps(join(path, entry.name))
  262. }
  263. }
  264. await utimes(path, stableArchiveDate, stableArchiveDate)
  265. }
  266. function normalizePublicWorkspaceRange(dependencyName, range) {
  267. if (!range?.startsWith?.('workspace:')) {
  268. return range
  269. }
  270. return range
  271. }
  272. function normalizePublicDependencyBlock(block) {
  273. if (!block) {
  274. return
  275. }
  276. for (const [dependencyName, range] of Object.entries(block)) {
  277. block[dependencyName] = normalizePublicWorkspaceRange(dependencyName, range)
  278. }
  279. }
  280. function normalizePublicPackageScripts(packageJson, relativePackageDir) {
  281. if (relativePackageDir === 'apps/viewer-demo') {
  282. packageJson.scripts = {
  283. dev: 'vite --host 127.0.0.1',
  284. build: 'vue-tsc --noEmit -p tsconfig.json && vite build',
  285. preview: 'vite preview --host 127.0.0.1',
  286. 'type-check': 'vue-tsc --noEmit -p tsconfig.json'
  287. }
  288. }
  289. if (relativePackageDir === 'apps/component-demo') {
  290. packageJson.scripts = {
  291. 'prepare-viewer': 'node scripts/sync-viewer-assets.mjs',
  292. dev: 'pnpm prepare-viewer && vite --host 127.0.0.1',
  293. build: 'pnpm prepare-viewer && tsc -b tsconfig.json && vite build',
  294. preview: 'vite preview --host 127.0.0.1'
  295. }
  296. }
  297. }
  298. async function rewritePublicPackageJsons(repoDir) {
  299. await walkFiles(repoDir, async (path, filename) => {
  300. if (filename !== 'package.json') {
  301. return
  302. }
  303. const packageJson = await readJson(path)
  304. const relativePackageDir = relative(repoDir, resolve(path, '..')).split(sep).join('/')
  305. normalizePublicDependencyBlock(packageJson.dependencies)
  306. normalizePublicDependencyBlock(packageJson.devDependencies)
  307. normalizePublicDependencyBlock(packageJson.peerDependencies)
  308. normalizePublicDependencyBlock(packageJson.optionalDependencies)
  309. normalizePublicPackageScripts(packageJson, relativePackageDir)
  310. await writeFile(path, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8')
  311. })
  312. }
  313. async function rewritePublicTsConfigs(repoDir) {
  314. await walkFiles(join(repoDir, 'packages'), async (path, filename) => {
  315. if (filename !== 'tsconfig.json') {
  316. return
  317. }
  318. const tsconfig = await readJson(path)
  319. if (Array.isArray(tsconfig.references)) {
  320. const references = tsconfig.references.filter(reference => reference.path !== '../../core')
  321. if (references.length) {
  322. tsconfig.references = references
  323. } else {
  324. delete tsconfig.references
  325. }
  326. }
  327. await writeFile(path, `${JSON.stringify(tsconfig, null, 2)}\n`, 'utf8')
  328. })
  329. }
  330. async function writePublicWorkspace(repoDir) {
  331. await writeFile(
  332. join(repoDir, 'pnpm-workspace.yaml'),
  333. [
  334. 'packages:',
  335. ' - packages/core',
  336. ' - packages/renderers/*',
  337. ' - packages/components/*',
  338. ' - packages/compat/*',
  339. ' - apps/*',
  340. '',
  341. 'peerDependencyRules:',
  342. ' allowedVersions:',
  343. " '@types/react': '>=17 <20'",
  344. " react: '>=17 <20'",
  345. " react-dom: '>=17 <20'",
  346. '',
  347. 'allowBuilds:',
  348. " '@parcel/watcher': true",
  349. ' canvas: true',
  350. ' core-js: true',
  351. ' es5-ext: true',
  352. ' esbuild: true',
  353. ''
  354. ].join('\n'),
  355. 'utf8'
  356. )
  357. }
  358. async function writePublicRootPackage(repoDir) {
  359. const publicPackage = {
  360. name: '@flyfish-group/file-viewer-open-source',
  361. version,
  362. private: true,
  363. type: 'module',
  364. packageManager: packageJson.packageManager,
  365. description: 'Flyfish File Viewer open source official site, demo, core, renderer engines, standard component packages, compatibility aliases, and documentation.',
  366. scripts: {
  367. dev: 'pnpm --filter @flyfish-group/file-viewer-demo dev',
  368. 'site:dev': 'pnpm --filter @flyfish-group/file-viewer-site dev',
  369. 'site:build': 'pnpm --filter @flyfish-group/file-viewer-site build',
  370. 'site:preview': 'pnpm --filter @flyfish-group/file-viewer-site preview',
  371. build: 'pnpm build:components && pnpm build:demo',
  372. 'build:demo': 'pnpm --filter @flyfish-group/file-viewer-demo build',
  373. 'build:component-demo': 'pnpm --filter @flyfish-group/file-viewer-component-demo build',
  374. 'build:pptx': 'pnpm --filter @file-viewer/pptx build',
  375. 'build:core': 'pnpm --filter @file-viewer/core build',
  376. 'build:components': 'pnpm build:core && pnpm --filter @file-viewer/web build && pnpm --filter @file-viewer/react build && pnpm --filter @file-viewer/react-legacy build && pnpm --filter @file-viewer/vue3 build && pnpm --filter @file-viewer/vue2.7 build && pnpm --filter @file-viewer/vue2.6 build && pnpm --filter @file-viewer/jquery build && pnpm --filter @file-viewer/svelte build',
  377. 'type-check': 'pnpm --filter @flyfish-group/file-viewer-demo type-check && pnpm type-check:core && pnpm type-check:components',
  378. 'type-check:core': 'pnpm --filter @file-viewer/core type-check',
  379. 'type-check:components': 'pnpm --filter @file-viewer/web type-check && pnpm --filter @file-viewer/react type-check && pnpm --filter @file-viewer/react-legacy type-check && pnpm --filter @file-viewer/vue3 type-check && pnpm --filter @file-viewer/vue2.7 type-check && pnpm --filter @file-viewer/vue2.6 type-check && pnpm --filter @file-viewer/jquery type-check && pnpm --filter @file-viewer/svelte type-check',
  380. 'docs:dev': 'vitepress dev docs',
  381. 'docs:build': 'vitepress build docs',
  382. 'docs:preview': 'vitepress preview docs'
  383. },
  384. devDependencies: {
  385. '@types/node': packageJson.devDependencies['@types/node'],
  386. prettier: packageJson.devDependencies.prettier,
  387. typescript: packageJson.devDependencies.typescript,
  388. vitepress: packageJson.devDependencies.vitepress
  389. },
  390. license: packageJson.license
  391. }
  392. await writeFile(join(repoDir, 'package.json'), `${JSON.stringify(publicPackage, null, 2)}\n`, 'utf8')
  393. }
  394. async function copyPublicSourceTree(repoDir) {
  395. await copySourceDir(join(sourceRoot, 'apps'), join(repoDir, 'apps'), {
  396. exclude: [
  397. 'component-demo/public/file-viewer',
  398. 'component-demo/public/vendor',
  399. 'component-demo/public/wasm'
  400. ]
  401. })
  402. await mkdir(join(repoDir, 'packages'), { recursive: true })
  403. await copySourceDir(join(sourceRoot, 'packages', 'core'), join(repoDir, 'packages', 'core'))
  404. await copySourceDir(join(sourceRoot, 'packages', 'renderers'), join(repoDir, 'packages', 'renderers'))
  405. await copySourceDir(join(sourceRoot, 'packages', 'components'), join(repoDir, 'packages', 'components'))
  406. await copySourceDir(join(sourceRoot, 'packages', 'compat'), join(repoDir, 'packages', 'compat'))
  407. await copySourceDir(join(sourceRoot, 'docs'), join(repoDir, 'docs'), {
  408. exclude: [
  409. '.vitepress/dist'
  410. ]
  411. })
  412. await writePublicWorkspace(repoDir)
  413. await writePublicRootPackage(repoDir)
  414. await rewritePublicPackageJsons(repoDir)
  415. }
  416. async function copyPublicCommunityFiles(repoDir) {
  417. for (const file of privateOperationsRootFiles) {
  418. await removePath(join(repoDir, file))
  419. }
  420. for (const file of publicCommunityRootFiles) {
  421. const sourceFile = join(sourceRoot, file)
  422. const targetFile = join(repoDir, file)
  423. if (existsSync(sourceFile)) {
  424. await cp(sourceFile, targetFile, { force: true })
  425. } else {
  426. await removePath(targetFile)
  427. }
  428. }
  429. const sourceGithubDir = join(sourceRoot, '.github')
  430. const targetGithubDir = join(repoDir, '.github')
  431. await removePath(targetGithubDir)
  432. if (!existsSync(sourceGithubDir)) {
  433. return
  434. }
  435. await mkdir(targetGithubDir, { recursive: true })
  436. for (const entry of publicGithubEntries) {
  437. const sourceEntry = join(sourceGithubDir, entry)
  438. const targetEntry = join(targetGithubDir, entry)
  439. if (!existsSync(sourceEntry)) {
  440. continue
  441. }
  442. const info = await stat(sourceEntry)
  443. if (info.isDirectory()) {
  444. await copyCleanDir(sourceEntry, targetEntry)
  445. } else {
  446. await cp(sourceEntry, targetEntry, { force: true })
  447. }
  448. }
  449. }
  450. async function removeOldArtifacts(artifactsDir) {
  451. await mkdir(artifactsDir, { recursive: true })
  452. if (keepOldArtifacts) {
  453. return
  454. }
  455. const entries = await readdir(artifactsDir)
  456. const currentEcosystemTarballs = new Set(
  457. ecosystemPackageEntries.map(ecosystemPackage => ecosystemPackage.tarballName)
  458. )
  459. const currentStaticArchives = new Set([
  460. `file-viewer-v2-${version}-demo.tar.gz`,
  461. `file-viewer-v2-${version}-component-demo.tar.gz`,
  462. `file-viewer-v2-${version}-lib-dist.tar.gz`,
  463. `file-viewer-v2-${version}-docs.tar.gz`
  464. ])
  465. for (const entry of entries) {
  466. if (
  467. /^file-viewer-v[23]-.*-(demo|component-demo|lib-dist|docs)\.tar\.gz$/.test(entry) ||
  468. entry.includes(`-${legacyStandaloneDemoArtifactSegment}.tar.gz`)
  469. ) {
  470. if (!currentStaticArchives.has(entry)) {
  471. await removePath(join(artifactsDir, entry))
  472. }
  473. }
  474. if (entry.endsWith('.tgz')) {
  475. if (!currentEcosystemTarballs.has(entry)) {
  476. await removePath(join(artifactsDir, entry))
  477. }
  478. }
  479. }
  480. }
  481. async function createTarball(sourceDir, targetFile) {
  482. const tempTar = `${targetFile}.tar-${process.pid}-${Date.now()}`
  483. const tempGzip = `${tempTar}.gz`
  484. await removePath(tempTar)
  485. await removePath(tempGzip)
  486. await normalizeArchiveTimestamps(sourceDir)
  487. run('tar', ['-cf', tempTar, '-C', sourceDir, '.'])
  488. run('gzip', ['-n', '-f', tempTar])
  489. await replaceFileIfChanged(tempGzip, targetFile, basename(targetFile))
  490. }
  491. function shouldPublishArtifactTarball(packageRecord) {
  492. return packageRecord.releaseArtifact?.includeTarball !== false
  493. }
  494. async function readEcosystemPackManifest() {
  495. const manifestPath = join(ecosystemPackDir, 'npm-release-manifest.json')
  496. await assertFile(manifestPath, 'Ecosystem npm release manifest')
  497. const manifest = await readJson(manifestPath)
  498. const packageRecords = manifest.packages || []
  499. const packedPackages = new Set(packageRecords.map(packageRecord => packageRecord.packageName))
  500. for (const entry of ecosystemPackageEntries) {
  501. if (!packedPackages.has(entry.packageName)) {
  502. throw new Error(`Ecosystem pack manifest is missing ${entry.packageName}`)
  503. }
  504. }
  505. for (const packageRecord of packageRecords) {
  506. if (shouldPublishArtifactTarball(packageRecord)) {
  507. await assertFile(join(ecosystemPackDir, packageRecord.tarball), `${packageRecord.packageName} tarball`)
  508. }
  509. }
  510. return manifest
  511. }
  512. async function writeReleaseManifest(repoDir, ecosystemPackManifest) {
  513. const allowedRoots = keepExpandedAssets
  514. ? ['README.md', 'README.en.md', 'BRANCHES.md', 'WRAPPER_ECOSYSTEM.md', 'LICENSE', ...publicCommunityRootFiles, 'package.json', 'pnpm-workspace.yaml', '.github', 'apps', 'packages', 'dist', 'demo', 'component-demo', 'docs', 'docs-dist', 'example', 'artifacts']
  515. : ['README.md', 'README.en.md', 'BRANCHES.md', 'WRAPPER_ECOSYSTEM.md', 'LICENSE', ...publicCommunityRootFiles, 'package.json', 'pnpm-workspace.yaml', '.github', 'apps', 'packages', 'dist', 'artifacts']
  516. const wrappersByPackageName = new Map(wrapperManifest.wrappers.map(wrapper => [wrapper.packageName, wrapper]))
  517. const renderersByPackageName = new Map((wrapperManifest.renderers || []).map(renderer => [renderer.packageName, renderer]))
  518. const packages = ecosystemPackManifest.packages || []
  519. const metadataAssets = [
  520. {
  521. name: 'release-manifest.json',
  522. role: 'release-manifest',
  523. required: true,
  524. description: 'Release index for ecosystem packages, repositories, source policy and public artifacts.'
  525. },
  526. {
  527. name: 'release-status.json',
  528. role: 'release-status',
  529. required: true,
  530. description: 'Machine-readable channel status, remaining gaps and blocker classification.'
  531. },
  532. {
  533. name: 'release-status.schema.json',
  534. role: 'release-status-schema',
  535. required: true,
  536. description: 'JSON Schema for release-status.json.'
  537. }
  538. ]
  539. const manifest = {
  540. version,
  541. package: packageJson.name,
  542. generatedAt: new Date().toISOString(),
  543. sourceBranch: releaseSourceBranch,
  544. sourceCommit: run('git', ['rev-parse', '--short', 'HEAD'], { capture: true }),
  545. corePackage: wrapperManifest.corePackage,
  546. ecosystemPackages: Object.fromEntries(
  547. packages.map(packageRecord => [packageRecord.packageName, packageRecord.version])
  548. ),
  549. metadataAssets,
  550. ecosystemArtifacts: packages.map(packageRecord => {
  551. const wrapper = wrappersByPackageName.get(packageRecord.packageName)
  552. const renderer = renderersByPackageName.get(packageRecord.packageName)
  553. const includeTarball = shouldPublishArtifactTarball(packageRecord)
  554. return {
  555. name: packageRecord.packageName,
  556. version: packageRecord.version,
  557. kind: packageRecord.kind,
  558. framework: wrapper?.framework ?? null,
  559. packageDir: packageRecord.packageDir,
  560. tarball: includeTarball ? packageRecord.tarball : null,
  561. artifactIncluded: includeTarball,
  562. artifactDuplicateOf: packageRecord.releaseArtifact?.duplicateOf ?? null,
  563. artifactNote: packageRecord.releaseArtifact?.reason ?? null,
  564. github: packageRecord.github ?? null,
  565. gitee: packageRecord.gitee ?? null,
  566. entryFormats: wrapper?.entryFormats ?? renderer?.entryFormats ?? [],
  567. historicalPackages: wrapper?.historicalPackages ?? []
  568. }
  569. }),
  570. wrapperRepositories: wrapperManifest.wrappers.map(wrapper => ({
  571. framework: wrapper.framework,
  572. packageName: wrapper.packageName,
  573. repository: wrapper.repository,
  574. github: wrapper.github,
  575. gitee: wrapper.gitee,
  576. entryFormats: wrapper.entryFormats,
  577. historicalPackages: wrapper.historicalPackages
  578. })),
  579. vue2Package: skipVue2Tarball
  580. ? null
  581. : {
  582. name: '@flyfish-group/file-viewer',
  583. version,
  584. tarball: basename(vue2Tarball)
  585. },
  586. publicRepo: repoDir,
  587. openSourceMain: true,
  588. coreSourceIncluded: true,
  589. sourcePolicy: 'public-open-source-main-repository',
  590. repositoryRole: 'open-source-main-repository',
  591. layout: keepExpandedAssets ? 'expanded' : 'slim',
  592. allowedRoots,
  593. archiveOnlyRoots: keepExpandedAssets ? [] : ['demo', 'component-demo', 'docs-dist', 'example'],
  594. slimMode: slimArtifacts
  595. }
  596. const manifestPath = join(repoDir, 'artifacts', 'release-manifest.json')
  597. if (existsSync(manifestPath)) {
  598. const previousManifest = await readJson(manifestPath)
  599. const previousComparable = {
  600. ...previousManifest,
  601. generatedAt: manifest.generatedAt
  602. }
  603. if (JSON.stringify(previousComparable) === JSON.stringify(manifest)) {
  604. manifest.generatedAt = previousManifest.generatedAt
  605. }
  606. }
  607. await writeTextIfChanged(
  608. manifestPath,
  609. `${JSON.stringify(manifest, null, 2)}\n`,
  610. 'artifacts/release-manifest.json'
  611. )
  612. }
  613. assertReleaseSourceBranch()
  614. await assertCleanGitRepo(publicRepoDir, 'Open-source main repository')
  615. await assertOpenSourceMainRepoLayout(publicRepoDir)
  616. if (!skipBuild) {
  617. await removePath(releaseDir)
  618. await mkdir(demoStagingDir, { recursive: true })
  619. await mkdir(wrapperDemoStagingDir, { recursive: true })
  620. await mkdir(ecosystemPackDir, { recursive: true })
  621. run('pnpm', ['run', 'build:viewer-assets'])
  622. await copyCleanDir(viewerDemoDistDir, demoStagingDir)
  623. run('pnpm', ['run', 'build:component-demo'])
  624. await copyCleanDir(join(sourceRoot, 'apps', 'component-demo', 'dist'), wrapperDemoStagingDir)
  625. run('pnpm', ['--filter', '@file-viewer/core', 'build'])
  626. run('pnpm', ['run', 'build:component-packages'])
  627. run('pnpm', ['run', 'obfuscate'])
  628. run('pnpm', ['run', 'docs:build'])
  629. run('node', ['scripts/release-ecosystem-packages.mjs', '--pack', '--pack-dir', ecosystemPackDir, '--clean'])
  630. } else {
  631. await assertDirectory(demoStagingDir, 'Demo staging directory')
  632. await assertDirectory(wrapperDemoStagingDir, 'Component demo staging directory')
  633. await assertDirectory(vue3LibraryDistDir, 'Vue3 library dist directory')
  634. await assertDirectory(join(sourceRoot, 'docs', '.vitepress', 'dist'), 'Docs dist directory')
  635. await assertDirectory(ecosystemPackDir, 'Ecosystem pack directory')
  636. }
  637. const artifactsDir = join(publicRepoDir, 'artifacts')
  638. await removeOldArtifacts(artifactsDir)
  639. await removePath(join(publicRepoDir, 'demo'))
  640. await removePath(join(publicRepoDir, legacyStandaloneDemoArtifactSegment))
  641. await removePath(join(publicRepoDir, 'component-demo'))
  642. await removePath(join(publicRepoDir, 'docs'))
  643. await removePath(join(publicRepoDir, 'docs-dist'))
  644. await removePath(join(publicRepoDir, 'example'))
  645. await removePath(join(publicRepoDir, 'apps'))
  646. await removePath(join(publicRepoDir, 'packages'))
  647. await removePath(join(publicRepoDir, 'ecosystem'))
  648. await removePath(join(publicRepoDir, 'pnpm-workspace.yaml'))
  649. await copyPublicSourceTree(publicRepoDir)
  650. await copyPublicCommunityFiles(publicRepoDir)
  651. if (keepExpandedAssets) {
  652. await copyCleanDir(demoStagingDir, join(publicRepoDir, 'demo'))
  653. await copyCleanDir(wrapperDemoStagingDir, join(publicRepoDir, 'component-demo'))
  654. await copyCleanDir(join(sourceRoot, 'docs', '.vitepress', 'dist'), join(publicRepoDir, 'docs-dist'))
  655. await copyCleanDir(viewerDemoExampleDir, join(publicRepoDir, 'example'))
  656. }
  657. await copyCleanDir(vue3LibraryDistDir, join(publicRepoDir, 'dist'))
  658. await cp(join(sourceRoot, 'README.md'), join(publicRepoDir, 'README.md'), { force: true })
  659. await cp(join(sourceRoot, 'README.en.md'), join(publicRepoDir, 'README.en.md'), { force: true })
  660. await cp(join(sourceRoot, 'BRANCHES.md'), join(publicRepoDir, 'BRANCHES.md'), { force: true })
  661. await cp(join(sourceRoot, 'WRAPPER_ECOSYSTEM.md'), join(publicRepoDir, 'WRAPPER_ECOSYSTEM.md'), { force: true })
  662. await cp(join(sourceRoot, 'LICENSE'), join(publicRepoDir, 'LICENSE'), { force: true })
  663. const ecosystemPackManifest = await readEcosystemPackManifest()
  664. for (const packageRecord of ecosystemPackManifest.packages || []) {
  665. if (!shouldPublishArtifactTarball(packageRecord)) {
  666. continue
  667. }
  668. await copyFileIfChanged(
  669. join(ecosystemPackDir, packageRecord.tarball),
  670. join(artifactsDir, packageRecord.tarball),
  671. packageRecord.tarball
  672. )
  673. }
  674. if (!skipVue2Tarball) {
  675. await assertFile(vue2Tarball, 'Vue2 npm tarball')
  676. await copyFileIfChanged(vue2Tarball, join(artifactsDir, basename(vue2Tarball)), basename(vue2Tarball))
  677. }
  678. await createTarball(demoStagingDir, join(artifactsDir, `file-viewer-v2-${version}-demo.tar.gz`))
  679. await createTarball(wrapperDemoStagingDir, join(artifactsDir, `file-viewer-v2-${version}-component-demo.tar.gz`))
  680. await createTarball(join(publicRepoDir, 'dist'), join(artifactsDir, `file-viewer-v2-${version}-lib-dist.tar.gz`))
  681. await createTarball(join(sourceRoot, 'docs', '.vitepress', 'dist'), join(artifactsDir, `file-viewer-v2-${version}-docs.tar.gz`))
  682. await copyFileIfChanged(
  683. join(sourceRoot, 'ecosystem', 'release-status.schema.json'),
  684. join(artifactsDir, 'release-status.schema.json'),
  685. 'release-status.schema.json'
  686. )
  687. await writeReleaseManifest(publicRepoDir, ecosystemPackManifest)
  688. run('node', ['scripts/write-release-status-report.mjs', '--public-repo-dir', publicRepoDir, '--fast'])
  689. await assertOpenSourceMainRepoLayout(publicRepoDir)
  690. run('node', ['scripts/verify-public-main.mjs', '--public-repo-dir', publicRepoDir])
  691. console.log(`Open-source main repository prepared in ${publicRepoDir}`)
  692. console.log('Review with:')
  693. console.log(` git -C ${publicRepoDir} status --short`)
  694. console.log(` git -C ${publicRepoDir} diff --stat`)