import os from 'node:os'; import { setTimeout } from 'node:timers/promises'; import { rm } from 'node:fs/promises'; import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg'; import { Pointcut } from '@eggjs/tegg/aop'; import type { EggHttpClient } from 'egg'; import { isEqual, isEmpty } from 'lodash-es'; import semver from 'semver'; import { BadRequestError } from 'egg-errors'; import type { NPMRegistry, RegistryResponse, } from '../../common/adapter/NPMRegistry.js'; import { detectInstallScript, getScopeAndName, } from '../../common/PackageUtil.js'; import { downloadToTempfile } from '../../common/FileUtil.js'; import { TaskState, TaskType } from '../../common/enum/Task.js'; import { AbstractService } from '../../common/AbstractService.js'; import type { TaskRepository } from '../../repository/TaskRepository.js'; import type { PackageJSONType, PackageManifestType, PackageRepository, } from '../../repository/PackageRepository.js'; import type { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository.js'; import type { UserRepository } from '../../repository/UserRepository.js'; import type { SyncPackageTaskOptions, CreateSyncPackageTask, } from '../entity/Task.js'; import { Task } from '../entity/Task.js'; import type { Package } from '../entity/Package.js'; import type { UserService } from './UserService.js'; import type { TaskService } from './TaskService.js'; import type { PackageManagerService } from './PackageManagerService.js'; import type { CacheService } from './CacheService.js'; import type { User } from '../entity/User.js'; import type { RegistryManagerService } from './RegistryManagerService.js'; import type { Registry } from '../entity/Registry.js'; import type { ScopeManagerService } from './ScopeManagerService.js'; import { EventCorkAdvice } from './EventCorkerAdvice.js'; import { PresetRegistryName, SyncDeleteMode } from '../../common/constants.js'; type syncDeletePkgOptions = { task: Task; pkg: Package | null; logUrl: string; url: string; logs: string[]; data: any; }; function isoNow() { return new Date().toISOString(); } export class RegistryNotMatchError extends BadRequestError {} @SingletonProto({ accessLevel: AccessLevel.PUBLIC, }) export class PackageSyncerService extends AbstractService { @Inject() private readonly taskRepository: TaskRepository; @Inject() private readonly packageRepository: PackageRepository; @Inject() private readonly packageVersionDownloadRepository: PackageVersionDownloadRepository; @Inject() private readonly userRepository: UserRepository; @Inject() private readonly npmRegistry: NPMRegistry; @Inject() private readonly userService: UserService; @Inject() private readonly taskService: TaskService; @Inject() private readonly packageManagerService: PackageManagerService; @Inject() private readonly cacheService: CacheService; @Inject() private readonly httpclient: EggHttpClient; @Inject() private readonly registryManagerService: RegistryManagerService; @Inject() private readonly scopeManagerService: ScopeManagerService; public async createTask(fullname: string, options?: SyncPackageTaskOptions) { const [scope, name] = getScopeAndName(fullname); const pkg = await this.packageRepository.findPackage(scope, name); // sync task request registry is not same as package registry if (pkg && pkg.registryId && options?.registryId) { if (pkg.registryId !== options.registryId) { throw new RegistryNotMatchError( `package ${fullname} is not in registry ${options.registryId}` ); } } return await this.taskService.createTask( Task.createSyncPackage(fullname, options), true ); } public async findTask(taskId: string) { return await this.taskService.findTask(taskId); } public async findTaskLog(task: Task) { return await this.taskService.findTaskLog(task); } public async findExecuteTask() { return (await this.taskService.findExecuteTask( TaskType.SyncPackage )) as CreateSyncPackageTask; } public get allowSyncDownloadData() { const config = this.config.cnpmcore; if ( config.enableSyncDownloadData && config.syncDownloadDataSourceRegistry && config.syncDownloadDataMaxDate ) { return true; } return false; } private async syncDownloadData(task: Task, pkg: Package) { if (!this.allowSyncDownloadData) { return; } const fullname = pkg.fullname; const start = '2011-01-01'; const end = this.config.cnpmcore.syncDownloadDataMaxDate; const registry = this.config.cnpmcore.syncDownloadDataSourceRegistry; const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry); const logs: string[] = []; let downloads: { day: string; downloads: number }[]; logs.push( `[${isoNow()}][DownloadData] ๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง Syncing "${fullname}" download data "${start}:${end}" on ${registry} ๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง` ); const failEnd = 'โŒโŒโŒโŒโŒ ๐Ÿšฎ give up ๐Ÿšฎ โŒโŒโŒโŒโŒ'; try { const { data, status, res } = await this.npmRegistry.getDownloadRanges( registry, fullname, start, end, { remoteAuthToken } ); downloads = data.downloads || []; logs.push( `[${isoNow()}][DownloadData] ๐Ÿšง HTTP [${status}] timing: ${JSON.stringify(res.timing)}, downloads: ${downloads.length}` ); } catch (err: any) { const status = err.status || 'unknow'; logs.push( `[${isoNow()}][DownloadData] โŒ Get download data error: ${err}, status: ${status}` ); logs.push(`[${isoNow()}][DownloadData] ${failEnd}`); await this.taskService.appendTaskLog(task, logs.join('\n')); return; } const datas = new Map(); for (const item of downloads) { // { // "day": "2021-09-21", // "downloads": 45 // }, const day = item.day; const [year, month, date] = day.split('-'); const yearMonth = parseInt(`${year}${month}`); if (!datas.has(yearMonth)) { datas.set(yearMonth, []); } const counters = datas.get(yearMonth); counters!.push([date, item.downloads]); } for (const [yearMonth, counters] of datas.entries()) { await this.packageVersionDownloadRepository.saveSyncDataByMonth( pkg.packageId, yearMonth, counters ); logs.push( `[${isoNow()}][DownloadData] ๐ŸŸข ${yearMonth}: ${counters.length} days` ); } logs.push( `[${isoNow()}][DownloadData] ๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข ${registry}/${fullname} ๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข` ); await this.taskService.appendTaskLog(task, logs.join('\n')); } private async syncUpstream(task: Task) { const registry = this.npmRegistry.registry; const fullname = task.targetName; const remoteAuthToken = await this.registryManagerService.getAuthTokenByRegistryHost(registry); let logs: string[] = []; let logId = ''; logs.push( `[${isoNow()}][UP] ๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง Waiting sync "${fullname}" task on ${registry} ๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง` ); const failEnd = `โŒโŒโŒโŒโŒ Sync ${registry}/${fullname} ๐Ÿšฎ give up ๐Ÿšฎ โŒโŒโŒโŒโŒ`; try { const { data, status, res } = await this.npmRegistry.createSyncTask( fullname, { remoteAuthToken } ); logs.push( `[${isoNow()}][UP] ๐Ÿšง HTTP [${status}] timing: ${JSON.stringify(res.timing)}, data: ${JSON.stringify(data)}` ); logId = data.logId; } catch (err: any) { const status = err.status || 'unknow'; // ๅฏ่ƒฝไผšๆŠ›ๅ‡บ AggregateError ๅผ‚ๅธธ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError logs.push( `[${isoNow()}][UP] โŒ Sync ${fullname} fail, create sync task error: ${err}, status: ${status} ${err instanceof AggregateError ? err.errors : ''}` ); logs.push(`[${isoNow()}][UP] ${failEnd}`); await this.taskService.appendTaskLog(task, logs.join('\n')); return; } if (!logId) { logs.push(`[${isoNow()}][UP] โŒ Sync ${fullname} fail, missing logId`); logs.push(`[${isoNow()}][UP] ${failEnd}`); await this.taskService.appendTaskLog(task, logs.join('\n')); return; } const startTime = Date.now(); const maxTimeout = this.config.cnpmcore.sourceRegistrySyncTimeout; let logUrl = ''; let offset = 0; let useTime = Date.now() - startTime; while (useTime < maxTimeout) { // sleep 1s ~ 6s in random const delay = process.env.NODE_ENV === 'test' ? 100 : 1000 + Math.random() * 5000; await setTimeout(delay); try { const { data, status, url } = await this.npmRegistry.getSyncTask( fullname, logId, offset, { remoteAuthToken } ); useTime = Date.now() - startTime; if (!logUrl) { logUrl = url; } const log = (data && data.log) || ''; offset += log.length; if (data && data.syncDone) { logs.push( `[${isoNow()}][UP] ๐ŸŽ‰ Sync ${fullname} success [${useTime}ms], log: ${logUrl}, offset: ${offset}` ); logs.push(`[${isoNow()}][UP] ๐Ÿ”— ${registry}/${fullname}`); await this.taskService.appendTaskLog(task, logs.join('\n')); return; } logs.push( `[${isoNow()}][UP] ๐Ÿšง HTTP [${status}] [${useTime}ms], offset: ${offset}` ); await this.taskService.appendTaskLog(task, logs.join('\n')); logs = []; } catch (err: any) { useTime = Date.now() - startTime; const status = err.status || 'unknow'; logs.push( `[${isoNow()}][UP] ๐Ÿšง HTTP [${status}] [${useTime}ms] error: ${err}` ); } } // timeout logs.push( `[${isoNow()}][UP] โŒ Sync ${fullname} fail, timeout, log: ${logUrl}, offset: ${offset}` ); logs.push(`[${isoNow()}][UP] ${failEnd}`); await this.taskService.appendTaskLog(task, logs.join('\n')); } private isRemovedInRemote(remoteFetchResult: RegistryResponse) { const { status, data } = remoteFetchResult; // deleted or blocked if (status === 451) { return true; } const hasMaintainers = data?.maintainers && data?.maintainers.length !== 0; if (hasMaintainers) { return false; } // unpublished const timeMap = data.time || {}; if (timeMap.unpublished) { return true; } // security holder // test/fixtures/registry.npmjs.org/security-holding-package.json // { // "_id": "xxx", // "_rev": "9-a740a77bcd978abeec47d2d027bf688c", // "name": "xxx", // "time": { // "modified": "2017-11-28T00:45:24.162Z", // "created": "2013-09-20T23:25:18.122Z", // "0.0.0": "2013-09-20T23:25:20.242Z", // "1.0.0": "2016-06-22T00:07:41.958Z", // "0.0.1-security": "2016-12-15T01:03:58.663Z", // "unpublished": { // "time": "2017-11-28T00:45:24.163Z", // "versions": [] // } // }, // "_attachments": {} // } let isSecurityHolder = true; for (const versionInfo of Object.entries<{ _npmUser?: { name: string } }>( data.versions || {} )) { const [v, info] = versionInfo; // >=0.0.1-security <0.0.2-0 const isSecurityVersion = semver.satisfies(v, '^0.0.1-security'); const isNpmUser = info?._npmUser?.name === 'npm'; if (!isSecurityVersion || !isNpmUser) { isSecurityHolder = false; break; } } return isSecurityHolder; } // sync deleted package, deps on the syncDeleteMode // - ignore: do nothing, just finish the task // - delete: remove the package from local registry // - block: block the package, update the manifest.block, instead of delete versions // ๆ นๆฎ syncDeleteMode ้…็ฝฎ๏ผŒๅค„็†ๅˆ ๅŒ…ๅœบๆ™ฏ // - ignore: ไธๅšไปปไฝ•ๅค„็†๏ผŒ็›ดๆŽฅ็ป“ๆŸไปปๅŠก // - delete: ๅˆ ้™คๅŒ…ๆ•ฐๆฎ๏ผŒๅŒ…ๆ‹ฌ manifest ๅญ˜ๅ‚จ // - block: ่ฝฏๅˆ ้™ค ๅฐ†ๅŒ…ๆ ‡่ฎฐไธบ block๏ผŒ็”จๆˆทๆ— ๆณ•็›ดๆŽฅไฝฟ็”จ private async syncDeletePkg({ task, pkg, logUrl, url, logs, data, }: syncDeletePkgOptions) { const fullname = task.targetName; const failEnd = `โŒโŒโŒโŒโŒ ${url || fullname} โŒโŒโŒโŒโŒ`; const syncDeleteMode: SyncDeleteMode = this.config.cnpmcore.syncDeleteMode; logs.push( `[${isoNow()}] ๐ŸŸข Package "${fullname}" was removed in remote registry, response data: ${JSON.stringify(data)}, config.syncDeleteMode = ${syncDeleteMode}` ); // pkg not exists in local registry if (!pkg) { task.error = `Package not exists, response data: ${JSON.stringify(data)}`; logs.push(`[${isoNow()}] โŒ ${task.error}, log: ${logUrl}`); logs.push(`[${isoNow()}] ${failEnd}`); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); this.logger.info( '[PackageSyncerService.executeTask:fail-404] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); return; } if (syncDeleteMode === SyncDeleteMode.ignore) { // ignore deleted package logs.push( `[${isoNow()}] ๐ŸŸข Skip remove since config.syncDeleteMode = ignore` ); } else if (syncDeleteMode === SyncDeleteMode.block) { // block deleted package await this.packageManagerService.blockPackage( pkg, 'Removed in remote registry' ); logs.push( `[${isoNow()}] ๐ŸŸข Block the package since config.syncDeleteMode = block` ); } else if (syncDeleteMode === SyncDeleteMode.delete) { // delete package await this.packageManagerService.unpublishPackage(pkg); logs.push( `[${isoNow()}] ๐ŸŸข Delete the package since config.syncDeleteMode = delete` ); } // update log logs.push(`[${isoNow()}] ๐Ÿ“ Log URL: ${logUrl}`); logs.push(`[${isoNow()}] ๐Ÿ”— ${url}`); await this.taskService.finishTask(task, TaskState.Success, logs.join('\n')); this.logger.info( '[PackageSyncerService.executeTask:remove-package] taskId: %s, targetName: %s', task.taskId, task.targetName ); } // ๅˆๅง‹ๅŒ–ๅฏนๅบ”็š„ Registry // 1. ไผ˜ๅ…ˆไปŽ pkg.registryId ่Žทๅ– (registryId ไธ€็ป่ฎพ็ฝฎ ไธๅบ”ๆ”นๅ˜) // 1. ๅ…ถๆฌกไปŽ task.data.registryId (ๅˆ›ๅปบๅ•ๅŒ…ๅŒๆญฅไปปๅŠกๆ—ถไผ ๅ…ฅ) // 2. ๆŽฅ็€ๆ นๆฎ scope ่ฟ›่กŒ่ฎก็ฎ— (ไฝœไธบๅญๅŒ…ไพ่ต–ๅŒๆญฅๆ—ถๅ€™๏ผŒๆ—  registryId) // 3. ๆœ€ๅŽ่ฟ”ๅ›ž default registryId (ๅฏ่ƒฝ default registry ไนŸไธๅญ˜ๅœจ) public async initSpecRegistry( task: Task, pkg: Package | null = null, scope?: string ): Promise { const registryId = pkg?.registryId || (task.data as SyncPackageTaskOptions).registryId; let targetHost: string = this.config.cnpmcore.sourceRegistry; let registry: Registry | null = null; // ๅฝ“ๅ‰ไปปๅŠกไฝœไธบ deps ๅผ•ๅ…ฅๆ—ถ๏ผŒไธไผš้…็ฝฎ registryId // ๅކๅฒ Task ๅฏ่ƒฝๆฒกๆœ‰้…็ฝฎ registryId if (registryId) { registry = await this.registryManagerService.findByRegistryId(registryId); } else if (scope) { const scopeModel = await this.scopeManagerService.findByName(scope); if (scopeModel?.registryId) { registry = await this.registryManagerService.findByRegistryId( scopeModel?.registryId ); } } // ้‡‡็”จ้ป˜่ฎค็š„ registry if (!registry) { registry = await this.registryManagerService.ensureDefaultRegistry(); } // ๆ›ดๆ–ฐ targetHost ๅœฐๅ€ // defaultRegistry ๅฏ่ƒฝ่ฟ˜ๆœชๅˆ›ๅปบ if (registry?.host) { targetHost = registry.host; } this.npmRegistry.setRegistryHost(targetHost); return registry; } // ็”ฑไบŽ cnpmcore ๅฐ† version ๅ’Œ tag ไฝœไธบไธคไธช็‹ฌ็ซ‹็š„ changes ไบ‹ไปถๅˆ†ๅ‘ // ๆ™ฎ้€š็‰ˆๆœฌๅ‘ๅธƒๆ—ถ๏ผŒ็Ÿญๆ—ถ้—ดๅ†…ไผšๆœ‰ไธคๆก็›ธๅŒ task ่ฟ›่กŒๅŒๆญฅ // ๅฐฝ้‡ไฟ่ฏ่ฏปๅ–ๅ’Œๅ†™ๅ…ฅ้ƒฝ้œ€ไฟ่ฏไปปๅŠกๅน‚็ญ‰๏ผŒ้œ€่ฆ็กฎไฟ changes ๅœจๅŒๆญฅไปปๅŠกๅฎŒๆˆๅŽๅ†่งฆๅ‘ // ้€š่ฟ‡ DB ๅ”ฏไธ€็ดขๅผ•ๆฅไฟ่ฏไปปๅŠกๅน‚็ญ‰๏ผŒๆ’ๅ…ฅๅคฑ่ดฅไธๅฝฑๅ“ pkg.manifests ๆ›ดๆ–ฐ // ้€š่ฟ‡ eventBus.cork/uncork ๆฅๆš‚็ผ“ไบ‹ไปถ่งฆๅ‘ @Pointcut(EventCorkAdvice) public async executeTask(task: Task) { const fullname = task.targetName; const [scope, name] = getScopeAndName(fullname); const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, specificVersions, } = task.data as SyncPackageTaskOptions; let pkg = await this.packageRepository.findPackage(scope, name); const registry = await this.initSpecRegistry(task, pkg, scope); const registryHost = this.npmRegistry.registry; const remoteAuthToken = registry.authToken; let logs: string[] = []; if (tips) { logs.push(`[${isoNow()}] ๐Ÿ‘‰๐Ÿ‘‰๐Ÿ‘‰๐Ÿ‘‰๐Ÿ‘‰ Tips: ${tips} ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ`); } const taskQueueLength = await this.taskService.getTaskQueueLength( task.type ); const taskQueueHighWaterSize = this.config.cnpmcore.taskQueueHighWaterSize; const taskQueueInHighWaterState = taskQueueLength >= taskQueueHighWaterSize; const skipDependencies = taskQueueInHighWaterState ? true : !!originSkipDependencies; const syncUpstream = !!( !taskQueueInHighWaterState && this.config.cnpmcore.sourceRegistryIsCNpm && this.config.cnpmcore.syncUpstreamFirst && registry.name === PresetRegistryName.default ); const logUrl = `${this.config.cnpmcore.registry}/-/package/${fullname}/syncs/${task.taskId}/log`; this.logger.info( '[PackageSyncerService.executeTask:start] taskId: %s, targetName: %s, attempts: %s, taskQueue: %s/%s, syncUpstream: %s, log: %s', task.taskId, task.targetName, task.attempts, taskQueueLength, taskQueueHighWaterSize, syncUpstream, logUrl ); logs.push( `[${isoNow()}] ๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง Syncing from ${registryHost}/${fullname}, skipDependencies: ${skipDependencies}, syncUpstream: ${syncUpstream}, syncDownloadData: ${!!syncDownloadData}, forceSyncHistory: ${!!forceSyncHistory} attempts: ${task.attempts}, worker: "${os.hostname()}/${process.pid}", taskQueue: ${taskQueueLength}/${taskQueueHighWaterSize} ๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง๐Ÿšง` ); if (specificVersions) { logs.push( `[${isoNow()}] ๐Ÿ‘‰ syncing specific versions: ${specificVersions.join(' | ')} ๐Ÿ‘ˆ` ); } logs.push(`[${isoNow()}] ๐Ÿšง log: ${logUrl}`); if (registry?.name === PresetRegistryName.self) { logs.push( `[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} has been published to the self registry, skip sync โŒโŒโŒโŒโŒ` ); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); this.logger.info( '[PackageSyncerService.executeTask:fail] taskId: %s, targetName: %s, invalid registryId', task.taskId, task.targetName ); return; } if (pkg && pkg?.registryId !== registry?.registryId) { if (pkg.registryId) { logs.push( `[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} registry is ${pkg.registryId} not belong to ${registry?.registryId}, skip sync โŒโŒโŒโŒโŒ` ); await this.taskService.finishTask( task, TaskState.Fail, logs.join('\n') ); this.logger.info( '[PackageSyncerService.executeTask:fail] taskId: %s, targetName: %s, invalid registryId', task.taskId, task.targetName ); return; } // ๅคšๅŒๆญฅๆบไน‹ๅ‰ๆฒกๆœ‰ registryId // publish() ็‰ˆๆœฌไธๅ˜ๆ—ถ๏ผŒไธไผšๆ›ดๆ–ฐ registryId // ๅœจๅŒๆญฅๅ‰๏ผŒ่ฟ›่กŒๆ›ดๆ–ฐๆ“ไฝœ pkg.registryId = registry?.registryId; await this.packageRepository.savePackage(pkg); } if (syncDownloadData && pkg) { await this.syncDownloadData(task, pkg); logs.push(`[${isoNow()}] ๐ŸŸข log: ${logUrl}`); logs.push( `[${isoNow()}] ๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข Sync "${fullname}" download data success ๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข` ); await this.taskService.finishTask( task, TaskState.Success, logs.join('\n') ); this.logger.info( '[PackageSyncerService.executeTask:success] taskId: %s, targetName: %s', task.taskId, task.targetName ); return; } if (syncUpstream) { await this.taskService.appendTaskLog(task, logs.join('\n')); logs = []; // create sync task on sourceRegistry and skipDependencies = true await this.syncUpstream(task); } if (this.config.cnpmcore.syncPackageBlockList.includes(fullname)) { task.error = `stop sync by block list: ${JSON.stringify(this.config.cnpmcore.syncPackageBlockList)}`; logs.push(`[${isoNow()}] โŒ ${task.error}, log: ${logUrl}`); logs.push(`[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} โŒโŒโŒโŒโŒ`); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); this.logger.info( '[PackageSyncerService.executeTask:fail-block-list] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); return; } let registryFetchResult: RegistryResponse; try { registryFetchResult = await this.npmRegistry.getFullManifests(fullname, { remoteAuthToken, }); } catch (err: any) { const status = err.status || 'unknown'; task.error = `request manifests error: ${err}, status: ${status}`; logs.push( `[${isoNow()}] โŒ Synced ${fullname} fail, ${task.error}, log: ${logUrl}` ); logs.push(`[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} โŒโŒโŒโŒโŒ`); this.logger.info( '[PackageSyncerService.executeTask:fail-request-error] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); await this.taskService.retryTask(task, logs.join('\n')); return; } const { url, data, headers, res, status } = registryFetchResult; /* c8 ignore next 13 */ if (status >= 500 || !data) { // GET https://registry.npmjs.org/%40modern-js%2Fstyle-compiler?t=1683348626499&cache=0, status: 522 // registry will response status 522 and data will be null // > TypeError: Cannot read properties of null (reading 'readme') task.error = `request manifests response error, status: ${status}, data: ${JSON.stringify(data)}`; logs.push( `[${isoNow()}] โŒ response headers: ${JSON.stringify(headers)}` ); logs.push( `[${isoNow()}] โŒ Synced ${fullname} fail, ${task.error}, log: ${logUrl}` ); logs.push(`[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} โŒโŒโŒโŒโŒ`); this.logger.info( '[PackageSyncerService.executeTask:fail-request-error] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); await this.taskService.retryTask(task, logs.join('\n')); return; } if (status === 404) { // ignore 404 status // https://github.com/node-modules/detect-port/issues/57 task.error = `Package not found, status 404, data: ${JSON.stringify(data)}`; logs.push(`[${isoNow()}] โŒ ${task.error}, log: ${logUrl}`); logs.push( `[${isoNow()}] โŒ Synced ${fullname} fail, ${task.error}, log: ${logUrl}` ); logs.push(`[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} โŒโŒโŒโŒโŒ`); this.logger.info( '[PackageSyncerService.executeTask:fail-request-error] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); return; } let readme = data.readme || ''; if (typeof readme !== 'string') { readme = JSON.stringify(readme); } // "time": { // "created": "2021-03-27T12:30:23.891Z", // "0.0.2": "2021-03-27T12:30:24.349Z", // "modified": "2021-12-08T14:59:57.264Z", const timeMap = data.time || {}; const failEnd = `โŒโŒโŒโŒโŒ ${url || fullname} โŒโŒโŒโŒโŒ`; const contentLength = headers['content-length'] || '-'; logs.push( `[${isoNow()}] HTTP [${status}] content-length: ${contentLength}, timing: ${JSON.stringify(res.timing)}` ); if (this.isRemovedInRemote(registryFetchResult)) { await this.syncDeletePkg({ task, pkg, logs, logUrl, url, data }); return; } const versionMap = data.versions || {}; const distTags = data['dist-tags'] || {}; // show latest information if (distTags.latest) { logs.push( `[${isoNow()}] ๐Ÿ“– ${fullname} latest version: ${distTags.latest ?? '-'}, published time: ${JSON.stringify(timeMap[distTags.latest])}` ); } // 1. save maintainers // maintainers: [ // { name: 'bomsy', email: 'b4bomsy@gmail.com' }, // { name: 'jasonlaster11', email: 'jason.laster.11@gmail.com' } // ], let maintainers = data.maintainers; const maintainersMap: Record = {}; const users: User[] = []; let changedUserCount = 0; if (!Array.isArray(maintainers) || maintainers.length === 0) { // https://r.cnpmjs.org/webpack.js.org/sync/log/61dbc7c8ff747911a5701068 // https://registry.npmjs.org/webpack.js.org // security holding package will not contains maintainers, auto set npm and npm@npmjs.com to maintainer // "description": "security holding package", // "repository": "npm/security-holder" if ( data.description === 'security holding package' || data.repository === 'npm/security-holder' ) { maintainers = data.maintainers = [ { name: 'npm', email: 'npm@npmjs.com' }, ]; } else { // try to use latest tag version's maintainers instead const latestPackageVersion = distTags.latest && versionMap[distTags.latest]; if ( latestPackageVersion && Array.isArray(latestPackageVersion.maintainers) ) { maintainers = latestPackageVersion.maintainers; logs.push( `[${isoNow()}] ๐Ÿ“– Use the latest version(${latestPackageVersion.version}) maintainers instead` ); } } } if (Array.isArray(maintainers) && maintainers.length > 0) { logs.push( `[${isoNow()}] ๐Ÿšง Syncing maintainers: ${JSON.stringify(maintainers)}` ); for (const maintainer of maintainers) { if (maintainer.name && maintainer.email) { maintainersMap[maintainer.name] = maintainer; const { changed, user } = await this.userService.saveUser( registry?.userPrefix, maintainer.name, maintainer.email ); users.push(user); if (changed) { changedUserCount++; logs.push( `[${isoNow()}] ๐ŸŸข [${changedUserCount}] Synced ${maintainer.name} => ${user.name}(${user.userId})` ); } } } } if (users.length === 0) { // check unpublished // https://r.cnpmjs.org/-/package/babel-plugin-autocss/syncs/61e4be46c7cbfac94d2ec597/log // { // "name": "babel-plugin-autocss", // "time": { // "created": "2021-10-29T08:21:56.032Z", // "0.0.1": "2021-10-29T08:21:56.206Z", // "modified": "2022-01-14T12:34:23.941Z", // "unpublished": { // "time": "2022-01-14T12:34:23.941Z", // "versions": [ // "0.0.1" // ] // } // } // } // invalid maintainers, sync fail task.error = `invalid maintainers: ${JSON.stringify(maintainers)}`; logs.push(`[${isoNow()}] โŒ ${task.error}, log: ${logUrl}`); logs.push(`[${isoNow()}] ${failEnd}`); this.logger.info( '[PackageSyncerService.executeTask:fail-invalid-maintainers] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); return; } let lastErrorMessage = ''; const dependenciesSet = new Set(); const { data: existsData } = await this.packageManagerService.listPackageFullManifests(scope, name); const { data: abbreviatedManifests } = await this.packageManagerService.listPackageAbbreviatedManifests( scope, name ); const existsVersionMap = existsData?.versions ?? {}; const existsVersionCount = Object.keys(existsVersionMap).length; const abbreviatedVersionMap = abbreviatedManifests?.versions ?? {}; // 2. save versions if ( specificVersions && !this.config.cnpmcore.strictSyncSpecivicVersion && !specificVersions.includes(distTags.latest) ) { logs.push( `[${isoNow()}] ๐Ÿ“ฆ Add latest tag version "${fullname}: ${distTags.latest}"` ); specificVersions.push(distTags.latest); } const versions = specificVersions ? Object.values(versionMap).filter(verItem => specificVersions.includes(verItem.version) ) : Object.values(versionMap); // ๅ…จ้‡ๅŒๆญฅๆ—ถ่ทณ่ฟ‡ๆŽ’ๅบ const sortedAvailableVersions = specificVersions ? versions.map(item => item.version).sort(semver.rcompare) : []; // ๅœจstrictSyncSpecivicVersionๆจกๅผไธ‹๏ผˆไธๅŒๆญฅlatest๏ผ‰ไธ”ๆ‰€ๆœ‰ไผ ๅ…ฅ็š„versionๅ‡ไธๅฏ็”จ if (specificVersions && sortedAvailableVersions.length === 0) { logs.push(`[${isoNow()}] โŒ `); task.error = 'There is no available specific versions, stop task.'; logs.push(`[${isoNow()}] ${task.error}, log: ${logUrl}`); logs.push(`[${isoNow()}] โŒโŒโŒโŒโŒ ${fullname} โŒโŒโŒโŒโŒ`); this.logger.info( '[PackageSyncerService.executeTask:fail-empty-list] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); return; } if (specificVersions) { // specific versions may not in manifest. const notAvailableVersionList = specificVersions.filter( i => !sortedAvailableVersions.includes(i) ); logs.push( `[${isoNow()}] ๐Ÿšง Syncing specific versions: ${sortedAvailableVersions.join(' | ')}` ); if (notAvailableVersionList.length > 0) { logs.push( `๐Ÿšง Some specific versions are not available: ๐Ÿ‘‰ ${notAvailableVersionList.join(' | ')} ๐Ÿ‘ˆ` ); } } else { logs.push( `[${isoNow()}] ๐Ÿšง Syncing versions ${existsVersionCount} => ${versions.length}` ); } const updateVersions: string[] = []; const differentMetas: [PackageJSONType, Partial][] = []; let syncIndex = 0; for (const item of versions) { const version: string = item.version; if (!version) continue; let existsItem: (typeof existsVersionMap)[string] | undefined = existsVersionMap[version]; let existsAbbreviatedItem: | (typeof abbreviatedVersionMap)[string] | undefined = abbreviatedVersionMap[version]; const shouldDeleteReadme = !!(existsItem && 'readme' in existsItem); if (pkg) { if (existsItem) { // check item on AbbreviatedManifests if (!existsAbbreviatedItem) { updateVersions.push(version); logs.push( `[${isoNow()}] ๐Ÿ› Remote version ${version} not exists on local abbreviated manifests, need to refresh` ); } } if (existsItem && forceSyncHistory === true) { const pkgVer = await this.packageRepository.findPackageVersion( pkg.packageId, version ); if (pkgVer) { logs.push( `[${isoNow()}] ๐Ÿšง [${syncIndex}] Remove version ${version} for force sync history` ); await this.packageManagerService.removePackageVersion( pkg, pkgVer, true ); existsItem = undefined; existsAbbreviatedItem = undefined; existsVersionMap[version] = undefined; abbreviatedVersionMap[version] = undefined; } } } if (existsItem) { // check metaDataKeys, if different value, override exists one // https://github.com/cnpm/cnpmjs.org/issues/1667 // need libc field https://github.com/cnpm/cnpmcore/issues/187 // fix _npmUser field since https://github.com/cnpm/cnpmcore/issues/553 const metaDataKeys = [ 'peerDependenciesMeta', 'os', 'cpu', 'libc', 'workspaces', 'hasInstallScript', 'deprecated', '_npmUser', 'funding', // https://github.com/cnpm/cnpmcore/issues/689 'acceptDependencies', ]; const ignoreInAbbreviated = new Set(['_npmUser']); const diffMeta: Partial = {}; for (const key of metaDataKeys) { let remoteItemValue = item[key]; // make sure hasInstallScript exists if (key === 'hasInstallScript' && remoteItemValue === undefined) { if (detectInstallScript(item)) { remoteItemValue = true; } } if (!isEqual(remoteItemValue, existsItem[key])) { diffMeta[key] = remoteItemValue; } else if ( !ignoreInAbbreviated.has(key) && existsAbbreviatedItem && !isEqual( remoteItemValue, (existsAbbreviatedItem as Record)[key] ) ) { // should diff exists abbreviated item too diffMeta[key] = remoteItemValue; } } // should delete readme if (shouldDeleteReadme) { diffMeta.readme = undefined; } if (!isEmpty(diffMeta)) { differentMetas.push([existsItem, diffMeta]); } continue; } syncIndex++; const description = item.description; // "dist": { // "shasum": "943e0ec03df00ebeb6273a5b94b916ba54b47581", // "tarball": "https://registry.npmjs.org/foo/-/foo-1.0.0.tgz" // }, const dist = item.dist; const tarball = dist && dist.tarball; if (!tarball) { lastErrorMessage = `missing tarball, dist: ${JSON.stringify(dist)}`; logs.push( `[${isoNow()}] โŒ [${syncIndex}] Synced version ${version} fail, ${lastErrorMessage}` ); await this.taskService.appendTaskLog(task, logs.join('\n')); logs = []; continue; } const publishTimeISO = timeMap[version]; const publishTime = publishTimeISO ? new Date(publishTimeISO) : new Date(); const delay = Date.now() - publishTime.getTime(); logs.push( `[${isoNow()}] ๐Ÿšง [${syncIndex}] Syncing version ${version}, delay: ${delay}ms [${publishTimeISO}], tarball: ${tarball}` ); let localFile: string; try { const { tmpfile, headers, timing } = await downloadToTempfile( this.httpclient, this.config.dataDir, tarball, { remoteAuthToken } ); localFile = tmpfile; logs.push( `[${isoNow()}] ๐Ÿšง [${syncIndex}] HTTP content-length: ${headers['content-length']}, timing: ${JSON.stringify(timing)} => ${localFile}` ); } catch (err: any) { if ( err.name === 'DownloadNotFoundError' || err.name === 'DownloadStatusInvalidError' ) { this.logger.warn('Download tarball %s error: %s', tarball, err); } else { this.logger.error('Download tarball %s error: %s', tarball, err); } lastErrorMessage = `download tarball error: ${err}`; logs.push( `[${isoNow()}] โŒ [${syncIndex}] Synced version ${version} fail, ${lastErrorMessage}` ); await this.taskService.appendTaskLog(task, logs.join('\n')); logs = []; continue; } if (!pkg) { pkg = await this.packageRepository.findPackage(scope, name); } const publishCmd = { scope, name, version, description, packageJson: item, readme, registryId: registry?.registryId, dist: { localFile, }, isPrivate: false, publishTime, skipRefreshPackageManifests: true, }; try { // ๅฝ“ version ่ฎฐๅฝ•ๅทฒ็ปๅญ˜ๅœจๆ—ถ๏ผŒ่ฟ˜้œ€่ฆๆ ก้ชŒไธ€ไธ‹ pkg.manifests ๆ˜ฏๅฆๅญ˜ๅœจ const publisher = users.find(user => user.displayName === item._npmUser?.name) || users[0]; const pkgVersion = await this.packageManagerService.publish( publishCmd, publisher ); updateVersions.push(pkgVersion.version); logs.push( `[${isoNow()}] ๐ŸŽ‰ [${syncIndex}] Synced version ${version} success, packageVersionId: ${pkgVersion.packageVersionId}, db id: ${pkgVersion.id}` ); } catch (err: any) { if (err.name === 'ForbiddenError') { logs.push( `[${isoNow()}] ๐Ÿ› [${syncIndex}] Synced version ${version} already exists, skip publish, try to set in local manifest` ); // ๅฆ‚ๆžœ pkg.manifests ไธๅญ˜ๅœจ๏ผŒ้œ€่ฆ่กฅๅ……ไธ€ไธ‹ updateVersions.push(version); } else { err.taskId = task.taskId; this.logger.error(err); lastErrorMessage = `publish error: ${err}`; logs.push( `[${isoNow()}] โŒ [${syncIndex}] Synced version ${version} error, ${lastErrorMessage}` ); if (err.name === 'BadRequestError') { // ็”ฑไบŽๅฝ“ๅ‰็‰ˆๆœฌ็š„ไพ่ต–ไธๆปก่ถณ๏ผŒๅฐ่ฏ•้‡่ฏ• // ้ป˜่ฎคไผšๅœจๅฝ“ๅ‰้˜Ÿๅˆ—ๆœ€ๅŽ้‡่ฏ• this.logger.info( '[PackageSyncerService.executeTask:fail-validate-deps] taskId: %s, targetName: %s, %s', task.taskId, task.targetName, task.error ); await this.taskService.retryTask(task, logs.join('\n')); return; } } } await this.taskService.appendTaskLog(task, logs.join('\n')); logs = []; await rm(localFile, { force: true }); if (!skipDependencies) { const dependencies: Record = item.dependencies || {}; for (const dependencyName in dependencies) { dependenciesSet.add(dependencyName); } const optionalDependencies: Record = item.optionalDependencies || {}; for (const dependencyName in optionalDependencies) { dependenciesSet.add(dependencyName); } } } // try to read package entity again after first sync if (!pkg) { pkg = await this.packageRepository.findPackage(scope, name); } if (!pkg || !pkg.id) { // sync all versions fail in the first time logs.push( `[${isoNow()}] โŒ All versions sync fail, package not exists, log: ${logUrl}` ); logs.push(`[${isoNow()}] ${failEnd}`); task.error = lastErrorMessage; this.logger.info( '[PackageSyncerService.executeTask:fail] taskId: %s, targetName: %s, package not exists', task.taskId, task.targetName ); await this.taskService.finishTask(task, TaskState.Fail, logs.join('\n')); return; } // 2.1 save differentMetas for (const [existsItem, diffMeta] of differentMetas) { const pkgVersion = await this.packageRepository.findPackageVersion( pkg.packageId, existsItem.version ); if (pkgVersion) { await this.packageManagerService.savePackageVersionManifest( pkgVersion, diffMeta, diffMeta ); updateVersions.push(pkgVersion.version); let diffMetaInfo = JSON.stringify(diffMeta); if ('readme' in diffMeta) { diffMetaInfo += ', delete exists readme'; } logs.push( `[${isoNow()}] ๐ŸŸข Synced version ${existsItem.version} success, different meta: ${diffMetaInfo}` ); } } const removeVersions: string[] = []; // 2.3 find out remove versions for (const existsVersion in existsVersionMap) { if (!(existsVersion in versionMap)) { const pkgVersion = await this.packageRepository.findPackageVersion( pkg.packageId, existsVersion ); if (pkgVersion) { await this.packageManagerService.removePackageVersion( pkg, pkgVersion, true ); logs.push( `[${isoNow()}] ๐ŸŸข Removed version ${existsVersion} success` ); } removeVersions.push(existsVersion); } } logs.push( `[${isoNow()}] ๐ŸŸข Synced updated ${updateVersions.length} versions, removed ${removeVersions.length} versions` ); if (updateVersions.length > 0 || removeVersions.length > 0) { logs.push(`[${isoNow()}] ๐Ÿšง Refreshing manifests to dists ......`); const start = Date.now(); await this.taskService.appendTaskLog(task, logs.join('\n')); logs = []; await this.packageManagerService.refreshPackageChangeVersionsToDists( pkg, updateVersions, removeVersions ); logs.push(`[${isoNow()}] ๐ŸŸข Refresh use ${Date.now() - start}ms`); } // 3. update tags // "dist-tags": { // "latest": "0.0.7" // }, const changedTags: { tag: string; version?: string; action: string }[] = []; const existsDistTags = (existsData && existsData['dist-tags']) || {}; let shouldRefreshDistTags = false; for (const tag in distTags) { const version = distTags[tag]; const utf8mb3Regex = /[\u0020-\uD7FF\uE000-\uFFFD]/; if (!utf8mb3Regex.test(tag)) { logs.push( `[${isoNow()}] ๐Ÿšง invalid tag(${tag}: ${version}), tag name is out of utf8mb3, skip` ); continue; } // ๆ–ฐ tag ๆŒ‡ๅ‘็š„็‰ˆๆœฌๆ—ขไธๅœจๅญ˜้‡ๆ•ฐๆฎ้‡Œ๏ผŒไนŸไธๅœจๆœฌๆฌกๅŒๆญฅ็‰ˆๆœฌๅˆ—่กจ้‡Œ // ไพ‹ๅฆ‚ latest ๅฏนๅบ”็š„ version ๅ†™ๅ…ฅๅคฑ่ดฅ่ทณ่ฟ‡ if (!existsVersionMap[version] && !updateVersions.includes(version)) { logs.push( `[${isoNow()}] ๐Ÿšง invalid tag(${tag}: ${version}), version is not exists, skip` ); continue; } const changed = await this.packageManagerService.savePackageTag( pkg, tag, version ); if (changed) { changedTags.push({ action: 'change', tag, version }); shouldRefreshDistTags = false; } else if (version !== existsDistTags[tag]) { shouldRefreshDistTags = true; logs.push( `[${isoNow()}] ๐Ÿšง Remote tag(${tag}: ${version}) not exists in local dist-tags` ); } } // 3.1 find out remove tags for (const tag in existsDistTags) { if (!(tag in distTags)) { const changed = await this.packageManagerService.removePackageTag( pkg, tag ); if (changed) { changedTags.push({ action: 'remove', tag }); shouldRefreshDistTags = false; } } } // 3.2 shoud add latest tag // ๅœจๅŒๆญฅ sepcific version ๆ—ถๅฆ‚ๆžœๆฒกๆœ‰ๅŒๆญฅ latestTag ็š„็‰ˆๆœฌไผšๅ‡บ็Žฐ latestTag ไธขๅคฑๆˆ–ๆŒ‡ๅ‘็‰ˆๆœฌไธๆญฃ็กฎ็š„ๆƒ…ๅ†ต if (specificVersions && this.config.cnpmcore.strictSyncSpecivicVersion) { // ไธๅ…่ฎธ่‡ชๅŠจๅŒๆญฅ latest ็‰ˆๆœฌ๏ผŒไปŽๅทฒๅŒๆญฅ็‰ˆๆœฌไธญ้€‰ๅ‡บ latest let latestStableVersion = semver.maxSatisfying( sortedAvailableVersions, '*' ); // ๆ‰€ๆœ‰็‰ˆๆœฌ้ƒฝไธๆ˜ฏ็จณๅฎš็‰ˆๆœฌๅˆ™ๆŒ‡ๅ‘้ž็จณๅฎš็‰ˆๆœฌไฟ่ฏ latest ๅญ˜ๅœจ if (!latestStableVersion) { latestStableVersion = sortedAvailableVersions[0]; } if ( !existsDistTags.latest || semver.rcompare(existsDistTags.latest, latestStableVersion) === 1 ) { logs.push( `[${isoNow()}] ๐Ÿšง patch latest tag from specific versions ๐Ÿšง` ); changedTags.push({ action: 'change', tag: 'latest', version: latestStableVersion, }); await this.packageManagerService.savePackageTag( pkg, 'latest', latestStableVersion ); } } if (changedTags.length > 0) { logs.push( `[${isoNow()}] ๐ŸŸข Synced ${changedTags.length} tags: ${JSON.stringify(changedTags)}` ); } if (shouldRefreshDistTags) { await this.packageManagerService.refreshPackageDistTagsToDists(pkg); logs.push(`[${isoNow()}] ๐ŸŸข Refresh dist-tags`); } // 4. add package maintainers await this.packageManagerService.savePackageMaintainers(pkg, users); // 4.1 find out remove maintainers const removedMaintainers: unknown[] = []; const existsMaintainers = (existsData && existsData.maintainers) || []; for (const maintainer of existsMaintainers) { const { name } = maintainer; if (!(name in maintainersMap)) { const user = await this.userRepository.findUserByName( `${registry?.userPrefix || 'npm:'}${name}` ); if (user) { await this.packageManagerService.removePackageMaintainer(pkg, user); removedMaintainers.push(maintainer); } } } if (removedMaintainers.length > 0) { logs.push( `[${isoNow()}] ๐ŸŸข Removed ${removedMaintainers.length} maintainers: ${JSON.stringify(removedMaintainers)}` ); } // 4.2 update package maintainers in dist // The event is initialized in the repository and distributed after uncork. // maintainers' information is updated in bulk to ensure consistency. if (!isEqual(maintainers, existsMaintainers)) { logs.push( `[${isoNow()}] ๐Ÿšง Syncing maintainers to package manifest, from: ${JSON.stringify(maintainers)} to: ${JSON.stringify(existsMaintainers)}` ); await this.packageManagerService.refreshPackageMaintainersToDists(pkg); logs.push( `[${isoNow()}] ๐ŸŸข Syncing maintainers to package manifest done` ); } // 5. add deps sync task for (const dependencyName of dependenciesSet) { const existsTask = await this.taskRepository.findTaskByTargetName( dependencyName, TaskType.SyncPackage, TaskState.Waiting ); if (existsTask) { logs.push( `[${isoNow()}] ๐Ÿ“– Has dependency "${dependencyName}" sync task: ${existsTask.taskId}, db id: ${existsTask.id}` ); continue; } const tips = `Sync cause by "${fullname}" dependencies, parent task: ${task.taskId}`; const dependencyTask = await this.createTask(dependencyName, { authorId: task.authorId, authorIp: task.authorIp, tips, }); logs.push( `[${isoNow()}] ๐Ÿ“ฆ Add dependency "${dependencyName}" sync task: ${dependencyTask.taskId}, db id: ${dependencyTask.id}` ); } if (syncDownloadData) { await this.syncDownloadData(task, pkg); } // clean cache await this.cacheService.removeCache(fullname); logs.push(`[${isoNow()}] ๐Ÿ—‘๏ธ Clean cache`); logs.push(`[${isoNow()}] ๐Ÿ“ Log URL: ${logUrl}`); logs.push(`[${isoNow()}] ๐Ÿ”— ${url}`); task.error = lastErrorMessage; this.logger.info( '[PackageSyncerService.executeTask:success] taskId: %s, targetName: %s', task.taskId, task.targetName ); await this.taskService.finishTask(task, TaskState.Success, logs.join('\n')); } }