132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
|
|
import * as fs from 'node:fs';
|
||
|
|
import * as path from 'node:path';
|
||
|
|
import { packageNameToPath } from './package-store.js';
|
||
|
|
import { maxSemver } from './versioning.js';
|
||
|
|
|
||
|
|
type JsonRecord = Record<string, unknown>;
|
||
|
|
|
||
|
|
export interface IRegistryVersionEntry {
|
||
|
|
readonly tarballPath: string;
|
||
|
|
readonly publishedAt: string;
|
||
|
|
readonly publishedBy: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IRegistryPackageEntry {
|
||
|
|
readonly versions: Record<string, IRegistryVersionEntry>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IRegistryIndex {
|
||
|
|
readonly packages: Record<string, IRegistryPackageEntry>;
|
||
|
|
}
|
||
|
|
|
||
|
|
function asRecord(value: unknown): JsonRecord | null {
|
||
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return value as JsonRecord;
|
||
|
|
}
|
||
|
|
|
||
|
|
function readRegistryIndex(indexPath: string): IRegistryIndex {
|
||
|
|
if (!fs.existsSync(indexPath)) {
|
||
|
|
return { packages: {} };
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsed = JSON.parse(fs.readFileSync(indexPath, 'utf8')) as unknown;
|
||
|
|
const root = asRecord(parsed);
|
||
|
|
const packagesRecord = asRecord(root?.packages) ?? {};
|
||
|
|
const packages: Record<string, IRegistryPackageEntry> = {};
|
||
|
|
|
||
|
|
for (const [packageName, packageValue] of Object.entries(packagesRecord)) {
|
||
|
|
const packageEntry = asRecord(packageValue);
|
||
|
|
const versionsRecord = asRecord(packageEntry?.versions) ?? {};
|
||
|
|
const versions: Record<string, IRegistryVersionEntry> = {};
|
||
|
|
|
||
|
|
for (const [version, versionValue] of Object.entries(versionsRecord)) {
|
||
|
|
const versionEntry = asRecord(versionValue);
|
||
|
|
if (!versionEntry) continue;
|
||
|
|
if (typeof versionEntry.tarballPath !== 'string') continue;
|
||
|
|
if (typeof versionEntry.publishedAt !== 'string') continue;
|
||
|
|
if (typeof versionEntry.publishedBy !== 'string') continue;
|
||
|
|
versions[version] = {
|
||
|
|
tarballPath: versionEntry.tarballPath,
|
||
|
|
publishedAt: versionEntry.publishedAt,
|
||
|
|
publishedBy: versionEntry.publishedBy,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
packages[packageName] = { versions };
|
||
|
|
}
|
||
|
|
|
||
|
|
return { packages };
|
||
|
|
}
|
||
|
|
|
||
|
|
function writeRegistryIndex(indexPath: string, index: IRegistryIndex): void {
|
||
|
|
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
||
|
|
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveIndexPath(registryRoot: string): string {
|
||
|
|
return path.join(registryRoot, 'index.json');
|
||
|
|
}
|
||
|
|
|
||
|
|
export function publishRegistryVersion(
|
||
|
|
registryRoot: string,
|
||
|
|
packageName: string,
|
||
|
|
version: string,
|
||
|
|
sourceTarballPath: string,
|
||
|
|
publishedBy: string,
|
||
|
|
): { readonly registryTarballPath: string } {
|
||
|
|
const indexPath = resolveIndexPath(registryRoot);
|
||
|
|
const index = readRegistryIndex(indexPath);
|
||
|
|
const packageEntry = index.packages[packageName] ?? { versions: {} };
|
||
|
|
if (packageEntry.versions[version]) {
|
||
|
|
throw new Error(`MX_VERSION_EXISTS: ${packageName}@${version} already published`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const packageVersionDir = path.join(registryRoot, 'packages', packageNameToPath(packageName), version);
|
||
|
|
fs.mkdirSync(packageVersionDir, { recursive: true });
|
||
|
|
const registryTarballPath = path.join(packageVersionDir, 'package.tar.gz');
|
||
|
|
fs.copyFileSync(sourceTarballPath, registryTarballPath);
|
||
|
|
|
||
|
|
packageEntry.versions[version] = {
|
||
|
|
tarballPath: registryTarballPath,
|
||
|
|
publishedAt: new Date().toISOString(),
|
||
|
|
publishedBy,
|
||
|
|
};
|
||
|
|
index.packages[packageName] = packageEntry;
|
||
|
|
writeRegistryIndex(indexPath, index);
|
||
|
|
|
||
|
|
return { registryTarballPath };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveRegistryTarball(
|
||
|
|
registryRoot: string,
|
||
|
|
packageName: string,
|
||
|
|
requestedVersion?: string,
|
||
|
|
): { readonly version: string; readonly tarballPath: string } {
|
||
|
|
const index = readRegistryIndex(resolveIndexPath(registryRoot));
|
||
|
|
const packageEntry = index.packages[packageName];
|
||
|
|
if (!packageEntry) {
|
||
|
|
throw new Error(`MX_PACKAGE_NOT_FOUND: ${packageName}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const version = requestedVersion ?? maxSemver(Object.keys(packageEntry.versions));
|
||
|
|
if (!version || !packageEntry.versions[version]) {
|
||
|
|
throw new Error(`MX_PACKAGE_NOT_FOUND: ${packageName}@${requestedVersion ?? 'latest'}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
version,
|
||
|
|
tarballPath: packageEntry.versions[version]!.tarballPath,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getLatestRegistryVersion(registryRoot: string, packageName: string): string | null {
|
||
|
|
const index = readRegistryIndex(resolveIndexPath(registryRoot));
|
||
|
|
const packageEntry = index.packages[packageName];
|
||
|
|
if (!packageEntry) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
return maxSemver(Object.keys(packageEntry.versions));
|
||
|
|
}
|