sync-renderer-repos.mjs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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 ecosystemManifest = 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, renderer) => {
  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+${renderer.github}.git`
  143. }
  144. packageJson.homepage = renderer.github
  145. packageJson.bugs = {
  146. url: `${renderer.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 writeGitignore = async targetDir => {
  169. await writeFile(
  170. join(targetDir, '.gitignore'),
  171. [
  172. 'node_modules/',
  173. 'dist/',
  174. '*.tgz',
  175. '*.tsbuildinfo',
  176. '.DS_Store',
  177. '.pnpm-store/',
  178. '.npm-cache/',
  179. ''
  180. ].join('\n'),
  181. 'utf8'
  182. )
  183. }
  184. const writeStandaloneManifest = async (targetDir, renderer, packageJson) => {
  185. await writeJson(join(targetDir, 'renderer-repo-manifest.json'), {
  186. schemaVersion: 1,
  187. generatedAt: new Date().toISOString(),
  188. sourceBranch: run('git', ['branch', '--show-current']),
  189. sourceCommit: run('git', ['rev-parse', '--short', 'HEAD']),
  190. organization: ecosystemManifest.organization,
  191. rendererId: renderer.id,
  192. packageName: renderer.packageName,
  193. packageVersion: packageJson.version,
  194. repository: renderer.repository,
  195. github: renderer.github,
  196. gitee: renderer.gitee,
  197. entryFormats: renderer.entryFormats,
  198. sourcePackageDir: renderer.packageDir,
  199. publicSource: renderer.publicSource === true
  200. })
  201. }
  202. const assertStandaloneRepo = async (targetDir, renderer) => {
  203. await assertFile(join(targetDir, 'package.json'))
  204. await assertFile(join(targetDir, 'README.md'))
  205. await assertFile(join(targetDir, 'README.en.md'))
  206. await assertFile(join(targetDir, 'LICENSE'))
  207. const packageJson = await readJson(join(targetDir, 'package.json'))
  208. if (packageJson.name !== renderer.packageName) {
  209. throw new Error(`Package name mismatch in ${targetDir}: ${packageJson.name} !== ${renderer.packageName}`)
  210. }
  211. const serialized = JSON.stringify(packageJson)
  212. if (serialized.includes('workspace:')) {
  213. throw new Error(`Workspace dependency leaked into standalone renderer repo: ${targetDir}`)
  214. }
  215. if (existsSync(join(targetDir, 'node_modules')) || existsSync(join(targetDir, 'dist'))) {
  216. throw new Error(`Generated dependency/build output leaked into standalone renderer repo: ${targetDir}`)
  217. }
  218. }
  219. const renderers = (ecosystemManifest.renderers || []).filter(renderer => {
  220. if (selectedPackages.size && !selectedPackages.has(renderer.packageName)) {
  221. return false
  222. }
  223. if (selectedIds.size && !selectedIds.has(renderer.id)) {
  224. return false
  225. }
  226. return true
  227. })
  228. if (!renderers.length) {
  229. throw new Error('No renderer packages selected for export.')
  230. }
  231. await mkdir(outputRoot, { recursive: true })
  232. await assertFile(join(sourceRoot, 'LICENSE'))
  233. for (const renderer of renderers) {
  234. const sourcePackageDir = join(sourceRoot, renderer.packageDir)
  235. await assertDirectory(sourcePackageDir)
  236. const targetDir = join(outputRoot, renderer.repository)
  237. await copyPackage(sourcePackageDir, targetDir)
  238. await cp(join(sourceRoot, 'LICENSE'), join(targetDir, 'LICENSE'), { force: true })
  239. await writeGitignore(targetDir)
  240. await normalizePackageJson(targetDir, renderer)
  241. await normalizeStandaloneTsConfig(targetDir)
  242. const targetPackageJson = await readJson(join(targetDir, 'package.json'))
  243. await writeStandaloneManifest(targetDir, renderer, targetPackageJson)
  244. await assertStandaloneRepo(targetDir, renderer)
  245. console.log(`Prepared ${renderer.packageName} -> ${targetDir}`)
  246. }
  247. console.log(`Prepared ${renderers.length} renderer package repo${renderers.length === 1 ? '' : 's'} in ${outputRoot}`)