335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
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<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() {
|
|
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}!`);
|
|
}
|
|
}
|