feat(publish): add registry resolution (useBase/extendBase) and migrate filesystem operations to async SmartFs; improve publish flow and docs

This commit is contained in:
2025-12-16 12:29:39 +00:00
parent 47fdb00d5b
commit 0f1fa22c46
10 changed files with 2792 additions and 2280 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tspublish',
version: '1.10.4',
version: '1.11.0',
description: 'A tool to publish multiple, concise, and small packages from monorepos, specifically for TypeScript projects within a git environment.'
}

View File

@@ -16,6 +16,11 @@ export interface IPublishModuleOptions {
dependencies?: { [key: string]: string };
}
export interface IResolvedRegistry {
url: string;
accessLevel: string;
}
export class PublishModule {
tsPublishRef: TsPublish;
public options: IPublishModuleOptions;
@@ -34,14 +39,14 @@ export class PublishModule {
if (!this.options.packageSubFolder.startsWith('ts')) {
throw new Error('subFolder must start with "ts"');
}
this.options.tsPublishJson = plugins.smartfile.fs.toObjectSync(
plugins.path.join(this.options.packageSubFolderFullPath, 'tspublish.json')
);
const tspublishJsonPath = plugins.path.join(this.options.packageSubFolderFullPath, 'tspublish.json');
const tspublishJsonContent = await plugins.smartfs.file(tspublishJsonPath).encoding('utf8').read();
this.options.tsPublishJson = JSON.parse(tspublishJsonContent as string);
// the package.json of the parent mono repo
const monoRepoPackageJson = JSON.parse(
plugins.smartfile.fs.toStringSync(plugins.path.join(this.options.monoRepoDir, 'package.json'))
);
const packageJsonPath = plugins.path.join(this.options.monoRepoDir, 'package.json');
const packageJsonContent = await plugins.smartfs.file(packageJsonPath).encoding('utf8').read();
const monoRepoPackageJson = JSON.parse(packageJsonContent as string);
this.options.dependencies = {
...this.options.dependencies,
@@ -91,9 +96,12 @@ export class PublishModule {
}
public async createTsconfigJson() {
const originalTsConfig = plugins.smartfile.fs.toObjectSync(
plugins.path.join(paths.cwd, 'tsconfig.json')
);
const tsconfigPath = plugins.path.join(paths.cwd, 'tsconfig.json');
let originalTsConfig: any = null;
if (await plugins.smartfs.file(tsconfigPath).exists()) {
const tsconfigContent = await plugins.smartfs.file(tsconfigPath).encoding('utf8').read();
originalTsConfig = JSON.parse(tsconfigContent as string);
}
if (originalTsConfig?.compilerOptions?.paths) {
for (const path of Object.keys(originalTsConfig.compilerOptions.paths)) {
originalTsConfig.compilerOptions.paths[
@@ -166,41 +174,35 @@ export class PublishModule {
this.options.monoRepoDir,
`dist_publish_${this.options.packageSubFolder}`
);
await plugins.smartfile.fs.ensureEmptyDir(this.options.publishModDirFullPath);
// Ensure empty directory
const publishDir = plugins.smartfs.directory(this.options.publishModDirFullPath);
if (await publishDir.exists()) {
await publishDir.recursive().delete();
}
await publishDir.recursive().create();
// package.json
const packageJson = await plugins.smartfile.SmartFile.fromString(
plugins.path.join(this.options.publishModDirFullPath, 'package.json'),
await this.createPackageJson(),
'utf8'
);
await packageJson.write();
const packageJsonPath = plugins.path.join(this.options.publishModDirFullPath, 'package.json');
await plugins.smartfs.file(packageJsonPath).encoding('utf8').write(await this.createPackageJson());
// tsconfig.json
const tsconfigJson = await plugins.smartfile.SmartFile.fromString(
plugins.path.join(this.options.publishModDirFullPath, 'tsconfig.json'),
await this.createTsconfigJson(),
'utf8'
);
await tsconfigJson.write();
const tsconfigJsonPath = plugins.path.join(this.options.publishModDirFullPath, 'tsconfig.json');
await plugins.smartfs.file(tsconfigJsonPath).encoding('utf8').write(await this.createTsconfigJson());
// ts subfolder, the folder that contains the source code and is being transpiled
await plugins.smartfile.fs.copy(
this.options.packageSubFolderFullPath,
plugins.path.join(this.options.publishModDirFullPath, this.options.packageSubFolder)
);
const destSubFolder = plugins.path.join(this.options.publishModDirFullPath, this.options.packageSubFolder);
await plugins.smartfs.directory(this.options.packageSubFolderFullPath).recursive().copy(destSubFolder);
// readme
await plugins.smartfile.fs.copy(
plugins.path.join(this.options.packageSubFolderFullPath, 'readme.md'),
plugins.path.join(this.options.publishModDirFullPath, 'readme.md')
);
const readmeSrc = plugins.path.join(this.options.packageSubFolderFullPath, 'readme.md');
const readmeDest = plugins.path.join(this.options.publishModDirFullPath, 'readme.md');
await plugins.smartfs.file(readmeSrc).copy(readmeDest);
// license
await plugins.smartfile.fs.copy(
plugins.path.join(this.options.monoRepoDir, 'license'),
plugins.path.join(this.options.publishModDirFullPath, 'license')
);
const licenseSrc = plugins.path.join(this.options.monoRepoDir, 'license');
const licenseDest = plugins.path.join(this.options.publishModDirFullPath, 'license');
await plugins.smartfs.file(licenseSrc).copy(licenseDest);
// cli stuff
this.createBinCliSetup();
@@ -227,22 +229,104 @@ export class PublishModule {
);
const indexPath = `./dist_${this.options.packageSubFolder}/index.js`;
const fileContent = atob(files[0].base64Content).replace('./dist_ts/index.js', indexPath);
await plugins.smartfile.memory.toFs(fileContent, plugins.path.join(this.options.publishModDirFullPath, 'cli.js'));
const cliJsPath = plugins.path.join(this.options.publishModDirFullPath, 'cli.js');
await plugins.smartfs.file(cliJsPath).encoding('utf8').write(fileContent);
}
/**
* Resolves the registries to publish to based on tspublish.json configuration.
* Supports:
* - "useBase": Use only registries from npmextra.json
* - "extendBase": Use base registries + additions, with exclusions via "-" prefix
* - Explicit registries: Direct registry URLs in format "url:accessLevel"
*/
private async resolveRegistries(): Promise<IResolvedRegistry[]> {
const rawRegistries = this.options.tsPublishJson?.registries || [];
// Empty → skip publishing
if (rawRegistries.length === 0) {
return [];
}
const hasUseBase = rawRegistries.includes('useBase');
const hasExtendBase = rawRegistries.includes('extendBase');
let baseRegistries: string[] = [];
let baseAccessLevel = 'public';
// Load base registries from npmextra.json if needed
if (hasUseBase || hasExtendBase) {
const npmextraInstance = new plugins.npmextra.Npmextra(this.options.monoRepoDir);
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
baseRegistries = gitzoneConfig?.release?.registries || [];
baseAccessLevel = gitzoneConfig?.release?.accessLevel || 'public';
if (baseRegistries.length === 0) {
throw new Error(
`useBase/extendBase specified in tspublish.json but no registries configured in npmextra.json at @git.zone/cli.release.registries`
);
}
}
// useBase: Only base registries
if (hasUseBase) {
return baseRegistries.map((url) => ({ url, accessLevel: baseAccessLevel }));
}
// extendBase: Base registries + additions - exclusions
if (hasExtendBase) {
const exclusions = rawRegistries
.filter((r) => r.startsWith('-'))
.map((r) => r.slice(1)); // remove '-' prefix
const additions = rawRegistries.filter((r) => r !== 'extendBase' && !r.startsWith('-'));
// Filter out excluded base registries
const result: IResolvedRegistry[] = baseRegistries
.filter((url) => !exclusions.includes(url))
.map((url) => ({ url, accessLevel: baseAccessLevel }));
// Add explicit registries
for (const addition of additions) {
const parts = addition.split(':');
const url = parts[0];
const access = parts[1] || 'public';
result.push({ url, accessLevel: access });
}
return result;
}
// Explicit registries only (original behavior)
return rawRegistries.map((r) => {
const parts = r.split(':');
const url = parts[0];
const access = parts[1] || 'public';
return { url, accessLevel: access };
});
}
public async publish() {
logPublish(`Publishing ${this.options.name} v${this.options.version}...`);
const registries = await this.resolveRegistries();
// Handle empty registries
if (registries.length === 0) {
logWarn(`No registries configured for ${this.options.name}. Skipping publish.`);
return;
}
logPublish(`Publishing ${this.options.name} v${this.options.version} to ${registries.length} registry(ies)...`);
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
for (const registry of this.options.tsPublishJson.registries) {
const registryArray = registry.split(':');
const registryUrl = registryArray[0];
const registryAccessLevel = registryArray[1];
for (const registry of registries) {
const registryUrl = registry.url.startsWith('https://') ? registry.url : `https://${registry.url}`;
logOngoing(`Publishing to ${registryUrl}...`);
await smartshellInstance.exec(
`cd ${this.options.publishModDirFullPath} && pnpm publish ${
registryAccessLevel === 'public' ? '--access public' : ''
} --no-git-checks --registry https://${registryUrl}`
registry.accessLevel === 'public' ? '--access public' : ''
} --no-git-checks --registry ${registryUrl}`
);
}
logSuccess(`Successfully published ${this.options.name} v${this.options.version}!`);

View File

@@ -43,22 +43,27 @@ export class TsPublish {
}
public async getModuleSubDirs(dirArg: string) {
const subDirs = await plugins.smartfile.fs.listFolders(dirArg);
// List all directories
const dirContents = await plugins.smartfs.directory(dirArg).list();
const publishModules: { [key: string]: interfaces.ITsPublishJson } = {};
for (const subDir of subDirs) {
if (!subDir.startsWith('ts')) {
continue;
}
const fileTree = await plugins.smartfile.fs.listFileTree(subDir, '**/*');
const hasPublishJson = fileTree.includes('tspublish.json');
if (!hasPublishJson) {
for (const entry of dirContents) {
const subDirName = entry.name;
if (!subDirName.startsWith('ts')) {
continue;
}
logPackage('Found module', subDir);
publishModules[subDir] = JSON.parse(
plugins.smartfile.fs.toStringSync(plugins.path.join(subDir, 'tspublish.json'))
);
// Check if this is a directory and has tspublish.json
const subDirPath = plugins.path.join(dirArg, subDirName);
const tspublishJsonPath = plugins.path.join(subDirPath, 'tspublish.json');
if (!(await plugins.smartfs.file(tspublishJsonPath).exists())) {
continue;
}
logPackage('Found module', subDirName);
const tspublishContent = await plugins.smartfs.file(tspublishJsonPath).encoding('utf8').read();
publishModules[subDirName] = JSON.parse(tspublishContent as string);
}
logSuccess(`Found ${Object.keys(publishModules).length} publish modules`);
logInfo('Ordering publish modules...');

View File

@@ -4,7 +4,9 @@ export { path };
// @push.rocks scope
import * as consolecolor from '@push.rocks/consolecolor';
import * as npmextra from '@push.rocks/npmextra';
import * as smartfile from '@push.rocks/smartfile';
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
import * as smartcli from '@push.rocks/smartcli';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartlog from '@push.rocks/smartlog';
@@ -13,4 +15,7 @@ import * as smartpath from '@push.rocks/smartpath';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartshell from '@push.rocks/smartshell';
export { consolecolor, smartfile, smartcli, smartdelay, smartlog, smartnpm, smartpath, smartrequest, smartshell };
// Create a pre-configured SmartFs instance for Node.js filesystem operations
const smartfs = new SmartFs(new SmartFsProviderNode());
export { consolecolor, npmextra, smartfile, smartfs, smartcli, smartdelay, smartlog, smartnpm, smartpath, smartrequest, smartshell };