sync-wrapper-repos.mjs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { existsSync } from 'node:fs'
  2. import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
  3. import { basename, dirname, join, resolve } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { fileURLToPath } from 'node:url'
  6. const scriptDir = dirname(fileURLToPath(import.meta.url))
  7. const sourceRoot = resolve(scriptDir, '..')
  8. const args = process.argv.slice(2)
  9. const readArg = (name, fallback) => {
  10. const index = args.indexOf(name)
  11. return index >= 0 ? args[index + 1] : fallback
  12. }
  13. const outputRoot = resolve(
  14. sourceRoot,
  15. readArg(
  16. '--out-dir',
  17. process.env.FILE_VIEWER_COMPONENT_REPO_DIR ||
  18. process.env.FILE_VIEWER_WRAPPER_REPO_DIR ||
  19. '.release/component-repos'
  20. )
  21. )
  22. const selectedPackages = new Set(
  23. args
  24. .filter(arg => arg.startsWith('--package='))
  25. .map(arg => arg.slice('--package='.length))
  26. )
  27. const selectedIds = new Set(
  28. args
  29. .filter(arg => arg.startsWith('--id='))
  30. .map(arg => arg.slice('--id='.length))
  31. )
  32. const keepExisting = args.includes('--keep-existing')
  33. const readJson = async path => JSON.parse(await readFile(path, 'utf8'))
  34. const writeJson = async (path, value) => writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
  35. const rootPackage = await readJson(join(sourceRoot, 'package.json'))
  36. const wrapperManifest = await readJson(join(sourceRoot, 'ecosystem', 'wrappers.json'))
  37. const collectPackageDirs = async workspaceRoot => {
  38. if (!existsSync(workspaceRoot)) {
  39. return []
  40. }
  41. const entries = await readdir(workspaceRoot, { withFileTypes: true })
  42. return entries
  43. .filter(entry => entry.isDirectory())
  44. .map(entry => join(workspaceRoot, entry.name))
  45. .filter(packageDir => existsSync(join(packageDir, 'package.json')))
  46. }
  47. const workspacePackageDirs = [
  48. sourceRoot,
  49. join(sourceRoot, 'packages', 'core'),
  50. ...(await collectPackageDirs(join(sourceRoot, 'packages', 'compat'))),
  51. ...(await collectPackageDirs(join(sourceRoot, 'packages', 'components'))),
  52. ...(await collectPackageDirs(join(sourceRoot, 'packages', 'renderers'))),
  53. ...(await collectPackageDirs(join(sourceRoot, 'apps')))
  54. ]
  55. const workspaceVersions = new Map()
  56. for (const packageDir of workspacePackageDirs) {
  57. const packagePath = join(packageDir, 'package.json')
  58. if (!existsSync(packagePath)) {
  59. continue
  60. }
  61. const packageJson = await readJson(packagePath)
  62. workspaceVersions.set(packageJson.name, packageJson.version)
  63. }
  64. const run = (command, commandArgs) => {
  65. const result = spawnSync(command, commandArgs, {
  66. cwd: sourceRoot,
  67. encoding: 'utf8',
  68. stdio: 'pipe'
  69. })
  70. if (result.status !== 0) {
  71. throw new Error(`Command failed: ${command} ${commandArgs.join(' ')}\n${result.stderr || result.stdout}`)
  72. }
  73. return result.stdout.trim()
  74. }
  75. const assertDirectory = async path => {
  76. if (!existsSync(path)) {
  77. throw new Error(`Missing directory: ${path}`)
  78. }
  79. const info = await stat(path)
  80. if (!info.isDirectory()) {
  81. throw new Error(`Not a directory: ${path}`)
  82. }
  83. }
  84. const assertFile = async path => {
  85. if (!existsSync(path)) {
  86. throw new Error(`Missing file: ${path}`)
  87. }
  88. const info = await stat(path)
  89. if (!info.isFile()) {
  90. throw new Error(`Not a file: ${path}`)
  91. }
  92. }
  93. const removePath = path => rm(path, { recursive: true, force: true })
  94. const shouldCopy = source => {
  95. const name = basename(source)
  96. return ![
  97. '.DS_Store',
  98. '.git',
  99. '.vercel',
  100. 'dist',
  101. 'node_modules',
  102. 'tsconfig.tsbuildinfo'
  103. ].includes(name)
  104. }
  105. const copyPackage = async (from, to) => {
  106. if (!keepExisting) {
  107. await removePath(to)
  108. }
  109. await mkdir(to, { recursive: true })
  110. await cp(from, to, {
  111. recursive: true,
  112. force: true,
  113. filter: shouldCopy
  114. })
  115. }
  116. const normalizeWorkspaceRange = (dependencyName, range) => {
  117. if (!range?.startsWith?.('workspace:')) {
  118. return range
  119. }
  120. const workspaceRange = range.slice('workspace:'.length)
  121. if (workspaceRange === '*' || workspaceRange === '') {
  122. const version = workspaceVersions.get(dependencyName)
  123. return version ? `^${version}` : `^${rootPackage.version}`
  124. }
  125. return workspaceRange
  126. }
  127. const normalizeDependencyBlock = block => {
  128. if (!block) {
  129. return
  130. }
  131. for (const [dependencyName, range] of Object.entries(block)) {
  132. block[dependencyName] = normalizeWorkspaceRange(dependencyName, range)
  133. }
  134. }
  135. const normalizePackageJson = async (targetDir, wrapper) => {
  136. const packagePath = join(targetDir, 'package.json')
  137. const packageJson = await readJson(packagePath)
  138. packageJson.packageManager = rootPackage.packageManager
  139. packageJson.private = false
  140. packageJson.repository = {
  141. type: 'git',
  142. url: `git+${wrapper.github}.git`
  143. }
  144. packageJson.homepage = wrapper.github
  145. packageJson.bugs = {
  146. url: `${wrapper.github}/issues`
  147. }
  148. normalizeDependencyBlock(packageJson.dependencies)
  149. normalizeDependencyBlock(packageJson.devDependencies)
  150. normalizeDependencyBlock(packageJson.peerDependencies)
  151. normalizeDependencyBlock(packageJson.optionalDependencies)
  152. await writeJson(packagePath, packageJson)
  153. }
  154. const normalizeStandaloneTsConfig = async targetDir => {
  155. const tsconfigPath = join(targetDir, 'tsconfig.json')
  156. if (!existsSync(tsconfigPath)) {
  157. return
  158. }
  159. const tsconfig = await readJson(tsconfigPath)
  160. if (tsconfig.compilerOptions?.paths) {
  161. delete tsconfig.compilerOptions.paths
  162. }
  163. if (tsconfig.references) {
  164. delete tsconfig.references
  165. }
  166. await writeJson(tsconfigPath, tsconfig)
  167. }
  168. const ensureWebViewerAssets = async (targetDir, wrapper) => {
  169. if (wrapper.id !== 'web') {
  170. return
  171. }
  172. const targetViewerDir = join(targetDir, 'viewer')
  173. if (existsSync(join(targetViewerDir, 'index.html'))) {
  174. return
  175. }
  176. const sourceViewerDir = join(sourceRoot, 'packages/components/web/viewer')
  177. const fallbackViewerDir = join(sourceRoot, 'packages/compat/web/viewer')
  178. const viewerSource = existsSync(join(sourceViewerDir, 'index.html'))
  179. ? sourceViewerDir
  180. : fallbackViewerDir
  181. await assertDirectory(viewerSource)
  182. await cp(viewerSource, targetViewerDir, { recursive: true, force: true })
  183. }
  184. const writeGitignore = async targetDir => {
  185. await writeFile(
  186. join(targetDir, '.gitignore'),
  187. [
  188. 'node_modules/',
  189. 'dist/',
  190. '*.tgz',
  191. '*.tsbuildinfo',
  192. '.DS_Store',
  193. '.pnpm-store/',
  194. '.npm-cache/',
  195. ''
  196. ].join('\n'),
  197. 'utf8'
  198. )
  199. }
  200. const writeStandaloneManifest = async (targetDir, wrapper, packageJson) => {
  201. const manifest = {
  202. schemaVersion: 1,
  203. generatedAt: new Date().toISOString(),
  204. sourceBranch: run('git', ['branch', '--show-current']),
  205. sourceCommit: run('git', ['rev-parse', '--short', 'HEAD']),
  206. organization: wrapperManifest.organization,
  207. framework: wrapper.framework,
  208. packageName: wrapper.packageName,
  209. packageVersion: packageJson.version,
  210. repository: wrapper.repository,
  211. github: wrapper.github,
  212. gitee: wrapper.gitee,
  213. entryFormats: wrapper.entryFormats,
  214. historicalPackages: wrapper.historicalPackages,
  215. sourcePackageDir: wrapper.packageDir,
  216. corePackage: wrapperManifest.corePackage.packageName,
  217. coreSourceVisibility: wrapperManifest.corePackage.visibility
  218. }
  219. await writeJson(join(targetDir, 'wrapper-repo-manifest.json'), manifest)
  220. }
  221. const assertStandaloneRepo = async (targetDir, wrapper) => {
  222. await assertFile(join(targetDir, 'package.json'))
  223. await assertFile(join(targetDir, 'README.md'))
  224. await assertFile(join(targetDir, 'README.en.md'))
  225. await assertFile(join(targetDir, 'LICENSE'))
  226. const packageJson = await readJson(join(targetDir, 'package.json'))
  227. if (packageJson.name !== wrapper.packageName) {
  228. throw new Error(`Package name mismatch in ${targetDir}: ${packageJson.name} !== ${wrapper.packageName}`)
  229. }
  230. const serialized = JSON.stringify(packageJson)
  231. if (serialized.includes('workspace:')) {
  232. throw new Error(`Workspace dependency leaked into standalone repo: ${targetDir}`)
  233. }
  234. if (existsSync(join(targetDir, 'node_modules')) || existsSync(join(targetDir, 'dist'))) {
  235. throw new Error(`Generated dependency/build output leaked into standalone repo: ${targetDir}`)
  236. }
  237. }
  238. const wrappers = wrapperManifest.wrappers.filter(wrapper => {
  239. if (selectedPackages.size && !selectedPackages.has(wrapper.packageName)) {
  240. return false
  241. }
  242. if (selectedIds.size && !selectedIds.has(wrapper.id)) {
  243. return false
  244. }
  245. return true
  246. })
  247. if (!wrappers.length) {
  248. throw new Error('No component packages selected for export.')
  249. }
  250. await mkdir(outputRoot, { recursive: true })
  251. await assertFile(join(sourceRoot, 'LICENSE'))
  252. for (const wrapper of wrappers) {
  253. const sourcePackageDir = join(sourceRoot, wrapper.packageDir)
  254. await assertDirectory(sourcePackageDir)
  255. const targetDir = join(outputRoot, wrapper.repository)
  256. await copyPackage(sourcePackageDir, targetDir)
  257. await ensureWebViewerAssets(targetDir, wrapper)
  258. await cp(join(sourceRoot, 'LICENSE'), join(targetDir, 'LICENSE'), { force: true })
  259. await writeGitignore(targetDir)
  260. await normalizePackageJson(targetDir, wrapper)
  261. await normalizeStandaloneTsConfig(targetDir)
  262. const targetPackageJson = await readJson(join(targetDir, 'package.json'))
  263. await writeStandaloneManifest(targetDir, wrapper, targetPackageJson)
  264. await assertStandaloneRepo(targetDir, wrapper)
  265. console.log(`Prepared ${wrapper.packageName} -> ${targetDir}`)
  266. }
  267. console.log(`Prepared ${wrappers.length} component package repo${wrappers.length === 1 ? '' : 's'} in ${outputRoot}`)