feat(upstream): Add upstream proxy/cache subsystem and integrate per-protocol upstreams
This commit is contained in:
200
ts/composer/classes.composerupstream.ts
Normal file
200
ts/composer/classes.composerupstream.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { BaseUpstream } from '../upstream/classes.baseupstream.js';
|
||||
import type {
|
||||
IProtocolUpstreamConfig,
|
||||
IUpstreamFetchContext,
|
||||
IUpstreamRegistryConfig,
|
||||
} from '../upstream/interfaces.upstream.js';
|
||||
|
||||
/**
|
||||
* Composer-specific upstream implementation.
|
||||
*
|
||||
* Handles:
|
||||
* - Package metadata fetching (packages.json, provider-includes)
|
||||
* - Package version metadata (p2/{vendor}/{package}.json)
|
||||
* - Dist file (zip) proxying
|
||||
* - Packagist v2 API support
|
||||
*/
|
||||
export class ComposerUpstream extends BaseUpstream {
|
||||
protected readonly protocolName = 'composer';
|
||||
|
||||
constructor(
|
||||
config: IProtocolUpstreamConfig,
|
||||
logger?: plugins.smartlog.Smartlog,
|
||||
) {
|
||||
super(config, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the root packages.json from upstream.
|
||||
*/
|
||||
public async fetchPackagesJson(): Promise<any | null> {
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: '*',
|
||||
resourceType: 'root',
|
||||
path: '/packages.json',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch package metadata using v2 API (p2/{vendor}/{package}.json).
|
||||
*/
|
||||
public async fetchPackageMetadata(vendor: string, packageName: string): Promise<any | null> {
|
||||
const fullName = `${vendor}/${packageName}`;
|
||||
const path = `/p2/${vendor}/${packageName}.json`;
|
||||
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: fullName,
|
||||
resourceType: 'metadata',
|
||||
path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch package metadata with dev versions (p2/{vendor}/{package}~dev.json).
|
||||
*/
|
||||
public async fetchPackageDevMetadata(vendor: string, packageName: string): Promise<any | null> {
|
||||
const fullName = `${vendor}/${packageName}`;
|
||||
const path = `/p2/${vendor}/${packageName}~dev.json`;
|
||||
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: fullName,
|
||||
resourceType: 'metadata-dev',
|
||||
path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a provider-includes file.
|
||||
*/
|
||||
public async fetchProviderIncludes(path: string): Promise<any | null> {
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: '*',
|
||||
resourceType: 'provider',
|
||||
path: path.startsWith('/') ? path : `/${path}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a dist file (zip) from upstream.
|
||||
*/
|
||||
public async fetchDist(url: string): Promise<Buffer | null> {
|
||||
// Parse the URL to get the path
|
||||
let path: string;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
path = parsed.pathname;
|
||||
} catch {
|
||||
path = url;
|
||||
}
|
||||
|
||||
const context: IUpstreamFetchContext = {
|
||||
protocol: 'composer',
|
||||
resource: '*',
|
||||
resourceType: 'dist',
|
||||
path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'accept': 'application/zip, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override URL building for Composer-specific handling.
|
||||
*/
|
||||
protected buildUpstreamUrl(
|
||||
upstream: IUpstreamRegistryConfig,
|
||||
context: IUpstreamFetchContext,
|
||||
): string {
|
||||
let baseUrl = upstream.url;
|
||||
|
||||
// Remove trailing slash
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
return `${baseUrl}${context.path}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user