import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { logger, logInfo, logSuccess, logWarn, logError, logBuild, logPublish, logOngoing, logStart, logDone } from './logging.js'; import { type ITsPublishJson } from './interfaces/index.js'; import type { TsPublish } from './classes.tspublish.js'; export interface IPublishModuleOptions { monoRepoDir: string; packageSubFolder: string; packageSubFolderFullPath?: string; tsPublishJson?: ITsPublishJson; publishModDirFullPath?: string; name?: string; version?: string; dependencies?: { [key: string]: string }; } export interface IResolvedRegistry { url: string; accessLevel: string; } export class PublishModule { tsPublishRef: TsPublish; public options: IPublishModuleOptions; constructor(tsPublishRef: TsPublish, options: IPublishModuleOptions) { this.tsPublishRef = tsPublishRef; this.options = options; } public async init() { this.options.packageSubFolderFullPath = plugins.path.join( this.options.monoRepoDir, this.options.packageSubFolder ); // check requirements if (!this.options.packageSubFolder.startsWith('ts')) { throw new Error('subFolder must start with "ts"'); } 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 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, ...(() => { const resultDependencies = {}; for (const dependency of this.options.tsPublishJson.dependencies) { if (monoRepoPackageJson.dependencies[dependency]) { resultDependencies[dependency] = monoRepoPackageJson.dependencies[dependency]; } else { resultDependencies[dependency] = monoRepoPackageJson.version; } } return resultDependencies; })(), }; this.options.name = this.options.name || this.options.tsPublishJson.name; this.options.version = monoRepoPackageJson.version; // now that we have a name and version, lets check if there is already a package under the same name and version. const smartnpmInstance = new plugins.smartnpm.NpmRegistry({}); // TODO: pass in options let packageInfo: plugins.smartnpm.NpmPackage; try { packageInfo = await smartnpmInstance.getPackageInfo(this.options.name); } catch (error) { logWarn(`Package ${this.options.name} does not yet seem to exist. Proceeding in 10 seconds...`); await plugins.smartdelay.delayFor(10000); } if (packageInfo) { const availableVersions = packageInfo.allVersions.map((versionArg) => versionArg.version); logInfo(`Available versions for ${this.options.name}: ${availableVersions.join(', ')}`); if (availableVersions.includes(this.options.version)) { logError( `Package ${this.options.name} already exists with version ${this.options.version}` ); process.exit(1); } } } public async getLatestVersionOfPackage(name: string) { const smartnpmInstance = new plugins.smartnpm.NpmRegistry({}); // TODO: pass in options const packageInfo = await smartnpmInstance.getPackageInfo(name); if (!packageInfo) { throw new Error(`package ${name} not found`); } return packageInfo.allVersions[0].version; } public async createTsconfigJson() { 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[ path ][0] = `.${originalTsConfig.compilerOptions.paths[path][0]}`; } } const tsconfigJson = { compilerOptions: { experimentalDecorators: true, useDefineForClassFields: false, target: 'ES2022', module: 'NodeNext', moduleResolution: 'NodeNext', esModuleInterop: true, verbatimModuleSyntax: true, paths: originalTsConfig?.compilerOptions?.paths, }, exclude: ['dist_*/**/*.d.ts'], }; return JSON.stringify(tsconfigJson, null, 2); } public async createPackageJson() { const packageJson = { name: this.options.name, version: this.options.version, type: 'module', description: '', exports: { '.': { import: `./dist_${this.options.packageSubFolder}/index.js`, }, }, scripts: { build: 'tsbuild tsfolders --allowimplicitany', }, dependencies: this.options.dependencies, devDependencies: { '@git.zone/tsbuild': await this.getLatestVersionOfPackage('@git.zone/tsbuild'), }, files: [ 'ts/**/*', 'ts_*/**/*', 'dist/**/*', 'dist_*/**/*', 'dist_ts/**/*', 'dist_ts_web/**/*', 'assets/**/*', 'cli.js', 'npmextra.json', 'readme.md', ], ...this.options.tsPublishJson.bin ? { bin: (() => { const binObject: {[key: string]: string} = {}; for (const bin of this.options.tsPublishJson.bin) { binObject[bin] = `./cli.js`; } return binObject; })() } : {}, }; return JSON.stringify(packageJson, null, 2); } public async createPublishModuleDir() { logOngoing(`Creating publish directory for ${this.options.name}`); this.options.publishModDirFullPath = plugins.path.join( this.options.monoRepoDir, `dist_publish_${this.options.packageSubFolder}` ); // 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 packageJsonPath = plugins.path.join(this.options.publishModDirFullPath, 'package.json'); await plugins.smartfs.file(packageJsonPath).encoding('utf8').write(await this.createPackageJson()); // tsconfig.json 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 const destSubFolder = plugins.path.join(this.options.publishModDirFullPath, this.options.packageSubFolder); await plugins.smartfs.directory(this.options.packageSubFolderFullPath).recursive().copy(destSubFolder); // readme 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 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(); } public async build() { logBuild(`Building ${this.options.name}...`); const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', }); await smartshellInstance.exec(`cd ${this.options.publishModDirFullPath} && pnpm run build`); logSuccess(`Build completed for ${this.options.name}`); } public async createBinCliSetup() { const binSetupApplies: boolean = this.options.tsPublishJson.bin && Array.isArray(this.options.tsPublishJson.bin) && this.options.tsPublishJson.bin.length > 0; const files = await this.tsPublishRef.giteaAssetsInstance.getFiles( 'git.zone', 'cli', 'assets/templates/cli/cli.js' ); const indexPath = `./dist_${this.options.packageSubFolder}/index.js`; const fileContent = atob(files[0].base64Content).replace('./dist_ts/index.js', indexPath); 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 { 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('@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() { 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 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 ${ registry.accessLevel === 'public' ? '--access public' : '' } --no-git-checks --registry ${registryUrl}` ); } logSuccess(`Successfully published ${this.options.name} v${this.options.version}!`); } }