verify-wrapper-repos.mjs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. import { existsSync } from 'node:fs'
  2. import { readdir, readFile, stat } from 'node:fs/promises'
  3. import { dirname, join, resolve } from 'node:path'
  4. import { spawnSync } from 'node:child_process'
  5. import { fileURLToPath } from 'node:url'
  6. import vm from 'node:vm'
  7. import ts from 'typescript'
  8. import { allowedEntryFormats, entryFormatLabels } from './lib/wrapper-entry-formats.mjs'
  9. const scriptDir = dirname(fileURLToPath(import.meta.url))
  10. const sourceRoot = resolve(scriptDir, '..')
  11. const args = process.argv.slice(2)
  12. const readArg = (name, fallback) => {
  13. const index = args.indexOf(name)
  14. return index >= 0 ? args[index + 1] : fallback
  15. }
  16. const outputRoot = resolve(
  17. sourceRoot,
  18. readArg(
  19. '--out-dir',
  20. process.env.FILE_VIEWER_COMPONENT_REPO_DIR ||
  21. process.env.FILE_VIEWER_WRAPPER_REPO_DIR ||
  22. '.release/component-repos'
  23. )
  24. )
  25. const sourceOnly = args.includes('--source-only')
  26. const selectedPackages = new Set(
  27. args
  28. .filter(arg => arg.startsWith('--package='))
  29. .map(arg => arg.slice('--package='.length))
  30. )
  31. const selectedIds = new Set(
  32. args
  33. .filter(arg => arg.startsWith('--id='))
  34. .map(arg => arg.slice('--id='.length))
  35. )
  36. const wrapperManifest = await readJson(join(sourceRoot, 'ecosystem', 'wrappers.json'))
  37. const corePackage = wrapperManifest.corePackage
  38. const readmeTemplate = await readJson(join(sourceRoot, 'ecosystem', 'wrapper-readme-template.json'))
  39. const formatModule = await loadTypescriptModule(join(sourceRoot, 'packages/core/src/registry/formats.ts'))
  40. const rendererCount = formatModule.DEFAULT_RENDERER_DEFINITIONS.length
  41. const extensionCount = formatModule.DEFAULT_SUPPORTED_EXTENSIONS.length
  42. const deprecatedFixedPriceTerms = [`${4}.${99}`, `${6}.${22}`]
  43. const historicalPackageNames = new Set(
  44. wrapperManifest.wrappers.flatMap(wrapper => wrapper.historicalPackages)
  45. )
  46. const currentSourceBranch = runGit(['branch', '--show-current'])
  47. const currentSourceCommit = runGit(['rev-parse', '--short', 'HEAD'])
  48. async function readJson(path) {
  49. return JSON.parse(await readFile(path, 'utf8'))
  50. }
  51. async function loadTypescriptModule(path) {
  52. const source = await readFile(path, 'utf8')
  53. const transpiled = ts.transpileModule(source, {
  54. compilerOptions: {
  55. module: ts.ModuleKind.CommonJS,
  56. target: ts.ScriptTarget.ES2020
  57. },
  58. fileName: path
  59. })
  60. const module = { exports: {} }
  61. const sandbox = {
  62. exports: module.exports,
  63. module,
  64. require(specifier) {
  65. throw new Error(`Unexpected module import while reading ${path}: ${specifier}`)
  66. }
  67. }
  68. vm.runInNewContext(transpiled.outputText, sandbox, { filename: path })
  69. return module.exports
  70. }
  71. function runGit(commandArgs) {
  72. const result = spawnSync('git', commandArgs, {
  73. cwd: sourceRoot,
  74. encoding: 'utf8',
  75. stdio: 'pipe'
  76. })
  77. if (result.status !== 0) {
  78. throw new Error(`Command failed: git ${commandArgs.join(' ')}\n${result.stderr || result.stdout}`)
  79. }
  80. return result.stdout.trim()
  81. }
  82. async function assertDirectory(path, label = path) {
  83. if (!existsSync(path)) {
  84. throw new Error(`Missing directory: ${label}`)
  85. }
  86. const info = await stat(path)
  87. if (!info.isDirectory()) {
  88. throw new Error(`Not a directory: ${label}`)
  89. }
  90. }
  91. async function assertFile(path, label = path) {
  92. if (!existsSync(path)) {
  93. throw new Error(`Missing file: ${label}`)
  94. }
  95. const info = await stat(path)
  96. if (!info.isFile()) {
  97. throw new Error(`Not a file: ${label}`)
  98. }
  99. }
  100. function assertIncludes(text, expected, label) {
  101. if (!text.includes(expected)) {
  102. throw new Error(`${label} does not include ${expected}`)
  103. }
  104. }
  105. function assertNotIncludes(text, forbidden, label) {
  106. if (text.includes(forbidden)) {
  107. throw new Error(`${label} unexpectedly includes ${forbidden}`)
  108. }
  109. }
  110. const monorepoOnlyForbiddenPatterns = [
  111. '../../scripts/',
  112. '../..\\scripts\\',
  113. 'pnpm --dir',
  114. 'npm --prefix ../..',
  115. 'yarn --cwd ../..'
  116. ]
  117. const exportedRepoForbiddenPatterns = [
  118. ...monorepoOnlyForbiddenPatterns,
  119. 'workspace:'
  120. ]
  121. function verifyStandalonePortableText(text, label, patterns = monorepoOnlyForbiddenPatterns) {
  122. for (const pattern of patterns) {
  123. assertNotIncludes(text, pattern, label)
  124. }
  125. }
  126. function shouldVerifyExportedPortableText(relativePath) {
  127. return !relativePath.startsWith('viewer/')
  128. }
  129. function dependencyBlocks(packageJson) {
  130. return [
  131. packageJson.dependencies,
  132. packageJson.devDependencies,
  133. packageJson.peerDependencies,
  134. packageJson.optionalDependencies
  135. ].filter(Boolean)
  136. }
  137. function verifyNoHistoricalPackageDependency(packageJson, label) {
  138. for (const block of dependencyBlocks(packageJson)) {
  139. for (const dependencyName of Object.keys(block)) {
  140. if (historicalPackageNames.has(dependencyName)) {
  141. throw new Error(`${label} must not depend on historical compatibility package ${dependencyName}`)
  142. }
  143. }
  144. }
  145. }
  146. function verifyWrapperEntryFormats(wrapper, packageJson, label) {
  147. if (!Array.isArray(wrapper.entryFormats) || !wrapper.entryFormats.length) {
  148. throw new Error(`${label} wrapper manifest must declare entryFormats`)
  149. }
  150. for (const format of wrapper.entryFormats) {
  151. if (!allowedEntryFormats.has(format)) {
  152. throw new Error(`${label} wrapper manifest has unsupported entry format: ${format}`)
  153. }
  154. }
  155. if (wrapper.entryFormats.includes('esm')) {
  156. if (!packageJson.module || !packageJson.exports?.['.']?.import) {
  157. throw new Error(`${label} declares ESM but package.json is missing module or exports["."].import`)
  158. }
  159. }
  160. if (wrapper.entryFormats.includes('types')) {
  161. if (!packageJson.types || !packageJson.exports?.['.']?.types) {
  162. throw new Error(`${label} declares type declarations but package.json is missing types or exports["."].types`)
  163. }
  164. }
  165. if (wrapper.entryFormats.includes('iife')) {
  166. if (!packageJson.unpkg || !packageJson.jsdelivr) {
  167. throw new Error(`${label} declares IIFE but package.json is missing unpkg/jsdelivr browser bundle metadata`)
  168. }
  169. }
  170. if (wrapper.entryFormats.includes('viewer-assets')) {
  171. if (!packageJson.exports?.['./viewer/*']) {
  172. throw new Error(`${label} declares bundled viewer assets but package.json is missing exports["./viewer/*"]`)
  173. }
  174. if (!packageJson.files?.includes('viewer')) {
  175. throw new Error(`${label} declares bundled viewer assets but package.json files does not include viewer`)
  176. }
  177. }
  178. if (wrapper.entryFormats.includes('copy-assets-cli')) {
  179. if (!packageJson.bin?.['file-viewer-copy-assets']) {
  180. throw new Error(`${label} declares asset copy CLI but package.json is missing bin.file-viewer-copy-assets`)
  181. }
  182. }
  183. if (wrapper.entryFormats.includes('svelte-component')) {
  184. if (!packageJson.exports?.['.']?.svelte || !String(packageJson.exports['.'].svelte).endsWith('.svelte')) {
  185. throw new Error(`${label} declares Svelte component but exports["."].svelte is missing`)
  186. }
  187. }
  188. }
  189. function isParentPath(value) {
  190. return typeof value === 'string' && (
  191. value.startsWith('../') ||
  192. value.startsWith('..\\') ||
  193. value.includes('/../') ||
  194. value.includes('\\..\\')
  195. )
  196. }
  197. const sourceBoundaryExtensions = new Set([
  198. '.js',
  199. '.mjs',
  200. '.ts',
  201. '.tsx',
  202. '.svelte',
  203. '.json'
  204. ])
  205. function extensionOf(path) {
  206. const dotIndex = path.lastIndexOf('.')
  207. return dotIndex >= 0 ? path.slice(dotIndex) : ''
  208. }
  209. function escapeRegExp(value) {
  210. return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  211. }
  212. function hasPackageReference(content, packageName) {
  213. const escaped = escapeRegExp(packageName)
  214. return new RegExp(`(^|[^@\\w./-])['"]${escaped}['"]`, 'm').test(content) ||
  215. new RegExp(`['"]${escaped}['"]\\s*:`, 'm').test(content)
  216. }
  217. function shouldVerifyBoundaryFile(relativePath) {
  218. if (
  219. relativePath === 'README.md' ||
  220. relativePath === 'README.en.md' ||
  221. relativePath === 'wrapper-repo-manifest.json' ||
  222. relativePath.startsWith('viewer/')
  223. ) {
  224. return false
  225. }
  226. return (
  227. relativePath === 'package.json' ||
  228. relativePath === 'tsconfig.json' ||
  229. relativePath.startsWith('src/') ||
  230. relativePath.startsWith('scripts/')
  231. ) && sourceBoundaryExtensions.has(extensionOf(relativePath))
  232. }
  233. async function verifyNoHistoricalPackageReferences(dir, label) {
  234. const files = await readAllFiles(dir)
  235. for (const file of files) {
  236. const relativePath = file.slice(dir.length + 1)
  237. if (!shouldVerifyBoundaryFile(relativePath)) {
  238. continue
  239. }
  240. const content = await readFile(file, 'utf8').catch(() => '')
  241. for (const historicalPackage of historicalPackageNames) {
  242. if (hasPackageReference(content, historicalPackage)) {
  243. throw new Error(`${label}/${relativePath} must not reference historical compatibility package ${historicalPackage}`)
  244. }
  245. }
  246. }
  247. }
  248. function collectExportEntrypoints(value, paths = new Set()) {
  249. if (!value) {
  250. return paths
  251. }
  252. if (typeof value === 'string') {
  253. paths.add(value)
  254. return paths
  255. }
  256. if (Array.isArray(value)) {
  257. for (const item of value) {
  258. collectExportEntrypoints(item, paths)
  259. }
  260. return paths
  261. }
  262. if (typeof value === 'object') {
  263. for (const item of Object.values(value)) {
  264. collectExportEntrypoints(item, paths)
  265. }
  266. }
  267. return paths
  268. }
  269. function collectPackageEntrypoints(packageJson) {
  270. const entrypoints = new Set()
  271. for (const field of ['main', 'module', 'browser', 'types', 'svelte']) {
  272. if (typeof packageJson[field] === 'string') {
  273. entrypoints.add(packageJson[field])
  274. }
  275. }
  276. for (const item of collectExportEntrypoints(packageJson.exports)) {
  277. entrypoints.add(item)
  278. }
  279. return [...entrypoints].filter(entrypoint =>
  280. !entrypoint.startsWith('/') &&
  281. !entrypoint.includes(':') &&
  282. !entrypoint.includes('*') &&
  283. !entrypoint.startsWith('#') &&
  284. entrypoint !== './' &&
  285. entrypoint !== '.'
  286. )
  287. }
  288. async function verifyPackageEntrypointMetadata(dir, packageJson, label) {
  289. for (const field of ['main', 'module', 'types']) {
  290. if (!packageJson[field]) {
  291. throw new Error(`${label} package.json is missing ${field}`)
  292. }
  293. }
  294. if (!packageJson.exports?.['.']) {
  295. throw new Error(`${label} package.json is missing exports["."]`)
  296. }
  297. for (const entrypoint of collectPackageEntrypoints(packageJson)) {
  298. if (entrypoint.includes('/dist/') || entrypoint.startsWith('dist/')) {
  299. continue
  300. }
  301. await assertFile(join(dir, entrypoint), `${label} package entry ${entrypoint}`)
  302. }
  303. }
  304. async function verifyExportedTsConfig(repoDir, label) {
  305. const tsconfigPath = join(repoDir, 'tsconfig.json')
  306. if (!existsSync(tsconfigPath)) {
  307. return
  308. }
  309. const tsconfig = await readJson(tsconfigPath)
  310. const paths = tsconfig.compilerOptions?.paths || {}
  311. for (const [alias, targets] of Object.entries(paths)) {
  312. const targetList = Array.isArray(targets) ? targets : [targets]
  313. for (const target of targetList) {
  314. if (isParentPath(target)) {
  315. throw new Error(`${label} tsconfig paths.${alias} still points outside the standalone repo: ${target}`)
  316. }
  317. }
  318. }
  319. for (const reference of tsconfig.references || []) {
  320. if (isParentPath(reference?.path)) {
  321. throw new Error(`${label} tsconfig reference still points outside the standalone repo: ${reference.path}`)
  322. }
  323. }
  324. }
  325. async function verifyExportedWebViewerAssets(repoDir, label) {
  326. const viewerDir = join(repoDir, 'viewer')
  327. await assertDirectory(viewerDir, `${label}/viewer`)
  328. for (const file of [
  329. 'flyfish-viewer-assets.json',
  330. 'flyfish-viewer-manifest.json'
  331. ]) {
  332. await assertFile(join(viewerDir, file), `${label}/viewer/${file}`)
  333. }
  334. await assertDirectory(join(viewerDir, 'wasm'), `${label}/viewer/wasm`)
  335. }
  336. async function verifyReadmePair(dir, wrapper, label) {
  337. if (readmeTemplate.schemaVersion !== 1) {
  338. throw new Error('ecosystem/wrapper-readme-template.json schemaVersion must be 1')
  339. }
  340. const zhPath = join(dir, 'README.md')
  341. const enPath = join(dir, 'README.en.md')
  342. await assertFile(zhPath, `${label} README.md`)
  343. await assertFile(enPath, `${label} README.en.md`)
  344. const zh = await readFile(zhPath, 'utf8')
  345. const en = await readFile(enPath, 'utf8')
  346. for (const [locale, readme] of [['zh', zh], ['en', en]]) {
  347. const template = readmeTemplate.locales[locale]
  348. const readmeLabel = `${label} ${locale} README`
  349. assertIncludes(readme, wrapper.packageName, readmeLabel)
  350. assertIncludes(readme, wrapper.github, readmeLabel)
  351. assertIncludes(readme, wrapper.gitee, readmeLabel)
  352. assertIncludes(readme, wrapperManifest.corePackage.packageName, readmeLabel)
  353. assertIncludes(readme, readmeTemplate.markers.wrapperGenerated.start, readmeLabel)
  354. assertIncludes(readme, readmeTemplate.markers.wrapperGenerated.end, readmeLabel)
  355. assertIncludes(readme, template.wrapperEcosystemHeading, readmeLabel)
  356. assertIncludes(readme, template.wrapperFormatHeading, readmeLabel)
  357. for (const header of [
  358. ...template.wrapperMatrixHeaders,
  359. ...template.formatMatrixHeaders
  360. ]) {
  361. assertIncludes(readme, header, readmeLabel)
  362. }
  363. for (const requiredLink of readmeTemplate.requiredLinks) {
  364. assertIncludes(readme, requiredLink, readmeLabel)
  365. }
  366. for (const requiredTerm of readmeTemplate.requiredTerms) {
  367. assertIncludes(readme, requiredTerm, readmeLabel)
  368. }
  369. assertIncludes(readme, String(rendererCount), readmeLabel)
  370. assertIncludes(readme, String(extensionCount), readmeLabel)
  371. for (const deprecatedPriceTerm of deprecatedFixedPriceTerms) {
  372. assertNotIncludes(readme, deprecatedPriceTerm, readmeLabel)
  373. }
  374. for (const ecosystemWrapper of wrapperManifest.wrappers) {
  375. assertIncludes(readme, ecosystemWrapper.packageName, readmeLabel)
  376. assertIncludes(readme, ecosystemWrapper.github, readmeLabel)
  377. assertIncludes(readme, ecosystemWrapper.gitee, readmeLabel)
  378. const labels = entryFormatLabels(locale)
  379. for (const format of ecosystemWrapper.entryFormats || []) {
  380. assertIncludes(readme, labels[format] || format, readmeLabel)
  381. }
  382. }
  383. for (const historicalPackage of wrapper.historicalPackages) {
  384. assertIncludes(readme, historicalPackage, readmeLabel)
  385. }
  386. }
  387. assertIncludes(zh, 'npm install', `${label} README.md`)
  388. assertIncludes(en, 'npm install', `${label} README.en.md`)
  389. }
  390. async function verifyCoreReadmePair(dir, label) {
  391. const zhPath = join(dir, 'README.md')
  392. const enPath = join(dir, 'README.en.md')
  393. await assertFile(zhPath, `${label} README.md`)
  394. await assertFile(enPath, `${label} README.en.md`)
  395. const zh = await readFile(zhPath, 'utf8')
  396. const en = await readFile(enPath, 'utf8')
  397. for (const [locale, readme] of [['zh', zh], ['en', en]]) {
  398. const readmeLabel = `${label} ${locale} README`
  399. assertIncludes(readme, corePackage.packageName, readmeLabel)
  400. assertIncludes(readme, corePackage.github, readmeLabel)
  401. assertIncludes(readme, corePackage.gitee, readmeLabel)
  402. assertIncludes(readme, 'https://doc.file-viewer.app/', readmeLabel)
  403. assertIncludes(readme, 'https://demo.file-viewer.app/', readmeLabel)
  404. assertIncludes(readme, 'createViewer', readmeLabel)
  405. assertIncludes(readme, 'headless', readmeLabel)
  406. assertIncludes(readme, 'browser', readmeLabel)
  407. for (const deprecatedPriceTerm of deprecatedFixedPriceTerms) {
  408. assertNotIncludes(readme, deprecatedPriceTerm, readmeLabel)
  409. }
  410. }
  411. }
  412. async function verifyCorePackageDir() {
  413. const packageDir = join(sourceRoot, 'packages', 'core')
  414. await assertDirectory(packageDir, 'packages/core')
  415. await verifyCoreReadmePair(packageDir, 'packages/core')
  416. await assertFile(join(packageDir, 'src', 'index.ts'), 'packages/core/src/index.ts')
  417. const packageJson = await readJson(join(packageDir, 'package.json'))
  418. if (packageJson.name !== corePackage.packageName) {
  419. throw new Error(`packages/core package name mismatch: ${packageJson.name} !== ${corePackage.packageName}`)
  420. }
  421. if (packageJson.private === true) {
  422. throw new Error('packages/core must be publishable, but package.json has private=true')
  423. }
  424. verifyNoHistoricalPackageDependency(packageJson, 'packages/core/package.json')
  425. verifyStandalonePortableText(JSON.stringify(packageJson), 'packages/core/package.json')
  426. await verifyPackageEntrypointMetadata(packageDir, packageJson, 'packages/core')
  427. }
  428. async function verifyPackageDir(wrapper) {
  429. const packageDir = join(sourceRoot, wrapper.packageDir)
  430. await assertDirectory(packageDir, wrapper.packageDir)
  431. await verifyReadmePair(packageDir, wrapper, wrapper.packageDir)
  432. const packageJsonPath = join(packageDir, 'package.json')
  433. await assertFile(packageJsonPath, `${wrapper.packageDir}/package.json`)
  434. const packageJson = await readJson(packageJsonPath)
  435. if (packageJson.name !== wrapper.packageName) {
  436. throw new Error(`${wrapper.packageDir} package name mismatch: ${packageJson.name} !== ${wrapper.packageName}`)
  437. }
  438. if (packageJson.private === true) {
  439. throw new Error(`${wrapper.packageDir} must be publishable, but package.json has private=true`)
  440. }
  441. verifyNoHistoricalPackageDependency(packageJson, `${wrapper.packageDir}/package.json`)
  442. verifyWrapperEntryFormats(wrapper, packageJson, `${wrapper.packageDir}/package.json`)
  443. await verifyNoHistoricalPackageReferences(packageDir, wrapper.packageDir)
  444. verifyStandalonePortableText(JSON.stringify(packageJson), `${wrapper.packageDir}/package.json`)
  445. await verifyPackageEntrypointMetadata(packageDir, packageJson, wrapper.packageDir)
  446. }
  447. async function readAllFiles(dir) {
  448. const entries = await readdir(dir, { withFileTypes: true })
  449. const files = []
  450. for (const entry of entries) {
  451. const path = join(dir, entry.name)
  452. if (entry.isDirectory()) {
  453. files.push(...await readAllFiles(path))
  454. } else if (entry.isFile()) {
  455. files.push(path)
  456. }
  457. }
  458. return files
  459. }
  460. async function verifyExportedRepo(wrapper) {
  461. const repoDir = join(outputRoot, wrapper.repository)
  462. await assertDirectory(repoDir, wrapper.repository)
  463. await verifyReadmePair(repoDir, wrapper, wrapper.repository)
  464. await assertFile(join(repoDir, 'LICENSE'), `${wrapper.repository}/LICENSE`)
  465. await assertFile(join(repoDir, '.gitignore'), `${wrapper.repository}/.gitignore`)
  466. await assertFile(join(repoDir, 'wrapper-repo-manifest.json'), `${wrapper.repository}/wrapper-repo-manifest.json`)
  467. const packageJson = await readJson(join(repoDir, 'package.json'))
  468. if (packageJson.name !== wrapper.packageName) {
  469. throw new Error(`${wrapper.repository} package name mismatch: ${packageJson.name} !== ${wrapper.packageName}`)
  470. }
  471. if (packageJson.private !== false) {
  472. throw new Error(`${wrapper.repository} package.json must set private=false`)
  473. }
  474. if (packageJson.repository?.url !== `git+${wrapper.github}.git`) {
  475. throw new Error(`${wrapper.repository} repository URL mismatch`)
  476. }
  477. if (packageJson.homepage !== wrapper.github) {
  478. throw new Error(`${wrapper.repository} homepage mismatch`)
  479. }
  480. if (packageJson.bugs?.url !== `${wrapper.github}/issues`) {
  481. throw new Error(`${wrapper.repository} bugs URL mismatch`)
  482. }
  483. await verifyExportedTsConfig(repoDir, wrapper.repository)
  484. if (wrapper.id === 'web') {
  485. await verifyExportedWebViewerAssets(repoDir, wrapper.repository)
  486. }
  487. await verifyPackageEntrypointMetadata(repoDir, packageJson, wrapper.repository)
  488. verifyNoHistoricalPackageDependency(packageJson, `${wrapper.repository}/package.json`)
  489. verifyWrapperEntryFormats(wrapper, packageJson, `${wrapper.repository}/package.json`)
  490. await verifyNoHistoricalPackageReferences(repoDir, wrapper.repository)
  491. for (const block of dependencyBlocks(packageJson)) {
  492. for (const [dependencyName, range] of Object.entries(block)) {
  493. if (String(range).includes('workspace:')) {
  494. throw new Error(`${wrapper.repository} dependency ${dependencyName} still uses ${range}`)
  495. }
  496. }
  497. }
  498. const standaloneManifest = await readJson(join(repoDir, 'wrapper-repo-manifest.json'))
  499. for (const [key, expected] of Object.entries({
  500. sourceBranch: currentSourceBranch,
  501. sourceCommit: currentSourceCommit,
  502. organization: wrapperManifest.organization,
  503. framework: wrapper.framework,
  504. packageName: wrapper.packageName,
  505. repository: wrapper.repository,
  506. github: wrapper.github,
  507. gitee: wrapper.gitee,
  508. entryFormats: JSON.stringify(wrapper.entryFormats),
  509. sourcePackageDir: wrapper.packageDir,
  510. corePackage: wrapperManifest.corePackage.packageName,
  511. coreSourceVisibility: wrapperManifest.corePackage.visibility
  512. })) {
  513. const actual = key === 'entryFormats' ? JSON.stringify(standaloneManifest.entryFormats) : standaloneManifest[key]
  514. if (actual !== expected) {
  515. throw new Error(`${wrapper.repository} manifest ${key} mismatch: ${standaloneManifest[key]} !== ${expected}`)
  516. }
  517. }
  518. const allFiles = await readAllFiles(repoDir)
  519. for (const file of allFiles) {
  520. const relativePath = file.slice(repoDir.length + 1)
  521. if (
  522. relativePath.includes('node_modules/') ||
  523. relativePath.startsWith('dist/') ||
  524. relativePath.startsWith('packages/')
  525. ) {
  526. throw new Error(`${wrapper.repository} exported build/dependency output leaked: ${relativePath}`)
  527. }
  528. if (shouldVerifyExportedPortableText(relativePath)) {
  529. const content = await readFile(file, 'utf8').catch(() => '')
  530. verifyStandalonePortableText(content, `${wrapper.repository}/${relativePath}`, exportedRepoForbiddenPatterns)
  531. }
  532. }
  533. }
  534. async function verifyExportedCoreRepo() {
  535. const repoDir = join(outputRoot, corePackage.repository)
  536. await assertDirectory(repoDir, corePackage.repository)
  537. await verifyCoreReadmePair(repoDir, corePackage.repository)
  538. await assertFile(join(repoDir, 'LICENSE'), `${corePackage.repository}/LICENSE`)
  539. await assertFile(join(repoDir, '.gitignore'), `${corePackage.repository}/.gitignore`)
  540. await assertFile(join(repoDir, 'package-repo-manifest.json'), `${corePackage.repository}/package-repo-manifest.json`)
  541. await assertFile(join(repoDir, 'src', 'index.ts'), `${corePackage.repository}/src/index.ts`)
  542. await assertFile(join(repoDir, 'scripts', 'clean-dist.mjs'), `${corePackage.repository}/scripts/clean-dist.mjs`)
  543. await assertFile(join(repoDir, 'scripts', 'fix-core-esm-extensions.mjs'), `${corePackage.repository}/scripts/fix-core-esm-extensions.mjs`)
  544. const packageJson = await readJson(join(repoDir, 'package.json'))
  545. if (packageJson.name !== corePackage.packageName) {
  546. throw new Error(`${corePackage.repository} package name mismatch: ${packageJson.name} !== ${corePackage.packageName}`)
  547. }
  548. if (packageJson.private !== false) {
  549. throw new Error(`${corePackage.repository} package.json must set private=false`)
  550. }
  551. if (packageJson.repository?.url !== `git+${corePackage.github}.git`) {
  552. throw new Error(`${corePackage.repository} repository URL mismatch`)
  553. }
  554. if (packageJson.homepage !== corePackage.github) {
  555. throw new Error(`${corePackage.repository} homepage mismatch`)
  556. }
  557. if (packageJson.bugs?.url !== `${corePackage.github}/issues`) {
  558. throw new Error(`${corePackage.repository} bugs URL mismatch`)
  559. }
  560. await verifyPackageEntrypointMetadata(repoDir, packageJson, corePackage.repository)
  561. verifyNoHistoricalPackageDependency(packageJson, `${corePackage.repository}/package.json`)
  562. await verifyNoHistoricalPackageReferences(repoDir, corePackage.repository)
  563. for (const block of dependencyBlocks(packageJson)) {
  564. for (const [dependencyName, range] of Object.entries(block)) {
  565. if (String(range).includes('workspace:')) {
  566. throw new Error(`${corePackage.repository} dependency ${dependencyName} still uses ${range}`)
  567. }
  568. }
  569. }
  570. const standaloneManifest = await readJson(join(repoDir, 'package-repo-manifest.json'))
  571. for (const [key, expected] of Object.entries({
  572. sourceBranch: currentSourceBranch,
  573. sourceCommit: currentSourceCommit,
  574. organization: wrapperManifest.organization,
  575. packageName: corePackage.packageName,
  576. repository: corePackage.repository,
  577. github: corePackage.github,
  578. gitee: corePackage.gitee,
  579. aggregateRepository: corePackage.aggregateRepository,
  580. sourcePackageDir: 'packages/core'
  581. })) {
  582. const actual = standaloneManifest[key]
  583. if (actual !== expected) {
  584. throw new Error(`${corePackage.repository} manifest ${key} mismatch: ${actual} !== ${expected}`)
  585. }
  586. }
  587. const allFiles = await readAllFiles(repoDir)
  588. for (const file of allFiles) {
  589. const relativePath = file.slice(repoDir.length + 1)
  590. if (
  591. relativePath.includes('node_modules/') ||
  592. relativePath.startsWith('dist/') ||
  593. relativePath.startsWith('packages/')
  594. ) {
  595. throw new Error(`${corePackage.repository} exported build/dependency output leaked: ${relativePath}`)
  596. }
  597. const content = await readFile(file, 'utf8').catch(() => '')
  598. verifyStandalonePortableText(content, `${corePackage.repository}/${relativePath}`, exportedRepoForbiddenPatterns)
  599. }
  600. }
  601. const includeCore = selectedIds.size === 0 && (
  602. selectedPackages.size === 0 ||
  603. selectedPackages.has(corePackage.packageName)
  604. )
  605. const wrappers = wrapperManifest.wrappers.filter(wrapper => {
  606. if (selectedPackages.size && !selectedPackages.has(wrapper.packageName)) {
  607. return false
  608. }
  609. if (selectedIds.size && !selectedIds.has(wrapper.id)) {
  610. return false
  611. }
  612. return true
  613. })
  614. if (!includeCore && !wrappers.length) {
  615. throw new Error('No core or component packages selected for verification.')
  616. }
  617. let verifiedCount = 0
  618. if (includeCore) {
  619. await verifyCorePackageDir()
  620. if (!sourceOnly) {
  621. await verifyExportedCoreRepo()
  622. }
  623. verifiedCount += 1
  624. console.log(`Verified ${corePackage.packageName}`)
  625. }
  626. for (const wrapper of wrappers) {
  627. await verifyPackageDir(wrapper)
  628. if (!sourceOnly) {
  629. await verifyExportedRepo(wrapper)
  630. }
  631. verifiedCount += 1
  632. console.log(`Verified ${wrapper.packageName}`)
  633. }
  634. console.log(
  635. `Verified ${verifiedCount} core/component package${verifiedCount === 1 ? '' : 's'}${sourceOnly ? '' : ` and standalone exports in ${outputRoot}`}.`
  636. )