diff --git a/app/core/entity/Task.ts b/app/core/entity/Task.ts index 4d2d6655..b996037c 100644 --- a/app/core/entity/Task.ts +++ b/app/core/entity/Task.ts @@ -38,6 +38,7 @@ export type SyncPackageTaskOptions = { // force sync history version forceSyncHistory?: boolean; registryId?: string; + specificVersions?: Array; }; export interface CreateHookTaskData extends TaskBaseData { @@ -56,6 +57,7 @@ export interface CreateSyncPackageTaskData extends TaskBaseData { skipDependencies?: boolean; syncDownloadData?: boolean; forceSyncHistory?: boolean; + specificVersions?: Array; } export interface ChangesStreamTaskData extends TaskBaseData { @@ -137,6 +139,7 @@ export class Task extends Entity { skipDependencies: options?.skipDependencies, syncDownloadData: options?.syncDownloadData, forceSyncHistory: options?.forceSyncHistory, + specificVersions: options?.specificVersions, }, }; const task = this.create(data); diff --git a/app/core/service/PackageSyncerService.ts b/app/core/service/PackageSyncerService.ts index 4bb906a4..4160d4b4 100644 --- a/app/core/service/PackageSyncerService.ts +++ b/app/core/service/PackageSyncerService.ts @@ -5,13 +5,13 @@ import { Inject, } from '@eggjs/tegg'; import { Pointcut } from '@eggjs/tegg/aop'; -import { - EggContextHttpClient, -} from 'egg'; +import { EggHttpClient } from 'egg'; import { setTimeout } from 'timers/promises'; import { rm } from 'fs/promises'; import { isEqual } from 'lodash'; import semver from 'semver'; +import semverRcompare from 'semver/functions/rcompare'; +import semverPrerelease from 'semver/functions/prerelease'; import { NPMRegistry, RegistryResponse } from '../../common/adapter/NPMRegistry'; import { detectInstallScript, getScopeAndName } from '../../common/PackageUtil'; import { downloadToTempfile } from '../../common/FileUtil'; @@ -74,7 +74,7 @@ export class PackageSyncerService extends AbstractService { @Inject() private readonly cacheService: CacheService; @Inject() - private readonly httpclient: EggContextHttpClient; + private readonly httpclient: EggHttpClient; @Inject() private readonly registryManagerService: RegistryManagerService; @Inject() @@ -350,7 +350,7 @@ export class PackageSyncerService extends AbstractService { public async executeTask(task: Task) { const fullname = task.targetName; const [ scope, name ] = getScopeAndName(fullname); - const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, remoteAuthToken } = task.data as SyncPackageTaskOptions; + const { tips, skipDependencies: originSkipDependencies, syncDownloadData, forceSyncHistory, remoteAuthToken, 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; @@ -367,6 +367,9 @@ export class PackageSyncerService extends AbstractService { 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 (pkg && pkg?.registryId !== registry?.registryId) { @@ -545,8 +548,20 @@ export class PackageSyncerService extends AbstractService { const existsVersionCount = Object.keys(existsVersionMap).length; const abbreviatedVersionMap = abbreviatedManifests?.versions ?? {}; // 2. save versions - const versions = Object.values(versionMap); + 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); logs.push(`[${isoNow()}] 🚧 Syncing versions ${existsVersionCount} => ${versions.length}`); + if (specificVersions) { + const availableVersionList = versions.map(item => item.version); + let notAvailableVersionList = specificVersions.filter(i => !availableVersionList.includes(i)); + if (notAvailableVersionList.length > 0) { + notAvailableVersionList = Array.from(new Set(notAvailableVersionList)); + logs.push(`[${isoNow()}] 🚧 Some specific versions are not available: 👉 ${notAvailableVersionList.join(' | ')} 👈`); + } + } const updateVersions: string[] = []; const differentMetas: any[] = []; let syncIndex = 0; @@ -788,6 +803,24 @@ export class PackageSyncerService extends AbstractService { } } } + // 3.2 shoud add latest tag + // 在同步sepcific version时如果没有同步latestTag的版本会出现latestTag丢失或指向版本不正确的情况 + if (specificVersions && this.config.cnpmcore.strictSyncSpecivicVersion) { + // 不允许自动同步latest版本,从已同步版本中选出latest + let latestStabelVersion; + const sortedVersionList = specificVersions.sort(semverRcompare); + latestStabelVersion = sortedVersionList.filter(i => !semverPrerelease(i))[0]; + // 所有版本都不是稳定版本则指向非稳定版本保证latest存在 + if (!latestStabelVersion) { + latestStabelVersion = sortedVersionList[0]; + } + if (!existsDistTags.latest || semverRcompare(existsDistTags.latest, latestStabelVersion) === 1) { + logs.push(`[${isoNow()}] 🚧 patch latest tag from specific versions 🚧`); + changedTags.push({ action: 'change', tag: 'latest', version: latestStabelVersion }); + await this.packageManagerService.savePackageTag(pkg, 'latest', latestStabelVersion); + } + } + if (changedTags.length > 0) { logs.push(`[${isoNow()}] 🟢 Synced ${changedTags.length} tags: ${JSON.stringify(changedTags)}`); } @@ -836,6 +869,7 @@ export class PackageSyncerService extends AbstractService { authorId: task.authorId, authorIp: task.authorIp, tips, + remoteAuthToken, }); logs.push(`[${isoNow()}] 📦 Add dependency "${dependencyName}" sync task: ${dependencyTask.taskId}, db id: ${dependencyTask.id}`); } diff --git a/app/core/service/TaskService.ts b/app/core/service/TaskService.ts index 4a431967..08fa34f3 100644 --- a/app/core/service/TaskService.ts +++ b/app/core/service/TaskService.ts @@ -7,7 +7,7 @@ import { NFSAdapter } from '../../common/adapter/NFSAdapter'; import { TaskState, TaskType } from '../../common/enum/Task'; import { AbstractService } from '../../common/AbstractService'; import { TaskRepository } from '../../repository/TaskRepository'; -import { Task } from '../entity/Task'; +import { Task, CreateSyncPackageTaskData } from '../entity/Task'; import { QueueAdapter } from '../../common/typing'; @SingletonProto({ @@ -31,6 +31,21 @@ export class TaskService extends AbstractService { // 如果任务还未被触发,就不继续重复创建 // 如果任务正在执行,可能任务状态已更新,这种情况需要继续创建 if (existsTask.state === TaskState.Waiting) { + if (task.type === TaskType.SyncPackage) { + // 如果是specificVersions的任务则可能可以和存量任务进行合并 + const specificVersions = (task as Task).data?.specificVersions; + const existsTaskSpecificVersions = (existsTask as Task).data?.specificVersions; + if (existsTaskSpecificVersions) { + if (specificVersions) { + // 存量的任务和新增任务都是同步指定版本的任务,合并两者版本至存量任务 + await this.taskRepository.updateSpecificVersionsOfWaitingTask(existsTask, specificVersions); + } else { + // 新增任务是全量同步任务,移除存量任务中的指定版本使其成为全量同步任务 + await this.taskRepository.updateSpecificVersionsOfWaitingTask(existsTask); + } + } + // 存量任务是全量同步任务,直接提高任务优先级 + } // 提高任务的优先级 if (addTaskQueueOnExists) { const queueLength = await this.getTaskQueueLength(task.type); diff --git a/app/port/config.ts b/app/port/config.ts index 2245042f..f5f7fa1c 100644 --- a/app/port/config.ts +++ b/app/port/config.ts @@ -145,4 +145,9 @@ export type CnpmcoreConfig = { * enable unpkg features, https://github.com/cnpm/cnpmcore/issues/452 */ enableUnpkg: boolean, + /** + * enable this would make sync specific version task not append latest version into this task automatically,it would mark the local latest stable version as latest tag. + * in most cases, you should set to false to keep the same behavior as source registry. + */ + strictSyncSpecivicVersion: boolean, }; diff --git a/app/port/controller/PackageSyncController.ts b/app/port/controller/PackageSyncController.ts index 2b57936d..c485a522 100644 --- a/app/port/controller/PackageSyncController.ts +++ b/app/port/controller/PackageSyncController.ts @@ -74,6 +74,7 @@ export class PackageSyncController extends AbstractController { force: !!data.force, // only admin allow to sync history version forceSyncHistory: !!data.forceSyncHistory && isAdmin, + specificVersions: data.specificVersions, }; ctx.tValidate(SyncPackageTaskRule, params); const [ scope, name ] = getScopeAndName(params.fullname); @@ -102,6 +103,7 @@ export class PackageSyncController extends AbstractController { syncDownloadData: params.syncDownloadData, forceSyncHistory: params.forceSyncHistory, registryId: registry?.registryId, + specificVersions: params.specificVersions && JSON.parse(params.specificVersions), }); ctx.logger.info('[PackageSyncController.createSyncTask:success] taskId: %s, fullname: %s', task.taskId, fullname); diff --git a/app/port/typebox.ts b/app/port/typebox.ts index 38d061e3..013538a9 100644 --- a/app/port/typebox.ts +++ b/app/port/typebox.ts @@ -54,6 +54,11 @@ export const Version = Type.String({ maxLength: 256, }); +export const VersionStringArray = Type.String({ + format: 'semver-version-array', + transform: [ 'trim' ], +}); + export const Spec = Type.String({ format: 'semver-spec', minLength: 1, @@ -85,6 +90,7 @@ export const SyncPackageTaskRule = Type.Object({ maxLength: 1024, }), skipDependencies: Type.Boolean(), + specificVersions: Type.Optional(VersionStringArray), syncDownloadData: Type.Boolean(), // force sync immediately, only allow by admin force: Type.Boolean(), @@ -149,6 +155,21 @@ export function patchAjv(ajv: any) { return !!binaryConfig[binaryName]; }, }); + ajv.addFormat('semver-version-array', { + type: 'string', + validate: (versionStringList: string) => { + let versionList; + try { + versionList = JSON.parse(versionStringList); + } catch (error) { + return false; + } + if (versionList instanceof Array) { + return versionList.every(version => !!semver.valid(version)); + } + return false; + }, + }); } export const QueryPageOptions = Type.Object({ diff --git a/app/repository/TaskRepository.ts b/app/repository/TaskRepository.ts index fe359e07..20bed616 100644 --- a/app/repository/TaskRepository.ts +++ b/app/repository/TaskRepository.ts @@ -67,6 +67,21 @@ export class TaskRepository extends AbstractRepository { await model.remove(); } + async updateSpecificVersionsOfWaitingTask(task: TaskEntity, specificVersions?: Array): Promise { + const model = await this.Task.findOne({ id: task.id }); + if (!model || !model.data.specificVersions) return; + if (specificVersions) { + const data = model.data; + const combinedVersions = Array.from(new Set(data.specificVersions.concat(specificVersions))); + data.specificVersions = combinedVersions; + await model.update({ data }); + } else { + const data = model.data; + Reflect.deleteProperty(data, 'specificVersions'); + await model.update({ data }); + } + } + async findTask(taskId: string) { const task = await this.Task.findOne({ taskId }); if (task) { diff --git a/config/config.default.ts b/config/config.default.ts index 7033c50c..f85eda51 100644 --- a/config/config.default.ts +++ b/config/config.default.ts @@ -52,6 +52,7 @@ export const cnpmcoreConfig: CnpmcoreConfig = { syncNotFound: false, redirectNotFound: true, enableUnpkg: true, + strictSyncSpecivicVersion: false, }; export default (appInfo: EggAppConfig) => { diff --git a/test/core/service/PackageSyncerService/createTask.test.ts b/test/core/service/PackageSyncerService/createTask.test.ts index 5cdcb0f0..ea05d4a9 100644 --- a/test/core/service/PackageSyncerService/createTask.test.ts +++ b/test/core/service/PackageSyncerService/createTask.test.ts @@ -74,6 +74,27 @@ describe('test/core/service/PackageSyncerService/createTask.test.ts', () => { assert(res[1].taskId === task.taskId); }); + it('should append specific version to waiting task.', async () => { + const name = '@cnpmcore/test-sync-package-has-two-versions'; + await packageSyncerService.createTask(name, { specificVersions: [ '1.0.0' ] }); + await packageSyncerService.createTask(name, { specificVersions: [ '2.0.0' ] }); + const task = await packageSyncerService.findExecuteTask(); + assert(task); + assert.equal(task.targetName, name); + assert(task.data.specificVersions); + assert(task.data.specificVersions.length === 2); + }); + + it('should remove specific version, switch waiting task to sync all versions.', async () => { + const name = '@cnpmcore/test-sync-package-has-two-versions'; + await packageSyncerService.createTask(name, { specificVersions: [ '1.0.0' ] }); + await packageSyncerService.createTask(name); + const task = await packageSyncerService.findExecuteTask(); + assert(task); + assert.equal(task.targetName, name); + assert(task.data.specificVersions === undefined); + }); + it('should not duplicate task when waiting', async () => { const task = await packageSyncerService.createTask(pkgName); const newTask = await packageSyncerService.createTask(pkgName); diff --git a/test/core/service/PackageSyncerService/executeTask.test.ts b/test/core/service/PackageSyncerService/executeTask.test.ts index f5551f29..809d5eaf 100644 --- a/test/core/service/PackageSyncerService/executeTask.test.ts +++ b/test/core/service/PackageSyncerService/executeTask.test.ts @@ -1228,6 +1228,80 @@ describe('test/core/service/PackageSyncerService/executeTask.test.ts', () => { app.mockAgent().assertNoPendingInterceptors(); }); + it('should only sync specific version when strictSyncSpecivicVersion is true.', async () => { + mock(app.config.cnpmcore, 'strictSyncSpecivicVersion', true); + app.mockHttpclient('https://registry.npmjs.org/%40cnpmcore%2Ftest-sync-package-has-two-versions', 'GET', { + data: '{"_id":"@cnpmcore/test-sync-package-has-two-versions","_rev":"4-541287ae0a14039fea89ac08fa5ec53d","name":"@cnpmcore/test-sync-package-has-two-versions","dist-tags":{"latest":"2.0.0","next":"2.0.0"},"versions":{"1.0.0":{"name":"@cnpmcore/test-sync-package-has-two-versions","version":"1.0.0","description":"cnpmcore local test package","main":"index.js","scripts":{"test":"echo \\"hello\\""},"author":"","license":"MIT","gitHead":"60cfb1cf401f87a60a1b0dfd7ee739f98ffd7847","_id":"@cnpmcore/test-sync-package-has-two-versions@1.0.0","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-WR0T96H8t7ss1FK8GWPPblx+usbjU4bNGRjMHS9t/oVA5DgJDxitydPSFPeIUtXciyekI7R47do9Lc3GgC4P5A==","shasum":"2ddc6ee93b92be6d64139fb1a631d2610f43e946","tarball":"https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-1.0.0.tgz","fileCount":2,"unpackedSize":238,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDj5Ui2GU8nVmHFk0hCt/i3gPW9eQdOCZgKzpAlkvERwQIhAPZ0NCefLoEfOpnbdKAUr7Ng9Sy6FMnTsDxDaM2dQHNw"}]},"_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"directories":{},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/test-sync-package-has-two-versions_1.0.0_1639442699824_0.6948988437963031"},"_hasShrinkwrap":false},"2.0.0":{"name":"@cnpmcore/test-sync-package-has-two-versions","version":"2.0.0","description":"cnpmcore local test package","main":"index.js","scripts":{"test":"echo \\"hello\\""},"author":"","license":"MIT","gitHead":"60cfb1cf401f87a60a1b0dfd7ee739f98ffd7847","_id":"@cnpmcore/test-sync-package-has-two-versions@2.0.0","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-qgHLQzXq+VN7q0JWibeBYrqb3Iajl4lpVuxlQstclRz4ejujfDFswBGSXmCv9FyIIdmSAe5bZo0oHQLsod3pAA==","shasum":"891eb8e08ceadbd86e75b6d66f31f7e5a28a8d68","tarball":"https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-2.0.0.tgz","fileCount":2,"unpackedSize":238,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAWVz7mIHF23Gq4a+Swsj2ZSdn87991HcE1+fQm8shNCAiByOIuhaZAbo9hct24qYf7FWqx6Lyluo+Rpnrn91//Ibg=="}]},"_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"directories":{},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/test-sync-package-has-two-versions_2.0.0_1639442732240_0.33204392278137207"},"_hasShrinkwrap":false}},"time":{"created":"2021-12-14T00:44:59.775Z","1.0.0":"2021-12-14T00:44:59.940Z","modified":"2022-05-23T02:33:52.613Z","2.0.0":"2021-12-14T00:45:32.457Z"},"maintainers":[{"email":"killa07071201@gmail.com","name":"killagu"},{"email":"fengmk2@gmail.com","name":"fengmk2"}],"description":"cnpmcore local test package","license":"MIT","readme":"ERROR: No README data found!","readmeFilename":""}', + persist: false, + }); + app.mockHttpclient('https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-1.0.0.tgz', 'GET', { + data: await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.0.0.tgz'), + persist: false, + }); + app.mockHttpclient('https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-2.0.0.tgz', 'GET', { + data: await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.0.0.tgz'), + persist: false, + }); + const name = '@cnpmcore/test-sync-package-has-two-versions'; + await packageSyncerService.createTask(name, { specificVersions: [ '1.0.0' ] }); + const task = await packageSyncerService.findExecuteTask(); + assert(task); + assert.equal(task.targetName, name); + await packageSyncerService.executeTask(task); + const stream = await packageSyncerService.findTaskLog(task); + assert(stream); + const log = await TestUtil.readStreamToLog(stream); + assert(log.includes('] 🟢 Synced updated 1 versions')); + assert(app.mockAgent().pendingInterceptors().length === 1); + }); + + it('should sync specific versions and latest version.', async () => { + app.mockHttpclient('https://registry.npmjs.org/%40cnpmcore%2Ftest-sync-package-has-two-versions', 'GET', { + data: '{"_id":"@cnpmcore/test-sync-package-has-two-versions","_rev":"4-541287ae0a14039fea89ac08fa5ec53d","name":"@cnpmcore/test-sync-package-has-two-versions","dist-tags":{"latest":"2.0.0","next":"2.0.0"},"versions":{"1.0.0":{"name":"@cnpmcore/test-sync-package-has-two-versions","version":"1.0.0","description":"cnpmcore local test package","main":"index.js","scripts":{"test":"echo \\"hello\\""},"author":"","license":"MIT","gitHead":"60cfb1cf401f87a60a1b0dfd7ee739f98ffd7847","_id":"@cnpmcore/test-sync-package-has-two-versions@1.0.0","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-WR0T96H8t7ss1FK8GWPPblx+usbjU4bNGRjMHS9t/oVA5DgJDxitydPSFPeIUtXciyekI7R47do9Lc3GgC4P5A==","shasum":"2ddc6ee93b92be6d64139fb1a631d2610f43e946","tarball":"https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-1.0.0.tgz","fileCount":2,"unpackedSize":238,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDj5Ui2GU8nVmHFk0hCt/i3gPW9eQdOCZgKzpAlkvERwQIhAPZ0NCefLoEfOpnbdKAUr7Ng9Sy6FMnTsDxDaM2dQHNw"}]},"_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"directories":{},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/test-sync-package-has-two-versions_1.0.0_1639442699824_0.6948988437963031"},"_hasShrinkwrap":false},"2.0.0":{"name":"@cnpmcore/test-sync-package-has-two-versions","version":"2.0.0","description":"cnpmcore local test package","main":"index.js","scripts":{"test":"echo \\"hello\\""},"author":"","license":"MIT","gitHead":"60cfb1cf401f87a60a1b0dfd7ee739f98ffd7847","_id":"@cnpmcore/test-sync-package-has-two-versions@2.0.0","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-qgHLQzXq+VN7q0JWibeBYrqb3Iajl4lpVuxlQstclRz4ejujfDFswBGSXmCv9FyIIdmSAe5bZo0oHQLsod3pAA==","shasum":"891eb8e08ceadbd86e75b6d66f31f7e5a28a8d68","tarball":"https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-2.0.0.tgz","fileCount":2,"unpackedSize":238,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAWVz7mIHF23Gq4a+Swsj2ZSdn87991HcE1+fQm8shNCAiByOIuhaZAbo9hct24qYf7FWqx6Lyluo+Rpnrn91//Ibg=="}]},"_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"directories":{},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/test-sync-package-has-two-versions_2.0.0_1639442732240_0.33204392278137207"},"_hasShrinkwrap":false}},"time":{"created":"2021-12-14T00:44:59.775Z","1.0.0":"2021-12-14T00:44:59.940Z","modified":"2022-05-23T02:33:52.613Z","2.0.0":"2021-12-14T00:45:32.457Z"},"maintainers":[{"email":"killa07071201@gmail.com","name":"killagu"},{"email":"fengmk2@gmail.com","name":"fengmk2"}],"description":"cnpmcore local test package","license":"MIT","readme":"ERROR: No README data found!","readmeFilename":""}', + persist: false, + }); + app.mockHttpclient('https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-1.0.0.tgz', 'GET', { + data: await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.0.0.tgz'), + persist: false, + }); + app.mockHttpclient('https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-2.0.0.tgz', 'GET', { + data: await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.0.0.tgz'), + persist: false, + }); + const name = '@cnpmcore/test-sync-package-has-two-versions'; + await packageSyncerService.createTask(name, { specificVersions: [ '1.0.0' ] }); + const task = await packageSyncerService.findExecuteTask(); + assert(task); + assert.equal(task.targetName, name); + await packageSyncerService.executeTask(task); + const stream = await packageSyncerService.findTaskLog(task); + assert(stream); + const log = await TestUtil.readStreamToLog(stream); + assert(log.includes('] 🟢 Synced updated 2 versions')); + assert(app.mockAgent().pendingInterceptors().length === 0); + }); + + it('should sync specific versions and latest version.', async () => { + app.mockHttpclient('https://registry.npmjs.org/%40cnpmcore%2Ftest-sync-package-has-two-versions', 'GET', { + data: '{"_id":"@cnpmcore/test-sync-package-has-two-versions","_rev":"4-541287ae0a14039fea89ac08fa5ec53d","name":"@cnpmcore/test-sync-package-has-two-versions","dist-tags":{"latest":"2.0.0","next":"2.0.0"},"versions":{"1.0.0":{"name":"@cnpmcore/test-sync-package-has-two-versions","version":"1.0.0","description":"cnpmcore local test package","main":"index.js","scripts":{"test":"echo \\"hello\\""},"author":"","license":"MIT","gitHead":"60cfb1cf401f87a60a1b0dfd7ee739f98ffd7847","_id":"@cnpmcore/test-sync-package-has-two-versions@1.0.0","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-WR0T96H8t7ss1FK8GWPPblx+usbjU4bNGRjMHS9t/oVA5DgJDxitydPSFPeIUtXciyekI7R47do9Lc3GgC4P5A==","shasum":"2ddc6ee93b92be6d64139fb1a631d2610f43e946","tarball":"https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-1.0.0.tgz","fileCount":2,"unpackedSize":238,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQDj5Ui2GU8nVmHFk0hCt/i3gPW9eQdOCZgKzpAlkvERwQIhAPZ0NCefLoEfOpnbdKAUr7Ng9Sy6FMnTsDxDaM2dQHNw"}]},"_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"directories":{},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/test-sync-package-has-two-versions_1.0.0_1639442699824_0.6948988437963031"},"_hasShrinkwrap":false},"2.0.0":{"name":"@cnpmcore/test-sync-package-has-two-versions","version":"2.0.0","description":"cnpmcore local test package","main":"index.js","scripts":{"test":"echo \\"hello\\""},"author":"","license":"MIT","gitHead":"60cfb1cf401f87a60a1b0dfd7ee739f98ffd7847","_id":"@cnpmcore/test-sync-package-has-two-versions@2.0.0","_nodeVersion":"16.13.1","_npmVersion":"8.1.2","dist":{"integrity":"sha512-qgHLQzXq+VN7q0JWibeBYrqb3Iajl4lpVuxlQstclRz4ejujfDFswBGSXmCv9FyIIdmSAe5bZo0oHQLsod3pAA==","shasum":"891eb8e08ceadbd86e75b6d66f31f7e5a28a8d68","tarball":"https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-2.0.0.tgz","fileCount":2,"unpackedSize":238,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIAWVz7mIHF23Gq4a+Swsj2ZSdn87991HcE1+fQm8shNCAiByOIuhaZAbo9hct24qYf7FWqx6Lyluo+Rpnrn91//Ibg=="}]},"_npmUser":{"name":"fengmk2","email":"fengmk2@gmail.com"},"directories":{},"maintainers":[{"name":"fengmk2","email":"fengmk2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/test-sync-package-has-two-versions_2.0.0_1639442732240_0.33204392278137207"},"_hasShrinkwrap":false}},"time":{"created":"2021-12-14T00:44:59.775Z","1.0.0":"2021-12-14T00:44:59.940Z","modified":"2022-05-23T02:33:52.613Z","2.0.0":"2021-12-14T00:45:32.457Z"},"maintainers":[{"email":"killa07071201@gmail.com","name":"killagu"},{"email":"fengmk2@gmail.com","name":"fengmk2"}],"description":"cnpmcore local test package","license":"MIT","readme":"ERROR: No README data found!","readmeFilename":""}', + persist: false, + }); + app.mockHttpclient('https://registry.npmjs.org/@cnpmcore/test-sync-package-has-two-versions/-/test-sync-package-has-two-versions-1.0.0.tgz', 'GET', { + data: await TestUtil.readFixturesFile('registry.npmjs.org/foobar/-/foobar-1.0.0.tgz'), + persist: false, + }); + const name = '@cnpmcore/test-sync-package-has-two-versions'; + await packageSyncerService.createTask(name, { specificVersions: [ '1.0.0', '9.99.9' ] }); + const task = await packageSyncerService.findExecuteTask(); + assert(task); + assert.equal(task.targetName, name); + await packageSyncerService.executeTask(task); + const stream = await packageSyncerService.findTaskLog(task); + assert(stream); + const log = await TestUtil.readStreamToLog(stream); + assert(log.includes('🚧 Some specific versions are not available: 👉 9.99.9')); + }); + // 有任务积压,不一定能够同步完 it.skip('should sync sourceRegistryIsCNpm = true && syncUpstreamFirst = true', async () => { app.mockHttpclient('https://r.cnpmjs.org/cnpmcore-test-sync-deprecated', 'GET', { diff --git a/test/port/controller/PackageSyncController/createSyncTask.test.ts b/test/port/controller/PackageSyncController/createSyncTask.test.ts index 6ffa1af4..36d2dd00 100644 --- a/test/port/controller/PackageSyncController/createSyncTask.test.ts +++ b/test/port/controller/PackageSyncController/createSyncTask.test.ts @@ -60,6 +60,13 @@ describe('test/port/controller/PackageSyncController/createSyncTask.test.ts', () assert(res.body.error === '[FORBIDDEN] Can\'t sync private package "@cnpm/koa"'); }); + it('should 422 if specificVersions cannot parse is not valideted', async () => { + await app.httpRequest() + .put('/-/package/koa/syncs') + .send({ specificVersions: '1.0.0' }) + .expect(422); + }); + it('should 201 if user login when alwaysAuth = true', async () => { mock(app.config.cnpmcore, 'alwaysAuth', true); const res = await app.httpRequest()