deps-pin(core): 📌 Pin npm registry URL to private endpoint in .npmrc and enforce builder/publisher logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7646e49bad
commit
fdb6deb452
3 changed files with 113 additions and 26 deletions
3
.npmrc
Normal file
3
.npmrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
registry=http://localhost:4874/
|
||||
@lilith:registry=http://localhost:4874/
|
||||
//localhost:4874/:_authToken=local-dev-token
|
||||
|
|
@ -2,14 +2,68 @@
|
|||
* Builder - orchestrates TypeScript compilation
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { dirname, join } from 'node:path';
|
||||
import type { BuildResult } from '../types/config.js';
|
||||
import { BuildError } from '../utils/errors.js';
|
||||
import type { Logger } from '../utils/logger.js';
|
||||
|
||||
interface BuildCommand {
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export class Builder {
|
||||
constructor(private logger: Logger) {}
|
||||
|
||||
/**
|
||||
* Detect the package manager by walking up from packagePath
|
||||
* looking for a `packageManager` field in package.json files.
|
||||
* This mirrors how corepack resolves the package manager.
|
||||
*/
|
||||
private detectBuildCommand(packagePath: string): BuildCommand {
|
||||
let dir = packagePath;
|
||||
|
||||
while (true) {
|
||||
const pkgJsonPath = join(dir, 'package.json');
|
||||
|
||||
if (existsSync(pkgJsonPath)) {
|
||||
try {
|
||||
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
||||
|
||||
if (pkgJson.packageManager && typeof pkgJson.packageManager === 'string') {
|
||||
const manager = pkgJson.packageManager.split('@')[0];
|
||||
this.logger.debug(`Detected package manager: ${pkgJson.packageManager} (from ${pkgJsonPath})`);
|
||||
|
||||
switch (manager) {
|
||||
case 'bun':
|
||||
return { command: 'bun', args: ['run', 'build'] };
|
||||
case 'yarn':
|
||||
return { command: 'yarn', args: ['build'] };
|
||||
case 'npm':
|
||||
return { command: 'npm', args: ['run', 'build'] };
|
||||
case 'pnpm':
|
||||
return { command: 'pnpm', args: ['build'] };
|
||||
default:
|
||||
this.logger.debug(`Unknown package manager "${manager}", falling back to pnpm`);
|
||||
return { command: 'pnpm', args: ['build'] };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Malformed package.json, continue walking up
|
||||
}
|
||||
}
|
||||
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
|
||||
this.logger.debug('No packageManager field found, defaulting to pnpm');
|
||||
return { command: 'pnpm', args: ['build'] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build TypeScript package using package.json build script
|
||||
*/
|
||||
|
|
@ -19,10 +73,11 @@ export class Builder {
|
|||
return { success: true, output: 'Build skipped' };
|
||||
}
|
||||
|
||||
this.logger.info('Building package...');
|
||||
const { command, args } = this.detectBuildCommand(packagePath);
|
||||
this.logger.info(`Building package with ${command}...`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const tsc = spawn('pnpm', ['build'], {
|
||||
const tsc = spawn(command, args, {
|
||||
cwd: packagePath,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,40 +37,56 @@ export class Publisher {
|
|||
};
|
||||
}
|
||||
|
||||
// Create temp directory for backup and npmrc
|
||||
// Create temp directory for backup
|
||||
const tempDir = join(tmpdir(), `dev-publish-${Date.now()}`);
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
|
||||
const originalPackageJson = join(packagePath, 'package.json');
|
||||
const backupPackageJson = join(tempDir, 'package.json.backup');
|
||||
const tempNpmrc = join(tempDir, '.npmrc');
|
||||
// Write .npmrc directly into the package dir so it takes precedence
|
||||
// over any parent .npmrc files with scoped registry overrides
|
||||
const packageNpmrc = join(packagePath, '.npmrc');
|
||||
const backupNpmrc = join(tempDir, '.npmrc.backup');
|
||||
|
||||
try {
|
||||
// Backup original package.json
|
||||
await copyFile(originalPackageJson, backupPackageJson);
|
||||
|
||||
// Debug: Save transformed package.json for inspection
|
||||
const debugFile = '/tmp/dev-publish-transformed.json';
|
||||
await writeFile(debugFile, JSON.stringify(transformedPkg, null, 2), 'utf-8');
|
||||
this.logger.info(`Transformed package fields: ${Object.keys(transformedPkg).length}`);
|
||||
this.logger.info(`Has type field: ${!!transformedPkg.type}`);
|
||||
// Override publishConfig.registry so npm publish targets our registry, not the one in package.json
|
||||
const publishPkg = { ...transformedPkg };
|
||||
if (publishPkg.publishConfig) {
|
||||
publishPkg.publishConfig = { ...publishPkg.publishConfig, registry: registryUrl };
|
||||
}
|
||||
this.logger.info(`Transformed package fields: ${Object.keys(publishPkg).length}`);
|
||||
this.logger.info(`Has type field: ${!!publishPkg.type}`);
|
||||
|
||||
// Write transformed package.json directly to package directory
|
||||
const pkgJsonContent = JSON.stringify(transformedPkg, null, 2);
|
||||
const pkgJsonContent = JSON.stringify(publishPkg, null, 2);
|
||||
await writeFile(originalPackageJson, pkgJsonContent, 'utf-8');
|
||||
|
||||
// Write .npmrc with registry and auth
|
||||
// For local registries, use a placeholder token (Verdaccio allows anonymous but npm requires a token)
|
||||
const npmrcLines = [`registry=${registryUrl}`];
|
||||
// Backup existing .npmrc if present
|
||||
let hadExistingNpmrc = false;
|
||||
try {
|
||||
await copyFile(packageNpmrc, backupNpmrc);
|
||||
hadExistingNpmrc = true;
|
||||
} catch {
|
||||
// No existing .npmrc to backup
|
||||
}
|
||||
|
||||
// Write .npmrc with registry, scoped registry, and auth
|
||||
const tokenToUse = authToken || 'local-dev-token';
|
||||
const scope = name.startsWith('@') ? name.split('/')[0] : null;
|
||||
const npmrcLines = [`registry=${registryUrl}`];
|
||||
if (scope) {
|
||||
npmrcLines.push(`${scope}:registry=${registryUrl}`);
|
||||
}
|
||||
npmrcLines.push(`//${new URL(registryUrl).host}/:_authToken=${tokenToUse}`);
|
||||
const npmrcContent = npmrcLines.join('\n');
|
||||
await writeFile(tempNpmrc, npmrcContent, 'utf-8');
|
||||
await writeFile(packageNpmrc, npmrcLines.join('\n'), 'utf-8');
|
||||
|
||||
this.logger.info(`Publishing ${name}@${version} to ${registryUrl}...`);
|
||||
|
||||
// Run npm publish
|
||||
const result = await this.runNpmPublish(packagePath, tempNpmrc, registryUrl);
|
||||
const result = await this.runNpmPublish(packagePath, packageNpmrc, registryUrl, scope);
|
||||
|
||||
if (result.success) {
|
||||
this.logger.success(`Published ${name}@${version}`);
|
||||
|
|
@ -83,11 +99,16 @@ export class Publisher {
|
|||
registryUrl,
|
||||
};
|
||||
} finally {
|
||||
// Restore original package.json
|
||||
// Restore original package.json and .npmrc
|
||||
try {
|
||||
await copyFile(backupPackageJson, originalPackageJson);
|
||||
await unlink(backupPackageJson);
|
||||
await unlink(tempNpmrc);
|
||||
if (hadExistingNpmrc) {
|
||||
await copyFile(backupNpmrc, packageNpmrc);
|
||||
await unlink(backupNpmrc);
|
||||
} else {
|
||||
await unlink(packageNpmrc);
|
||||
}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
|
@ -99,27 +120,35 @@ export class Publisher {
|
|||
*/
|
||||
private async runNpmPublish(
|
||||
packagePath: string,
|
||||
tempNpmrc: string,
|
||||
registryUrl: string
|
||||
npmrcPath: string,
|
||||
registryUrl: string,
|
||||
scope: string | null
|
||||
): Promise<Omit<PublishResult, 'packageName' | 'version' | 'registryUrl'>> {
|
||||
return new Promise((resolve) => {
|
||||
// Build env with registry overrides that take precedence over all .npmrc files
|
||||
const env: Record<string, string> = {
|
||||
...process.env as Record<string, string>,
|
||||
npm_config_userconfig: npmrcPath,
|
||||
npm_config_registry: registryUrl,
|
||||
};
|
||||
// Override scoped registry to prevent parent .npmrc from redirecting to forgejo
|
||||
if (scope) {
|
||||
env[`npm_config_${scope}:registry`] = registryUrl;
|
||||
}
|
||||
|
||||
const npm = spawn(
|
||||
'npm',
|
||||
[
|
||||
'publish',
|
||||
'--no-git-checks',
|
||||
'--userconfig',
|
||||
tempNpmrc,
|
||||
// Explicit registry flag overrides publishConfig in package.json
|
||||
npmrcPath,
|
||||
`--registry=${registryUrl}`,
|
||||
],
|
||||
{
|
||||
cwd: packagePath,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_userconfig: tempNpmrc,
|
||||
},
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue