261 lines
6.9 KiB
TypeScript
261 lines
6.9 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
|
import type {
|
|
IProtocolUpstreamConfig,
|
|
IUpstreamFetchContext,
|
|
IUpstreamResult,
|
|
IUpstreamRegistryConfig,
|
|
} from '../upstream/interfaces.upstream.js';
|
|
import type { IPackument, INpmVersion } from './interfaces.npm.js';
|
|
|
|
/**
|
|
* NPM-specific upstream implementation.
|
|
*
|
|
* Handles:
|
|
* - Package metadata (packument) fetching
|
|
* - Tarball proxying
|
|
* - Scoped package routing (@scope/* patterns)
|
|
* - NPM-specific URL rewriting
|
|
*/
|
|
export class NpmUpstream extends BaseUpstream {
|
|
protected readonly protocolName = 'npm';
|
|
|
|
/** Local registry URL for rewriting tarball URLs */
|
|
private readonly localRegistryUrl: string;
|
|
|
|
constructor(
|
|
config: IProtocolUpstreamConfig,
|
|
localRegistryUrl: string,
|
|
logger?: plugins.smartlog.Smartlog,
|
|
) {
|
|
super(config, logger);
|
|
this.localRegistryUrl = localRegistryUrl;
|
|
}
|
|
|
|
/**
|
|
* Fetch a packument from upstream registries.
|
|
*/
|
|
public async fetchPackument(packageName: string): Promise<IPackument | null> {
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'npm',
|
|
resource: packageName,
|
|
resourceType: 'packument',
|
|
path: `/${encodeURIComponent(packageName).replace('%40', '@')}`,
|
|
method: 'GET',
|
|
headers: {
|
|
'accept': 'application/json',
|
|
},
|
|
query: {},
|
|
};
|
|
|
|
const result = await this.fetch(context);
|
|
|
|
if (!result || !result.success) {
|
|
return null;
|
|
}
|
|
|
|
// Parse and process packument
|
|
let packument: IPackument;
|
|
if (Buffer.isBuffer(result.body)) {
|
|
packument = JSON.parse(result.body.toString('utf8'));
|
|
} else {
|
|
packument = result.body;
|
|
}
|
|
|
|
// Rewrite tarball URLs to point to local registry
|
|
packument = this.rewriteTarballUrls(packument);
|
|
|
|
return packument;
|
|
}
|
|
|
|
/**
|
|
* Fetch a specific version from upstream registries.
|
|
*/
|
|
public async fetchVersion(packageName: string, version: string): Promise<INpmVersion | null> {
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'npm',
|
|
resource: packageName,
|
|
resourceType: 'version',
|
|
path: `/${encodeURIComponent(packageName).replace('%40', '@')}/${version}`,
|
|
method: 'GET',
|
|
headers: {
|
|
'accept': 'application/json',
|
|
},
|
|
query: {},
|
|
};
|
|
|
|
const result = await this.fetch(context);
|
|
|
|
if (!result || !result.success) {
|
|
return null;
|
|
}
|
|
|
|
let versionData: INpmVersion;
|
|
if (Buffer.isBuffer(result.body)) {
|
|
versionData = JSON.parse(result.body.toString('utf8'));
|
|
} else {
|
|
versionData = result.body;
|
|
}
|
|
|
|
// Rewrite tarball URL
|
|
if (versionData.dist?.tarball) {
|
|
versionData.dist.tarball = this.rewriteSingleTarballUrl(
|
|
packageName,
|
|
versionData.version,
|
|
versionData.dist.tarball,
|
|
);
|
|
}
|
|
|
|
return versionData;
|
|
}
|
|
|
|
/**
|
|
* Fetch a tarball from upstream registries.
|
|
*/
|
|
public async fetchTarball(packageName: string, version: string): Promise<Buffer | null> {
|
|
// First, try to get the tarball URL from packument
|
|
const packument = await this.fetchPackument(packageName);
|
|
let tarballPath: string;
|
|
|
|
if (packument?.versions?.[version]?.dist?.tarball) {
|
|
// Extract path from original (upstream) tarball URL
|
|
const tarballUrl = packument.versions[version].dist.tarball;
|
|
try {
|
|
const url = new URL(tarballUrl);
|
|
tarballPath = url.pathname;
|
|
} catch {
|
|
// Fallback to standard NPM tarball path
|
|
tarballPath = this.buildTarballPath(packageName, version);
|
|
}
|
|
} else {
|
|
tarballPath = this.buildTarballPath(packageName, version);
|
|
}
|
|
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'npm',
|
|
resource: packageName,
|
|
resourceType: 'tarball',
|
|
path: tarballPath,
|
|
method: 'GET',
|
|
headers: {
|
|
'accept': 'application/octet-stream',
|
|
},
|
|
query: {},
|
|
};
|
|
|
|
const result = await this.fetch(context);
|
|
|
|
if (!result || !result.success) {
|
|
return null;
|
|
}
|
|
|
|
return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body);
|
|
}
|
|
|
|
/**
|
|
* Search packages in upstream registries.
|
|
*/
|
|
public async search(text: string, size: number = 20, from: number = 0): Promise<any | null> {
|
|
const context: IUpstreamFetchContext = {
|
|
protocol: 'npm',
|
|
resource: '*',
|
|
resourceType: 'search',
|
|
path: '/-/v1/search',
|
|
method: 'GET',
|
|
headers: {
|
|
'accept': 'application/json',
|
|
},
|
|
query: {
|
|
text,
|
|
size: size.toString(),
|
|
from: from.toString(),
|
|
},
|
|
};
|
|
|
|
const result = await this.fetch(context);
|
|
|
|
if (!result || !result.success) {
|
|
return null;
|
|
}
|
|
|
|
if (Buffer.isBuffer(result.body)) {
|
|
return JSON.parse(result.body.toString('utf8'));
|
|
}
|
|
|
|
return result.body;
|
|
}
|
|
|
|
/**
|
|
* Build the standard NPM tarball path.
|
|
*/
|
|
private buildTarballPath(packageName: string, version: string): string {
|
|
// NPM uses: /{package}/-/{package-name}-{version}.tgz
|
|
// For scoped packages: /@scope/name/-/name-version.tgz
|
|
if (packageName.startsWith('@')) {
|
|
const [scope, name] = packageName.split('/');
|
|
return `/${scope}/${name}/-/${name}-${version}.tgz`;
|
|
} else {
|
|
return `/${packageName}/-/${packageName}-${version}.tgz`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rewrite all tarball URLs in a packument to point to local registry.
|
|
*/
|
|
private rewriteTarballUrls(packument: IPackument): IPackument {
|
|
if (!packument.versions) {
|
|
return packument;
|
|
}
|
|
|
|
const rewritten = { ...packument };
|
|
rewritten.versions = {};
|
|
|
|
for (const [version, versionData] of Object.entries(packument.versions)) {
|
|
const newVersionData = { ...versionData };
|
|
if (newVersionData.dist?.tarball) {
|
|
newVersionData.dist = {
|
|
...newVersionData.dist,
|
|
tarball: this.rewriteSingleTarballUrl(
|
|
packument.name,
|
|
version,
|
|
newVersionData.dist.tarball,
|
|
),
|
|
};
|
|
}
|
|
rewritten.versions[version] = newVersionData;
|
|
}
|
|
|
|
return rewritten;
|
|
}
|
|
|
|
/**
|
|
* Rewrite a single tarball URL to point to local registry.
|
|
*/
|
|
private rewriteSingleTarballUrl(
|
|
packageName: string,
|
|
version: string,
|
|
_originalUrl: string,
|
|
): string {
|
|
// Generate local tarball URL
|
|
// Format: {localRegistryUrl}/{package}/-/{package-name}-{version}.tgz
|
|
const safeName = packageName.replace('@', '').replace('/', '-');
|
|
return `${this.localRegistryUrl}/${packageName}/-/${safeName}-${version}.tgz`;
|
|
}
|
|
|
|
/**
|
|
* Override URL building for NPM-specific handling.
|
|
*/
|
|
protected buildUpstreamUrl(
|
|
upstream: IUpstreamRegistryConfig,
|
|
context: IUpstreamFetchContext,
|
|
): string {
|
|
// NPM registries often don't have trailing slashes
|
|
let baseUrl = upstream.url;
|
|
if (baseUrl.endsWith('/')) {
|
|
baseUrl = baseUrl.slice(0, -1);
|
|
}
|
|
|
|
return `${baseUrl}${context.path}`;
|
|
}
|
|
}
|