1377 lines
44 KiB
TypeScript
1377 lines
44 KiB
TypeScript
import { stat, readFile } from 'node:fs/promises';
|
|
import { strict as assert } from 'node:assert';
|
|
import type { EventBus } from '@eggjs/tegg';
|
|
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
|
|
import { BadRequestError, ForbiddenError, NotFoundError } from 'egg-errors';
|
|
import type { RequireAtLeastOne } from 'type-fest';
|
|
import npa from 'npm-package-arg';
|
|
import semver from 'semver';
|
|
import pMap from 'p-map';
|
|
import {
|
|
calculateIntegrity,
|
|
detectInstallScript,
|
|
formatTarball,
|
|
getFullname,
|
|
getScopeAndName,
|
|
hasShrinkWrapInTgz,
|
|
} from '../../common/PackageUtil.js';
|
|
import { AbstractService } from '../../common/AbstractService.js';
|
|
import type {
|
|
AbbreviatedPackageJSONType,
|
|
AbbreviatedPackageManifestType,
|
|
PackageJSONType,
|
|
PackageManifestType,
|
|
PackageRepository,
|
|
} from '../../repository/PackageRepository.js';
|
|
import type { PackageVersionBlockRepository } from '../../repository/PackageVersionBlockRepository.js';
|
|
import type { PackageVersionDownloadRepository } from '../../repository/PackageVersionDownloadRepository.js';
|
|
import type { DistRepository } from '../../repository/DistRepository.js';
|
|
import { isDuplicateKeyError } from '../../repository/util/ErrorUtil.js';
|
|
import { Package } from '../entity/Package.js';
|
|
import { PackageVersion } from '../entity/PackageVersion.js';
|
|
import { PackageVersionBlock } from '../entity/PackageVersionBlock.js';
|
|
import { PackageTag } from '../entity/PackageTag.js';
|
|
import type { User } from '../entity/User.js';
|
|
import type { Dist } from '../entity/Dist.js';
|
|
import {
|
|
PACKAGE_UNPUBLISHED,
|
|
PACKAGE_BLOCKED,
|
|
PACKAGE_UNBLOCKED,
|
|
PACKAGE_MAINTAINER_CHANGED,
|
|
PACKAGE_MAINTAINER_REMOVED,
|
|
PACKAGE_VERSION_ADDED,
|
|
PACKAGE_VERSION_REMOVED,
|
|
PACKAGE_TAG_ADDED,
|
|
PACKAGE_TAG_CHANGED,
|
|
PACKAGE_TAG_REMOVED,
|
|
PACKAGE_META_CHANGED,
|
|
} from '../event/index.js';
|
|
import type { BugVersionService } from './BugVersionService.js';
|
|
import type { BugVersion } from '../entity/BugVersion.js';
|
|
import type { RegistryManagerService } from './RegistryManagerService.js';
|
|
import type { Registry } from '../entity/Registry.js';
|
|
import type { PackageVersionService } from './PackageVersionService.js';
|
|
|
|
export interface PublishPackageCmd {
|
|
// maintainer: Maintainer;
|
|
scope: string;
|
|
// name don't include scope
|
|
name: string;
|
|
version: string;
|
|
description?: string;
|
|
packageJson: PackageJSONType;
|
|
registryId?: string;
|
|
readme: string;
|
|
// require content or localFile field
|
|
dist: RequireAtLeastOne<
|
|
{
|
|
// package controller will use content field
|
|
content?: Uint8Array;
|
|
// sync worker will use localFile field
|
|
localFile?: string;
|
|
},
|
|
'content' | 'localFile'
|
|
>;
|
|
tags?: string[];
|
|
isPrivate: boolean;
|
|
// only use on sync package
|
|
publishTime?: Date;
|
|
// only use on sync package for speed up https://github.com/cnpm/cnpmcore/issues/28
|
|
skipRefreshPackageManifests?: boolean;
|
|
}
|
|
|
|
const TOTAL = '@@TOTAL@@';
|
|
const SCOPE_TOTAL_PREFIX = '@@SCOPE@@:';
|
|
const DESCRIPTION_LIMIT = 1024 * 10;
|
|
|
|
@SingletonProto({
|
|
accessLevel: AccessLevel.PUBLIC,
|
|
})
|
|
export class PackageManagerService extends AbstractService {
|
|
@Inject()
|
|
private readonly eventBus: EventBus;
|
|
@Inject()
|
|
private readonly packageRepository: PackageRepository;
|
|
@Inject()
|
|
private readonly packageVersionBlockRepository: PackageVersionBlockRepository;
|
|
@Inject()
|
|
private readonly packageVersionDownloadRepository: PackageVersionDownloadRepository;
|
|
@Inject()
|
|
private readonly bugVersionService: BugVersionService;
|
|
@Inject()
|
|
private readonly distRepository: DistRepository;
|
|
@Inject()
|
|
private readonly registryManagerService: RegistryManagerService;
|
|
@Inject()
|
|
private readonly packageVersionService: PackageVersionService;
|
|
|
|
private static downloadCounters = {};
|
|
|
|
// support user publish private package and sync worker publish public package
|
|
async publish(cmd: PublishPackageCmd, publisher: User) {
|
|
if (this.config.cnpmcore.strictValidatePackageDeps) {
|
|
await this._checkPackageDepsVersion(cmd.packageJson);
|
|
}
|
|
let pkg = await this.packageRepository.findPackage(cmd.scope, cmd.name);
|
|
if (!pkg) {
|
|
pkg = Package.create({
|
|
scope: cmd.scope,
|
|
name: cmd.name,
|
|
isPrivate: cmd.isPrivate,
|
|
description: cmd.description || '',
|
|
registryId: cmd.registryId,
|
|
});
|
|
} else {
|
|
// update description
|
|
// will read database twice to update description by model to entity and entity to model
|
|
if (pkg.description !== cmd.description) {
|
|
pkg.description = cmd.description || '';
|
|
}
|
|
|
|
/* c8 ignore next 3 */
|
|
// package can be migrated into another registry
|
|
if (cmd.registryId) {
|
|
pkg.registryId = cmd.registryId;
|
|
}
|
|
}
|
|
|
|
// 防止 description 长度超过 db 限制
|
|
if (pkg.description?.length > DESCRIPTION_LIMIT) {
|
|
pkg.description = pkg.description.substring(0, DESCRIPTION_LIMIT);
|
|
}
|
|
await this.packageRepository.savePackage(pkg);
|
|
// create maintainer
|
|
await this.packageRepository.savePackageMaintainer(
|
|
pkg.packageId,
|
|
publisher.userId
|
|
);
|
|
|
|
let pkgVersion = await this.packageRepository.findPackageVersion(
|
|
pkg.packageId,
|
|
cmd.version
|
|
);
|
|
if (pkgVersion) {
|
|
throw new ForbiddenError(
|
|
`Can't modify pre-existing version: ${pkg.fullname}@${pkgVersion.version}`
|
|
);
|
|
}
|
|
|
|
// make sure cmd.packageJson.readme is deleted
|
|
if ('readme' in cmd.packageJson) {
|
|
delete cmd.packageJson.readme;
|
|
}
|
|
|
|
const publishTime = cmd.publishTime || new Date();
|
|
|
|
// add _cnpmcore_publish_time field to cmd.packageJson
|
|
if (!cmd.packageJson._cnpmcore_publish_time) {
|
|
cmd.packageJson._cnpmcore_publish_time = publishTime;
|
|
}
|
|
if (!cmd.packageJson.publish_time) {
|
|
cmd.packageJson.publish_time = publishTime.getTime();
|
|
}
|
|
if (cmd.packageJson._hasShrinkwrap === undefined) {
|
|
cmd.packageJson._hasShrinkwrap = await hasShrinkWrapInTgz(
|
|
cmd.dist.content || cmd.dist.localFile!
|
|
);
|
|
}
|
|
|
|
// set _npmUser field to cmd.packageJson
|
|
cmd.packageJson._npmUser = {
|
|
// clean user scope prefix
|
|
name: publisher.displayName,
|
|
email: publisher.email,
|
|
};
|
|
|
|
// add _registry_name field to cmd.packageJson
|
|
const registry = await this.getSourceRegistry(pkg);
|
|
if (registry) {
|
|
cmd.packageJson._source_registry_name = registry.name;
|
|
}
|
|
|
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
|
|
const hasInstallScript = detectInstallScript(cmd.packageJson)
|
|
? true
|
|
: undefined;
|
|
let tarDistIntegrity: any;
|
|
let tarDistSize = 0;
|
|
if (cmd.dist.content) {
|
|
const tarDistBytes = cmd.dist.content;
|
|
tarDistIntegrity = await calculateIntegrity(tarDistBytes);
|
|
tarDistSize = tarDistBytes.length;
|
|
} else if (cmd.dist.localFile) {
|
|
const localFile = cmd.dist.localFile;
|
|
const fileStat = await stat(localFile);
|
|
tarDistIntegrity = await calculateIntegrity(localFile);
|
|
tarDistSize = fileStat.size;
|
|
}
|
|
const tarDist = pkg.createTar(cmd.version, {
|
|
size: tarDistSize,
|
|
shasum: tarDistIntegrity.shasum,
|
|
integrity: tarDistIntegrity.integrity,
|
|
});
|
|
if (cmd.dist.content) {
|
|
await this.distRepository.saveDist(tarDist, cmd.dist.content);
|
|
} else if (cmd.dist.localFile) {
|
|
await this.distRepository.saveDist(tarDist, cmd.dist.localFile);
|
|
}
|
|
|
|
cmd.packageJson.dist = {
|
|
...cmd.packageJson.dist,
|
|
tarball: formatTarball(
|
|
this.config.cnpmcore.registry,
|
|
pkg.scope,
|
|
pkg.name,
|
|
cmd.version
|
|
),
|
|
size: tarDistSize,
|
|
shasum: tarDistIntegrity.shasum,
|
|
integrity: tarDistIntegrity.integrity,
|
|
};
|
|
|
|
// https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md#abbreviated-version-object
|
|
// Abbreviated version object
|
|
const abbreviated = JSON.stringify({
|
|
name: cmd.packageJson.name,
|
|
version: cmd.packageJson.version,
|
|
deprecated: cmd.packageJson.deprecated,
|
|
dependencies: cmd.packageJson.dependencies,
|
|
acceptDependencies: cmd.packageJson.acceptDependencies,
|
|
optionalDependencies: cmd.packageJson.optionalDependencies,
|
|
devDependencies: cmd.packageJson.devDependencies,
|
|
bundleDependencies: cmd.packageJson.bundleDependencies,
|
|
peerDependencies: cmd.packageJson.peerDependencies,
|
|
peerDependenciesMeta: cmd.packageJson.peerDependenciesMeta,
|
|
bin: cmd.packageJson.bin,
|
|
directories: cmd.packageJson.directories,
|
|
os: cmd.packageJson.os,
|
|
cpu: cmd.packageJson.cpu,
|
|
libc: cmd.packageJson.libc,
|
|
workspaces: cmd.packageJson.workspaces,
|
|
dist: cmd.packageJson.dist,
|
|
engines: cmd.packageJson.engines,
|
|
_hasShrinkwrap: cmd.packageJson._hasShrinkwrap,
|
|
hasInstallScript,
|
|
funding: cmd.packageJson.funding,
|
|
// https://github.com/cnpm/npminstall/blob/13efc7eec21a61e509226e3772bfb75cd5605612/lib/install_package.js#L176
|
|
// npminstall require publish time to show the recently update versions
|
|
publish_time: cmd.packageJson.publish_time,
|
|
_source_registry_name: cmd.packageJson._source_registry_name,
|
|
} as AbbreviatedPackageJSONType);
|
|
const abbreviatedDistBytes = Buffer.from(abbreviated);
|
|
const abbreviatedDistIntegrity =
|
|
await calculateIntegrity(abbreviatedDistBytes);
|
|
const readmeDistBytes = Buffer.from(cmd.readme);
|
|
const readmeDistIntegrity = await calculateIntegrity(readmeDistBytes);
|
|
const manifestDistBytes = Buffer.from(JSON.stringify(cmd.packageJson));
|
|
const manifestDistIntegrity = await calculateIntegrity(manifestDistBytes);
|
|
|
|
pkgVersion = PackageVersion.create({
|
|
packageId: pkg.packageId,
|
|
version: cmd.version,
|
|
publishTime,
|
|
manifestDist: pkg.createManifest(cmd.version, {
|
|
size: manifestDistBytes.length,
|
|
shasum: manifestDistIntegrity.shasum,
|
|
integrity: manifestDistIntegrity.integrity,
|
|
}),
|
|
readmeDist: pkg.createReadme(cmd.version, {
|
|
size: readmeDistBytes.length,
|
|
shasum: readmeDistIntegrity.shasum,
|
|
integrity: readmeDistIntegrity.integrity,
|
|
}),
|
|
abbreviatedDist: pkg.createAbbreviated(cmd.version, {
|
|
size: abbreviatedDistBytes.length,
|
|
shasum: abbreviatedDistIntegrity.shasum,
|
|
integrity: abbreviatedDistIntegrity.integrity,
|
|
}),
|
|
tarDist,
|
|
});
|
|
await Promise.all([
|
|
this.distRepository.saveDist(
|
|
pkgVersion.abbreviatedDist,
|
|
abbreviatedDistBytes
|
|
),
|
|
this.distRepository.saveDist(pkgVersion.manifestDist, manifestDistBytes),
|
|
this.distRepository.saveDist(pkgVersion.readmeDist, readmeDistBytes),
|
|
]);
|
|
try {
|
|
await this.packageRepository.createPackageVersion(pkgVersion);
|
|
} catch (e) {
|
|
if (isDuplicateKeyError(e)) {
|
|
throw new ForbiddenError(
|
|
`Can't modify pre-existing version: ${pkg.fullname}@${cmd.version}`
|
|
);
|
|
}
|
|
throw e;
|
|
}
|
|
if (cmd.skipRefreshPackageManifests !== true) {
|
|
await this.refreshPackageChangeVersionsToDists(pkg, [pkgVersion.version]);
|
|
}
|
|
if (cmd.tags) {
|
|
for (const tag of cmd.tags) {
|
|
await this.savePackageTag(pkg, tag, cmd.version, true);
|
|
this.eventBus.emit(
|
|
PACKAGE_VERSION_ADDED,
|
|
pkg.fullname,
|
|
pkgVersion.version,
|
|
tag
|
|
);
|
|
}
|
|
} else {
|
|
this.eventBus.emit(
|
|
PACKAGE_VERSION_ADDED,
|
|
pkg.fullname,
|
|
pkgVersion.version,
|
|
undefined
|
|
);
|
|
}
|
|
|
|
return pkgVersion;
|
|
}
|
|
|
|
async blockPackageByFullname(name: string, reason: string) {
|
|
const [scope, pkgName] = getScopeAndName(name);
|
|
const pkg = await this.packageRepository.findPackage(scope, pkgName);
|
|
if (!pkg) {
|
|
throw new NotFoundError(`Package name(${name}) not found`);
|
|
}
|
|
return await this.blockPackage(pkg, reason);
|
|
}
|
|
|
|
async blockPackage(pkg: Package, reason: string) {
|
|
let block = await this.packageVersionBlockRepository.findPackageBlock(
|
|
pkg.packageId
|
|
);
|
|
if (block) {
|
|
block.reason = reason;
|
|
} else {
|
|
block = PackageVersionBlock.create({
|
|
packageId: pkg.packageId,
|
|
version: '*',
|
|
reason,
|
|
});
|
|
}
|
|
await this.packageVersionBlockRepository.savePackageVersionBlock(block);
|
|
if (pkg.manifestsDist && pkg.abbreviatedsDist) {
|
|
const fullManifests =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
pkg.manifestsDist
|
|
);
|
|
if (fullManifests) {
|
|
fullManifests.block = reason;
|
|
}
|
|
const abbreviatedManifests =
|
|
await this.distRepository.readDistBytesToJSON<AbbreviatedPackageManifestType>(
|
|
pkg.abbreviatedsDist
|
|
);
|
|
if (abbreviatedManifests) {
|
|
abbreviatedManifests.block = reason;
|
|
}
|
|
await this._updatePackageManifestsToDists(
|
|
pkg,
|
|
fullManifests || null,
|
|
abbreviatedManifests || null
|
|
);
|
|
this.eventBus.emit(PACKAGE_BLOCKED, pkg.fullname);
|
|
this.logger.info(
|
|
'[packageManagerService.blockPackage:success] packageId: %s, reason: %j',
|
|
pkg.packageId,
|
|
reason
|
|
);
|
|
}
|
|
return block;
|
|
}
|
|
|
|
async unblockPackageByFullname(name: string) {
|
|
const [scope, pkgName] = getScopeAndName(name);
|
|
const pkg = await this.packageRepository.findPackage(scope, pkgName);
|
|
if (!pkg) {
|
|
throw new NotFoundError(`Package name(${name}) not found`);
|
|
}
|
|
return await this.unblockPackage(pkg);
|
|
}
|
|
|
|
async unblockPackage(pkg: Package) {
|
|
const block =
|
|
await this.packageVersionBlockRepository.findPackageVersionBlock(
|
|
pkg.packageId,
|
|
'*'
|
|
);
|
|
if (block) {
|
|
await this.packageVersionBlockRepository.removePackageVersionBlock(
|
|
block.packageVersionBlockId
|
|
);
|
|
}
|
|
if (pkg.manifestsDist && pkg.abbreviatedsDist) {
|
|
const fullManifests =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
pkg.manifestsDist
|
|
);
|
|
if (fullManifests) {
|
|
fullManifests.block = undefined;
|
|
}
|
|
const abbreviatedManifests =
|
|
await this.distRepository.readDistBytesToJSON<AbbreviatedPackageManifestType>(
|
|
pkg.abbreviatedsDist
|
|
);
|
|
if (abbreviatedManifests) {
|
|
abbreviatedManifests.block = undefined;
|
|
}
|
|
await this._updatePackageManifestsToDists(
|
|
pkg,
|
|
fullManifests || null,
|
|
abbreviatedManifests || null
|
|
);
|
|
this.eventBus.emit(PACKAGE_UNBLOCKED, pkg.fullname);
|
|
this.logger.info(
|
|
'[packageManagerService.unblockPackage:success] packageId: %s',
|
|
pkg.packageId
|
|
);
|
|
}
|
|
}
|
|
|
|
async replacePackageMaintainersAndDist(pkg: Package, maintainers: User[]) {
|
|
await this.packageRepository.replacePackageMaintainers(
|
|
pkg.packageId,
|
|
maintainers.map(m => m.userId)
|
|
);
|
|
await this.refreshPackageMaintainersToDists(pkg);
|
|
this.eventBus.emit(PACKAGE_MAINTAINER_CHANGED, pkg.fullname, maintainers);
|
|
}
|
|
|
|
async savePackageMaintainers(pkg: Package, maintainers: User[]) {
|
|
let hasNewRecord = false;
|
|
for (const maintainer of maintainers) {
|
|
const newRecord = await this.packageRepository.savePackageMaintainer(
|
|
pkg.packageId,
|
|
maintainer.userId
|
|
);
|
|
if (newRecord) {
|
|
hasNewRecord = true;
|
|
}
|
|
}
|
|
if (hasNewRecord) {
|
|
this.eventBus.emit(PACKAGE_MAINTAINER_CHANGED, pkg.fullname, maintainers);
|
|
}
|
|
}
|
|
|
|
async removePackageMaintainer(pkg: Package, maintainer: User) {
|
|
await this.packageRepository.removePackageMaintainer(
|
|
pkg.packageId,
|
|
maintainer.userId
|
|
);
|
|
this.eventBus.emit(
|
|
PACKAGE_MAINTAINER_REMOVED,
|
|
pkg.fullname,
|
|
maintainer.name
|
|
);
|
|
}
|
|
|
|
async refreshPackageMaintainersToDists(pkg: Package) {
|
|
await this._refreshPackageManifestRootAttributeOnlyToDists(
|
|
pkg,
|
|
'maintainers'
|
|
);
|
|
}
|
|
|
|
async refreshPackageDistTagsToDists(pkg: Package) {
|
|
await this._refreshPackageManifestRootAttributeOnlyToDists(
|
|
pkg,
|
|
'dist-tags'
|
|
);
|
|
}
|
|
|
|
async listPackageFullManifests(scope: string, name: string, isSync = false) {
|
|
return await this._listPackageFullOrAbbreviatedManifests<PackageManifestType>(
|
|
scope,
|
|
name,
|
|
true,
|
|
isSync
|
|
);
|
|
}
|
|
|
|
async listPackageAbbreviatedManifests(
|
|
scope: string,
|
|
name: string,
|
|
isSync = false
|
|
) {
|
|
return await this._listPackageFullOrAbbreviatedManifests(
|
|
scope,
|
|
name,
|
|
false,
|
|
isSync
|
|
);
|
|
}
|
|
|
|
async showPackageVersionByVersionOrTag(
|
|
scope: string,
|
|
name: string,
|
|
spec: string
|
|
): Promise<{
|
|
blockReason?: string;
|
|
pkg?: Package;
|
|
packageVersion?: PackageVersion | null;
|
|
}> {
|
|
const pkg = await this.packageRepository.findPackage(scope, name);
|
|
if (!pkg) return {};
|
|
const block = await this.packageVersionBlockRepository.findPackageBlock(
|
|
pkg.packageId
|
|
);
|
|
if (block) {
|
|
return { blockReason: block.reason, pkg };
|
|
}
|
|
const fullname = getFullname(scope, name);
|
|
const result = npa(`${fullname}@${spec}`);
|
|
const version = await this.packageVersionService.getVersion(result);
|
|
if (!version) {
|
|
return {};
|
|
}
|
|
const packageVersion = await this.packageRepository.findPackageVersion(
|
|
pkg.packageId,
|
|
version
|
|
);
|
|
return { packageVersion, pkg };
|
|
}
|
|
|
|
async showPackageVersionManifest(
|
|
scope: string,
|
|
name: string,
|
|
spec: string,
|
|
isSync = false,
|
|
isFullManifests = false
|
|
) {
|
|
const pkg = await this.packageRepository.findPackage(scope, name);
|
|
if (!pkg) return {};
|
|
const block = await this.packageVersionBlockRepository.findPackageBlock(
|
|
pkg.packageId
|
|
);
|
|
if (block) {
|
|
return { blockReason: block.reason, pkg };
|
|
}
|
|
const fullname = getFullname(scope, name);
|
|
const result = npa(`${fullname}@${spec}`);
|
|
const manifest = await this.packageVersionService.readManifest(
|
|
pkg.packageId,
|
|
result,
|
|
isFullManifests,
|
|
!isSync
|
|
);
|
|
return { manifest, blockReason: null, pkg };
|
|
}
|
|
|
|
async downloadPackageVersionTar(packageVersion: PackageVersion) {
|
|
return await this.distRepository.downloadDist(packageVersion.tarDist);
|
|
}
|
|
|
|
public plusPackageVersionCounter(fullname: string, version: string) {
|
|
// set counter + 1, schedule will store them into database
|
|
const counters: Record<
|
|
string,
|
|
Record<string, number>
|
|
> = PackageManagerService.downloadCounters;
|
|
if (!counters[fullname]) counters[fullname] = {};
|
|
counters[fullname][version] = (counters[fullname][version] || 0) + 1;
|
|
// Total
|
|
const ALL = '*';
|
|
if (!counters[TOTAL]) counters[TOTAL] = {};
|
|
counters[TOTAL][ALL] = (counters[TOTAL][ALL] || 0) + 1;
|
|
// scope total
|
|
const scope = getScopeAndName(fullname)[0];
|
|
if (scope) {
|
|
const scopeKey = `${SCOPE_TOTAL_PREFIX}${scope}`;
|
|
if (!counters[scopeKey]) counters[scopeKey] = {};
|
|
counters[scopeKey][ALL] = (counters[scopeKey][ALL] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
// will be call by schedule/SavePackageVersionDownloadCounter.ts
|
|
async savePackageVersionCounters() {
|
|
// { [fullname]: { [version]: number } }
|
|
const counters: Record<
|
|
string,
|
|
Record<string, number>
|
|
> = PackageManagerService.downloadCounters;
|
|
const fullnames = Object.keys(counters);
|
|
if (fullnames.length === 0) return;
|
|
|
|
PackageManagerService.downloadCounters = {};
|
|
this.logger.info(
|
|
'[packageManagerService.savePackageVersionCounters:saving] %d fullnames',
|
|
fullnames.length
|
|
);
|
|
|
|
let total = 0;
|
|
for (const fullname in counters) {
|
|
const versions = counters[fullname];
|
|
let packageId: string | null = null;
|
|
if (fullname === TOTAL) {
|
|
packageId = 'total';
|
|
} else if (fullname.startsWith(SCOPE_TOTAL_PREFIX)) {
|
|
packageId = fullname.replace(SCOPE_TOTAL_PREFIX, '');
|
|
} else {
|
|
// find packageId from fullname
|
|
const [scope, name] = getScopeAndName(fullname);
|
|
packageId = await this.packageRepository.findPackageId(scope, name);
|
|
}
|
|
if (!packageId) continue;
|
|
|
|
for (const version in versions) {
|
|
const counter = versions[version];
|
|
await this.packageVersionDownloadRepository.plus(
|
|
packageId,
|
|
version,
|
|
counter
|
|
);
|
|
total += counter;
|
|
}
|
|
}
|
|
this.logger.info(
|
|
'[packageManagerService.savePackageVersionCounters:saved] %d total',
|
|
total
|
|
);
|
|
}
|
|
|
|
public async saveDeprecatedVersions(
|
|
pkg: Package,
|
|
deprecatedList: { version: string; deprecated: string }[]
|
|
) {
|
|
const updateVersions: string[] = [];
|
|
for (const { version, deprecated } of deprecatedList) {
|
|
const pkgVersion = await this.packageRepository.findPackageVersion(
|
|
pkg.packageId,
|
|
version
|
|
);
|
|
if (!pkgVersion) continue;
|
|
const message = deprecated === '' ? undefined : deprecated;
|
|
await this._mergeManifestDist(pkgVersion.manifestDist, {
|
|
deprecated: message,
|
|
});
|
|
await this._mergeManifestDist(pkgVersion.abbreviatedDist, {
|
|
deprecated: message,
|
|
});
|
|
await this.packageRepository.savePackageVersion(pkgVersion);
|
|
updateVersions.push(version);
|
|
}
|
|
await this.refreshPackageChangeVersionsToDists(pkg, updateVersions);
|
|
this.eventBus.emit(PACKAGE_META_CHANGED, pkg.fullname, {
|
|
deprecateds: deprecatedList,
|
|
});
|
|
}
|
|
|
|
public async savePackageVersionManifest(
|
|
pkgVersion: PackageVersion,
|
|
mergeManifest: object,
|
|
mergeAbbreviated: object
|
|
) {
|
|
await this._mergeManifestDist(pkgVersion.manifestDist, mergeManifest);
|
|
await this._mergeManifestDist(pkgVersion.abbreviatedDist, mergeAbbreviated);
|
|
}
|
|
|
|
/**
|
|
* save package version readme
|
|
*/
|
|
public async savePackageVersionReadme(
|
|
pkgVersion: PackageVersion,
|
|
readmeFile: string
|
|
) {
|
|
await this.distRepository.saveDist(pkgVersion.readmeDist, readmeFile);
|
|
this.logger.info(
|
|
'[PackageManagerService.savePackageVersionReadme] save packageVersionId:%s readme:%s to dist:%s',
|
|
pkgVersion.packageVersionId,
|
|
readmeFile,
|
|
pkgVersion.readmeDist.distId
|
|
);
|
|
}
|
|
|
|
public async savePackageReadme(pkg: Package, readmeFile: string) {
|
|
if (!pkg.manifestsDist) return;
|
|
const fullManifests =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
pkg.manifestsDist
|
|
);
|
|
if (!fullManifests) return;
|
|
fullManifests.readme = await readFile(readmeFile, 'utf-8');
|
|
await this._updatePackageManifestsToDists(pkg, fullManifests, null);
|
|
this.logger.info(
|
|
'[PackageManagerService.savePackageReadme] save packageId:%s readme, size: %s',
|
|
pkg.packageId,
|
|
fullManifests.readme.length
|
|
);
|
|
}
|
|
|
|
private async _removePackageVersionAndDist(pkgVersion: PackageVersion) {
|
|
// remove nfs dists
|
|
await Promise.all([
|
|
this.distRepository.destroyDist(pkgVersion.abbreviatedDist),
|
|
this.distRepository.destroyDist(pkgVersion.manifestDist),
|
|
this.distRepository.destroyDist(pkgVersion.readmeDist),
|
|
this.distRepository.destroyDist(pkgVersion.tarDist),
|
|
]);
|
|
// remove from repository
|
|
await this.packageRepository.removePackageVersion(pkgVersion);
|
|
}
|
|
|
|
public async unpublishPackage(pkg: Package) {
|
|
const pkgVersions = await this.packageRepository.listPackageVersions(
|
|
pkg.packageId
|
|
);
|
|
// already unpublished
|
|
if (pkgVersions.length === 0) {
|
|
this.logger.info(
|
|
`[packageManagerService.unpublishPackage:skip] ${pkg.packageId} already unpublished`
|
|
);
|
|
return;
|
|
}
|
|
for (const pkgVersion of pkgVersions) {
|
|
await this._removePackageVersionAndDist(pkgVersion);
|
|
}
|
|
// set unpublished dist to package's manifestDist and abbreviatedDist
|
|
const unpublishedInfo = {
|
|
_id: pkg.fullname,
|
|
name: pkg.fullname,
|
|
time: {
|
|
created: pkg.createdAt,
|
|
modified: pkg.updatedAt,
|
|
unpublished: new Date(),
|
|
},
|
|
// keep this property exists for forward compatibility
|
|
// https://github.com/cnpm/cnpmjs.org/blob/ad622d55e384743b48e79bb6aec574a7f354ee9f/controllers/sync_module_worker.js#L828
|
|
'dist-tags': {},
|
|
};
|
|
await this._mergeManifestDist(
|
|
pkg.manifestsDist!,
|
|
undefined,
|
|
unpublishedInfo
|
|
);
|
|
await this._mergeManifestDist(
|
|
pkg.abbreviatedsDist!,
|
|
undefined,
|
|
unpublishedInfo
|
|
);
|
|
this.eventBus.emit(PACKAGE_UNPUBLISHED, pkg.fullname);
|
|
}
|
|
|
|
public async removePackageVersion(
|
|
pkg: Package,
|
|
pkgVersion: PackageVersion,
|
|
skipRefreshPackageManifests = false
|
|
) {
|
|
const currentVersions =
|
|
await this.packageRepository.listPackageVersionNames(pkg.packageId);
|
|
// only one version, unpublish the package
|
|
if (
|
|
currentVersions.length === 1 &&
|
|
currentVersions[0] === pkgVersion.version
|
|
) {
|
|
await this.unpublishPackage(pkg);
|
|
return;
|
|
}
|
|
// remove version & update tags
|
|
await this._removePackageVersionAndDist(pkgVersion);
|
|
const versions = await this.packageRepository.listPackageVersionNames(
|
|
pkg.packageId
|
|
);
|
|
if (versions.length > 0) {
|
|
let updateTag: string | undefined;
|
|
// make sure latest tag exists
|
|
const latestTag = await this.packageRepository.findPackageTag(
|
|
pkg.packageId,
|
|
'latest'
|
|
);
|
|
if (latestTag?.version === pkgVersion.version) {
|
|
// change latest version
|
|
// https://github.com/npm/libnpmpublish/blob/main/unpublish.js#L62
|
|
const latestVersion = versions.sort(semver.compareLoose).pop();
|
|
if (latestVersion) {
|
|
updateTag = latestTag.tag;
|
|
await this.savePackageTag(pkg, latestTag.tag, latestVersion, true);
|
|
}
|
|
}
|
|
if (skipRefreshPackageManifests !== true) {
|
|
await this.refreshPackageChangeVersionsToDists(pkg, undefined, [
|
|
pkgVersion.version,
|
|
]);
|
|
this.eventBus.emit(
|
|
PACKAGE_VERSION_REMOVED,
|
|
pkg.fullname,
|
|
pkgVersion.version,
|
|
updateTag
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
public async savePackageTag(
|
|
pkg: Package,
|
|
tag: string,
|
|
version: string,
|
|
skipEvent = false
|
|
) {
|
|
let tagEntity = await this.packageRepository.findPackageTag(
|
|
pkg.packageId,
|
|
tag
|
|
);
|
|
if (!tagEntity) {
|
|
tagEntity = PackageTag.create({
|
|
packageId: pkg.packageId,
|
|
tag,
|
|
version,
|
|
});
|
|
await this.packageRepository.savePackageTag(tagEntity);
|
|
await this._refreshPackageManifestRootAttributeOnlyToDists(
|
|
pkg,
|
|
'dist-tags'
|
|
);
|
|
if (!skipEvent) {
|
|
this.eventBus.emit(PACKAGE_TAG_ADDED, pkg.fullname, tagEntity.tag);
|
|
}
|
|
return true;
|
|
}
|
|
if (tagEntity.version === version) {
|
|
// nothing change
|
|
return false;
|
|
}
|
|
tagEntity.version = version;
|
|
await this.packageRepository.savePackageTag(tagEntity);
|
|
await this._refreshPackageManifestRootAttributeOnlyToDists(
|
|
pkg,
|
|
'dist-tags'
|
|
);
|
|
if (!skipEvent) {
|
|
this.eventBus.emit(PACKAGE_TAG_CHANGED, pkg.fullname, tagEntity.tag);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public async removePackageTag(pkg: Package, tag: string) {
|
|
const tagEntity = await this.packageRepository.findPackageTag(
|
|
pkg.packageId,
|
|
tag
|
|
);
|
|
if (!tagEntity) return false;
|
|
await this.packageRepository.removePackageTag(tagEntity);
|
|
await this._refreshPackageManifestRootAttributeOnlyToDists(
|
|
pkg,
|
|
'dist-tags'
|
|
);
|
|
this.eventBus.emit(PACKAGE_TAG_REMOVED, pkg.fullname, tagEntity.tag);
|
|
return true;
|
|
}
|
|
|
|
public async refreshPackageChangeVersionsToDists(
|
|
pkg: Package,
|
|
updateVersions?: string[],
|
|
removeVersions?: string[]
|
|
) {
|
|
if (!pkg.manifestsDist?.distId || !pkg.abbreviatedsDist?.distId) {
|
|
return await this._refreshPackageManifestsToDists(pkg);
|
|
}
|
|
const fullManifests =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
pkg.manifestsDist
|
|
);
|
|
const abbreviatedManifests =
|
|
await this.distRepository.readDistBytesToJSON<AbbreviatedPackageManifestType>(
|
|
pkg.abbreviatedsDist
|
|
);
|
|
if (!fullManifests?.versions || !abbreviatedManifests?.versions) {
|
|
// is unpublished, refresh all again
|
|
return await this._refreshPackageManifestsToDists(pkg);
|
|
}
|
|
|
|
if (updateVersions) {
|
|
for (const version of updateVersions) {
|
|
const packageVersion = await this.packageRepository.findPackageVersion(
|
|
pkg.packageId,
|
|
version
|
|
);
|
|
if (packageVersion) {
|
|
const manifest =
|
|
await this.distRepository.readDistBytesToJSON<PackageJSONType>(
|
|
packageVersion.manifestDist
|
|
);
|
|
if (!manifest) continue;
|
|
if ('readme' in manifest) {
|
|
delete manifest.readme;
|
|
}
|
|
fullManifests.versions[packageVersion.version] = manifest;
|
|
fullManifests.time[packageVersion.version] =
|
|
packageVersion.publishTime;
|
|
|
|
const abbreviatedManifest =
|
|
await this.distRepository.readDistBytesToJSON<AbbreviatedPackageJSONType>(
|
|
packageVersion.abbreviatedDist
|
|
);
|
|
if (abbreviatedManifest) {
|
|
abbreviatedManifests.versions[packageVersion.version] =
|
|
abbreviatedManifest;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (removeVersions) {
|
|
for (const version of removeVersions) {
|
|
delete fullManifests.versions[version];
|
|
delete fullManifests.time[version];
|
|
delete abbreviatedManifests.versions[version];
|
|
}
|
|
}
|
|
|
|
// update dist-tags
|
|
await this._setPackageDistTagsAndLatestInfos(
|
|
pkg,
|
|
fullManifests,
|
|
abbreviatedManifests
|
|
);
|
|
// store to nfs dist
|
|
await this._updatePackageManifestsToDists(
|
|
pkg,
|
|
fullManifests,
|
|
abbreviatedManifests
|
|
);
|
|
}
|
|
|
|
async getSourceRegistry(pkg: Package): Promise<Registry | null> {
|
|
let registry: Registry | null;
|
|
if (pkg.registryId) {
|
|
registry = await this.registryManagerService.findByRegistryId(
|
|
pkg.registryId
|
|
);
|
|
} else {
|
|
registry = await this.registryManagerService.ensureDefaultRegistry();
|
|
}
|
|
return registry;
|
|
}
|
|
|
|
private async _listPackageDistTags(pkg: Package) {
|
|
const tags = await this.packageRepository.listPackageTags(pkg.packageId);
|
|
const distTags: { [key: string]: string } = {};
|
|
for (const tag of tags) {
|
|
distTags[tag.tag] = tag.version;
|
|
}
|
|
return distTags;
|
|
}
|
|
|
|
// refresh package full manifests and abbreviated manifests to NFS
|
|
private async _refreshPackageManifestsToDists(pkg: Package) {
|
|
const [fullManifests, abbreviatedManifests] = await Promise.all([
|
|
this._listPackageFullManifests(pkg),
|
|
this._listPackageAbbreviatedManifests(pkg),
|
|
]);
|
|
await this._updatePackageManifestsToDists(
|
|
pkg,
|
|
fullManifests,
|
|
abbreviatedManifests
|
|
);
|
|
}
|
|
|
|
// only refresh root attributes only, e.g.: dist-tags, maintainers
|
|
private async _refreshPackageManifestRootAttributeOnlyToDists(
|
|
pkg: Package,
|
|
refreshAttr: 'dist-tags' | 'maintainers'
|
|
) {
|
|
if (refreshAttr === 'maintainers') {
|
|
const fullManifests =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
pkg.manifestsDist!
|
|
);
|
|
const maintainers = await this._listPackageMaintainers(pkg);
|
|
if (fullManifests) {
|
|
fullManifests.maintainers = maintainers;
|
|
await this._updatePackageManifestsToDists(pkg, fullManifests, null);
|
|
}
|
|
} else if (refreshAttr === 'dist-tags') {
|
|
const fullManifests =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
pkg.manifestsDist!
|
|
);
|
|
if (fullManifests) {
|
|
const abbreviatedManifests =
|
|
await this.distRepository.readDistBytesToJSON<AbbreviatedPackageManifestType>(
|
|
pkg.abbreviatedsDist!
|
|
);
|
|
if (abbreviatedManifests) {
|
|
await this._setPackageDistTagsAndLatestInfos(
|
|
pkg,
|
|
fullManifests,
|
|
abbreviatedManifests
|
|
);
|
|
}
|
|
await this._updatePackageManifestsToDists(
|
|
pkg,
|
|
fullManifests,
|
|
abbreviatedManifests || null
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _mergeLatestManifestFields(
|
|
fullManifests: PackageManifestType,
|
|
latestManifest: PackageJSONType | null
|
|
) {
|
|
if (!latestManifest) return;
|
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#full-metadata-format
|
|
const fieldsFromLatestManifest = [
|
|
'author',
|
|
'bugs',
|
|
'contributors',
|
|
'description',
|
|
'homepage',
|
|
'keywords',
|
|
'license',
|
|
'readmeFilename',
|
|
'repository',
|
|
] as const;
|
|
// the latest version metas
|
|
for (const field of fieldsFromLatestManifest) {
|
|
if (latestManifest[field]) {
|
|
(fullManifests as Record<string, unknown>)[field] =
|
|
latestManifest[field];
|
|
}
|
|
}
|
|
}
|
|
|
|
private async _setPackageDistTagsAndLatestInfos(
|
|
pkg: Package,
|
|
fullManifests: PackageManifestType,
|
|
abbreviatedManifests: AbbreviatedPackageManifestType
|
|
) {
|
|
const distTags = await this._listPackageDistTags(pkg);
|
|
if (distTags.latest) {
|
|
const packageVersion = await this.packageRepository.findPackageVersion(
|
|
pkg.packageId,
|
|
distTags.latest
|
|
);
|
|
if (packageVersion) {
|
|
fullManifests.readme = await this.distRepository.readDistBytesToString(
|
|
packageVersion.readmeDist
|
|
);
|
|
const latestManifest =
|
|
await this.distRepository.readDistBytesToJSON<PackageJSONType>(
|
|
packageVersion.manifestDist
|
|
);
|
|
this._mergeLatestManifestFields(fullManifests, latestManifest || null);
|
|
}
|
|
}
|
|
fullManifests['dist-tags'] = distTags;
|
|
abbreviatedManifests['dist-tags'] = distTags;
|
|
}
|
|
|
|
private async _mergeManifestDist(
|
|
manifestDist: Dist,
|
|
mergeData?: any,
|
|
replaceData?: any
|
|
) {
|
|
let manifest =
|
|
await this.distRepository.readDistBytesToJSON<PackageManifestType>(
|
|
manifestDist
|
|
);
|
|
if (mergeData && manifest) {
|
|
Object.assign(manifest, mergeData);
|
|
}
|
|
if (replaceData) {
|
|
manifest = replaceData;
|
|
}
|
|
const manifestBytes = Buffer.from(JSON.stringify(manifest));
|
|
const manifestIntegrity = await calculateIntegrity(manifestBytes);
|
|
manifestDist.size = manifestBytes.length;
|
|
manifestDist.shasum = manifestIntegrity.shasum;
|
|
manifestDist.integrity = manifestIntegrity.integrity;
|
|
await this.distRepository.saveDist(manifestDist, manifestBytes);
|
|
}
|
|
|
|
private async _updatePackageManifestsToDists(
|
|
pkg: Package,
|
|
fullManifests: PackageManifestType | null,
|
|
abbreviatedManifests: AbbreviatedPackageManifestType | null
|
|
): Promise<void> {
|
|
const modified = new Date();
|
|
if (fullManifests) {
|
|
fullManifests.time.modified = modified;
|
|
// same to dist
|
|
const fullManifestsDistBytes = Buffer.from(JSON.stringify(fullManifests));
|
|
const fullManifestsDistIntegrity = await calculateIntegrity(
|
|
fullManifestsDistBytes
|
|
);
|
|
if (pkg.manifestsDist?.distId) {
|
|
pkg.manifestsDist.size = fullManifestsDistBytes.length;
|
|
pkg.manifestsDist.shasum = fullManifestsDistIntegrity.shasum;
|
|
pkg.manifestsDist.integrity = fullManifestsDistIntegrity.integrity;
|
|
} else {
|
|
pkg.manifestsDist = pkg.createFullManifests({
|
|
size: fullManifestsDistBytes.length,
|
|
shasum: fullManifestsDistIntegrity.shasum,
|
|
integrity: fullManifestsDistIntegrity.integrity,
|
|
});
|
|
}
|
|
await this.distRepository.saveDist(
|
|
pkg.manifestsDist,
|
|
fullManifestsDistBytes
|
|
);
|
|
await this.packageRepository.savePackageDist(pkg, true);
|
|
}
|
|
if (abbreviatedManifests) {
|
|
abbreviatedManifests.modified = modified;
|
|
const abbreviatedManifestsDistBytes = Buffer.from(
|
|
JSON.stringify(abbreviatedManifests)
|
|
);
|
|
const abbreviatedManifestsDistIntegrity = await calculateIntegrity(
|
|
abbreviatedManifestsDistBytes
|
|
);
|
|
if (pkg.abbreviatedsDist?.distId) {
|
|
pkg.abbreviatedsDist.size = abbreviatedManifestsDistBytes.length;
|
|
pkg.abbreviatedsDist.shasum = abbreviatedManifestsDistIntegrity.shasum;
|
|
pkg.abbreviatedsDist.integrity =
|
|
abbreviatedManifestsDistIntegrity.integrity;
|
|
} else {
|
|
pkg.abbreviatedsDist = pkg.createAbbreviatedManifests({
|
|
size: abbreviatedManifestsDistBytes.length,
|
|
shasum: abbreviatedManifestsDistIntegrity.shasum,
|
|
integrity: abbreviatedManifestsDistIntegrity.integrity,
|
|
});
|
|
}
|
|
await this.distRepository.saveDist(
|
|
pkg.abbreviatedsDist,
|
|
abbreviatedManifestsDistBytes
|
|
);
|
|
await this.packageRepository.savePackageDist(pkg, false);
|
|
}
|
|
}
|
|
|
|
private async _listPackageFullOrAbbreviatedManifests<
|
|
T extends PackageManifestType | AbbreviatedPackageManifestType,
|
|
>(scope: string, name: string, isFullManifests: boolean, isSync: boolean) {
|
|
let etag = '';
|
|
let blockReason = '';
|
|
const pkg = await this.packageRepository.findPackage(scope, name);
|
|
if (!pkg) return { etag, data: null, blockReason };
|
|
const registry = await this.getSourceRegistry(pkg);
|
|
|
|
const block = await this.packageVersionBlockRepository.findPackageBlock(
|
|
pkg.packageId
|
|
);
|
|
if (block) {
|
|
blockReason = block.reason;
|
|
}
|
|
|
|
let bugVersion: BugVersion | undefined;
|
|
// sync mode response no bug version fixed
|
|
if (!isSync) {
|
|
bugVersion = await this.bugVersionService.getBugVersion();
|
|
}
|
|
const fullname = getFullname(scope, name);
|
|
|
|
let dist = isFullManifests ? pkg.manifestsDist : pkg.abbreviatedsDist;
|
|
// read from dist
|
|
if (dist?.distId) {
|
|
etag = `"${dist.shasum}"`;
|
|
const data = (await this.distRepository.readDistBytesToJSON(dist)) as T;
|
|
if (bugVersion) {
|
|
await this.bugVersionService.fixPackageBugVersions(
|
|
bugVersion,
|
|
fullname,
|
|
data.versions
|
|
);
|
|
}
|
|
// set _source_registry_name in full manifestDist
|
|
if (registry) {
|
|
data._source_registry_name = registry?.name;
|
|
}
|
|
|
|
const distBytes = Buffer.from(JSON.stringify(data));
|
|
const distIntegrity = await calculateIntegrity(distBytes);
|
|
etag = `"${distIntegrity.shasum}"`;
|
|
return { etag, data, blockReason };
|
|
}
|
|
|
|
// read from database
|
|
const fullManifests = isFullManifests
|
|
? await this._listPackageFullManifests(pkg)
|
|
: null;
|
|
const abbreviatedManifests = isFullManifests
|
|
? null
|
|
: await this._listPackageAbbreviatedManifests(pkg);
|
|
if (!fullManifests && !abbreviatedManifests) {
|
|
// not exists
|
|
return { etag, data: null, blockReason };
|
|
}
|
|
await this._updatePackageManifestsToDists(
|
|
pkg,
|
|
fullManifests,
|
|
abbreviatedManifests
|
|
);
|
|
const manifests = (fullManifests || abbreviatedManifests)! as T;
|
|
/* c8 ignore next 5 */
|
|
if (bugVersion) {
|
|
await this.bugVersionService.fixPackageBugVersions(
|
|
bugVersion,
|
|
fullname,
|
|
(manifests as any).versions
|
|
);
|
|
const distBytes = Buffer.from(JSON.stringify(manifests));
|
|
const distIntegrity = await calculateIntegrity(distBytes);
|
|
etag = `"${distIntegrity.shasum}"`;
|
|
} else {
|
|
dist = isFullManifests ? pkg.manifestsDist : pkg.abbreviatedsDist;
|
|
etag = `"${dist!.shasum}"`;
|
|
}
|
|
return { etag, data: manifests, blockReason };
|
|
}
|
|
|
|
private async _listPackageMaintainers(pkg: Package) {
|
|
const users = await this.packageRepository.listPackageMaintainers(
|
|
pkg.packageId
|
|
);
|
|
return users.map(({ displayName, email }) => ({
|
|
name: displayName,
|
|
email,
|
|
}));
|
|
}
|
|
|
|
private async _listPackageFullManifests(
|
|
pkg: Package
|
|
): Promise<PackageManifestType | null> {
|
|
// read all verions from db
|
|
const packageVersions = await this.packageRepository.listPackageVersions(
|
|
pkg.packageId
|
|
);
|
|
if (packageVersions.length === 0) return null;
|
|
|
|
const distTags = await this._listPackageDistTags(pkg);
|
|
const maintainers = await this._listPackageMaintainers(pkg);
|
|
const registry = await this.getSourceRegistry(pkg);
|
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#full-metadata-format
|
|
const data: PackageManifestType = {
|
|
_id: `${pkg.fullname}`,
|
|
_rev: `${pkg.id}-${pkg.packageId}`,
|
|
'dist-tags': distTags,
|
|
// the package name
|
|
name: pkg.fullname,
|
|
// an object mapping versions to the time published, along with created and modified timestamps
|
|
time: {
|
|
// '1.0.0': '2012-09-18T14:46:08.346Z',
|
|
created: pkg.createdAt,
|
|
modified: pkg.updatedAt,
|
|
},
|
|
// a mapping of semver-compliant version numbers to version data
|
|
versions: {},
|
|
// The following fields are hoisted to the top-level of the package json from the latest version published:
|
|
// human object
|
|
author: undefined,
|
|
bugs: undefined,
|
|
description: pkg.description,
|
|
homepage: undefined,
|
|
keywords: undefined,
|
|
// the SPDX identifier of the package's license
|
|
license: undefined,
|
|
// array of human objects for people with permission to publish this package; not authoritative but informational
|
|
maintainers,
|
|
// contributors: array of human objects
|
|
// the first 64K of the README data for the most-recently published version of the package
|
|
readme: '',
|
|
// The name of the file from which the readme data was taken
|
|
readmeFilename: undefined,
|
|
// as given in package.json, for the latest version
|
|
repository: undefined,
|
|
// users: an object whose keys are the npm user names of people who have starred this package
|
|
_source_registry_name: registry?.name,
|
|
};
|
|
|
|
let latestTagVersion = '';
|
|
if (distTags.latest) {
|
|
latestTagVersion = distTags.latest;
|
|
}
|
|
|
|
let latestManifest: any;
|
|
let latestPackageVersion = packageVersions[0];
|
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#package-metadata
|
|
for (const packageVersion of packageVersions) {
|
|
const manifest =
|
|
await this.distRepository.readDistBytesToJSON<PackageJSONType>(
|
|
packageVersion.manifestDist
|
|
);
|
|
if (!manifest) continue;
|
|
/* c8 ignore next 3 */
|
|
if ('readme' in manifest) {
|
|
delete manifest.readme;
|
|
}
|
|
if (latestTagVersion && packageVersion.version === latestTagVersion) {
|
|
latestManifest = manifest;
|
|
latestPackageVersion = packageVersion;
|
|
}
|
|
data.versions[packageVersion.version] = manifest;
|
|
data.time[packageVersion.version] = packageVersion.publishTime;
|
|
}
|
|
// the latest version readme
|
|
data.readme = await this.distRepository.readDistBytesToString(
|
|
latestPackageVersion.readmeDist
|
|
);
|
|
if (!latestManifest) {
|
|
latestManifest = data.versions[latestPackageVersion.version];
|
|
}
|
|
this._mergeLatestManifestFields(data, latestManifest);
|
|
return data;
|
|
}
|
|
|
|
private async _listPackageAbbreviatedManifests(
|
|
pkg: Package
|
|
): Promise<AbbreviatedPackageManifestType | null> {
|
|
// read all verions from db
|
|
const packageVersions = await this.packageRepository.listPackageVersions(
|
|
pkg.packageId
|
|
);
|
|
if (packageVersions.length === 0) return null;
|
|
|
|
const distTags = await this._listPackageDistTags(pkg);
|
|
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#package-metadata
|
|
// tiny-tarball is a small package with only one version and no dependencies.
|
|
const data: AbbreviatedPackageManifestType = {
|
|
'dist-tags': distTags,
|
|
modified: pkg.updatedAt,
|
|
name: pkg.fullname,
|
|
versions: {},
|
|
};
|
|
|
|
for (const packageVersion of packageVersions) {
|
|
const manifest =
|
|
await this.distRepository.readDistBytesToJSON<AbbreviatedPackageJSONType>(
|
|
packageVersion.abbreviatedDist
|
|
);
|
|
if (manifest) {
|
|
data.versions[packageVersion.version] = manifest;
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
|
|
private async _checkPackageDepsVersion(pkgJSON: PackageJSONType) {
|
|
// 只校验 dependencies
|
|
// devDependencies、optionalDependencies、peerDependencies 不会影响依赖安装 不在这里进行校验
|
|
const { dependencies } = pkgJSON;
|
|
await pMap(
|
|
Object.entries(dependencies || {}),
|
|
async ([fullname, spec]) => {
|
|
try {
|
|
const specResult = npa(`${fullname}@${spec}`);
|
|
// 对于 git、alias、file 等类型的依赖,不进行版本校验
|
|
if (!['range', 'tag', 'version'].includes(specResult.type)) {
|
|
return;
|
|
}
|
|
const pkgVersion = await this.packageVersionService.getVersion(
|
|
npa(`${fullname}@${spec}`)
|
|
);
|
|
assert(pkgVersion);
|
|
} catch {
|
|
throw new BadRequestError(`deps ${fullname}@${spec} not found`);
|
|
}
|
|
},
|
|
{
|
|
concurrency: 12,
|
|
}
|
|
);
|
|
}
|
|
}
|