Files
cnpmcore/app/core/service/PackageSyncerService.ts
MK (fengmk2) e5162f20aa fix: improve TypeScript type definitions across codebase (#844)
This commit enhances type safety and fixes type-related issues
throughout the project including:
- Updated type definitions in entities, repositories, and models
- Improved type annotations in services and controllers
- Fixed type issues in adapters and utilities
- Enhanced test file type definitions
- Added typings/index.d.ts for global type declarations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-23 00:58:59 +08:00

1335 lines
47 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os from 'node:os';
import { setTimeout } from 'node:timers/promises';
import { rm } from 'node:fs/promises';
import { AccessLevel, Inject, SingletonProto } from '@eggjs/tegg';
import { Pointcut } from '@eggjs/tegg/aop';
import type { EggHttpClient } from 'egg';
import { isEmpty, isEqual } from 'lodash-es';
import semver from 'semver';
import { BadRequestError } from 'egg-errors';
import type {
NPMRegistry,
RegistryResponse,
} from '../../common/adapter/NPMRegistry.ts';
import {
detectInstallScript,
getScopeAndName,
} from '../../common/PackageUtil.ts';
import { downloadToTempfile } from '../../common/FileUtil.ts';
import { TaskState, TaskType } from '../../common/enum/Task.ts';
import { AbstractService } from '../../common/AbstractService.ts';
import type { TaskRepository } from '../../repository/TaskRepository.ts';
import type {
PackageJSONType,
PackageManifestType,
PackageRepository,
} from '../../repository/PackageRepository.ts';
import type { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository.ts';
import type { UserRepository } from '../../repository/UserRepository.ts';
import {
type CreateSyncPackageTask,
type SyncPackageTaskOptions,
Task,
} from '../entity/Task.ts';
import type { Package } from '../entity/Package.ts';
import type { UserService } from './UserService.ts';
import type { TaskService } from './TaskService.ts';
import type { PackageManagerService } from './PackageManagerService.ts';
import type { CacheService } from './CacheService.ts';
import type { User } from '../entity/User.ts';
import type { RegistryManagerService } from './RegistryManagerService.ts';
import type { Registry } from '../entity/Registry.ts';
import type { ScopeManagerService } from './ScopeManagerService.ts';
import { EventCorkAdvice } from './EventCorkerAdvice.ts';
import { PresetRegistryName, SyncDeleteMode } from '../../common/constants.ts';
interface syncDeletePkgOptions {
task: Task;
pkg: Package | null;
logUrl: string;
url: string;
logs: string[];
data: unknown;
}
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 &&
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) {
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<number, [string, number][]>();
for (const item of downloads) {
// {
// "day": "2021-09-21",
// "downloads": 45
// },
const day = item.day;
const [year, month, date] = day.split('-');
const yearMonth = Number.parseInt(`${year}${month}`);
if (!datas.has(yearMonth)) {
datas.set(yearMonth, []);
}
const counters = datas.get(yearMonth);
// oxlint-disable-next-line typescript-eslint/no-non-null-assertion
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) {
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) {
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<Registry> {
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) {
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<string, PackageManifestType['maintainers']> =
{};
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'
) {
data.maintainers = [{ name: 'npm', email: 'npm@npmjs.com' }];
maintainers = data.maintainers;
} 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(
maintainer.name,
maintainer.email,
registry.userPrefix
);
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<string>();
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<PackageJSONType>(versionMap).filter(verItem =>
specificVersions.includes(verItem.version)
)
: Object.values<PackageJSONType>(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<PackageJSONType>][] = [];
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 && !existsAbbreviatedItem) {
// check item on AbbreviatedManifests
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<PackageJSONType> = {};
for (const key of metaDataKeys) {
let remoteItemValue = item[key];
// make sure hasInstallScript exists
if (
key === 'hasInstallScript' &&
remoteItemValue === undefined &&
detectInstallScript(item)
) {
remoteItemValue = true;
}
if (!isEqual(remoteItemValue, existsItem[key])) {
diffMeta[key] = remoteItemValue;
} else if (
!ignoreInAbbreviated.has(key) &&
existsAbbreviatedItem &&
!isEqual(
remoteItemValue,
(existsAbbreviatedItem as Record<string, unknown>)[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) {
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) {
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<string, string> = item.dependencies || {};
for (const dependencyName in dependencies) {
dependenciesSet.add(dependencyName);
}
const optionalDependencies: Record<string, string> =
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 should 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'));
}
}