projectz
Version:
Stop wasting time syncing and updating your project's README and Package Files!
438 lines (437 loc) • 19.5 kB
JavaScript
"use strict";
/* eslint key-spacing:0 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Projectz = void 0;
// builtin
const node_path_1 = require("node:path");
const trim_empty_keys_1 = __importDefault(require("trim-empty-keys"));
const arrange_package_json_1 = __importDefault(require("arrange-package-json"));
const fs_list_1 = __importDefault(require("@bevry/fs-list"));
const fs_read_1 = __importDefault(require("@bevry/fs-read"));
const fs_write_1 = __importDefault(require("@bevry/fs-write"));
const json_1 = require("@bevry/json");
const typechecker_1 = require("typechecker");
const github_api_1 = require("@bevry/github-api");
const render_1 = require("@bevry/render");
__exportStar(require("./types.js"), exports);
const backer_js_1 = require("./backer.js");
const badge_js_1 = require("./badge.js");
const history_js_1 = require("./history.js");
const install_js_1 = require("./install.js");
const license_js_1 = require("./license.js");
const util_js_1 = require("./util.js");
/** Projectz, use to merge data files and render meta files. */
class Projectz {
/** The log function to use, first argument being the log level */
log = function () { };
/** The directory to process, defaults to the current working directory*/
cwd;
/** If enabled, then remote updates will be not performed (such as fetching latest backers). */
offline = false;
/**
* Resolved absolute paths for the package files.
* Should be arranged in the order of merging preference.
*/
filenamesForPackageFiles = {
component: null,
bower: null,
jquery: null,
package: null,
projectz: null,
};
/** Resolved data for the package files */
dataForPackageFiles = {};
/** Resolved absolute paths for the readme files */
filenamesForReadmeFiles = {
// gets filled in with relative paths
readme: null,
history: null,
contributing: null,
backers: null,
license: null,
};
/** Resolved data for the readme files */
dataForReadmeFiles = {};
/** Configure our instance. */
constructor(opts = {}) {
this.cwd = (0, node_path_1.resolve)(opts.cwd || '.');
this.offline = opts.offline || false;
if (opts.log)
this.log = opts.log;
}
/** Use the configuration to compile the project. */
async compile() {
await this.loadPaths();
const enhancedPackageData = await this.enhanceDataForPackageFiles();
const enhancedReadmeData = await this.enhanceDataForReadmeFiles(enhancedPackageData);
await this.save(enhancedPackageData, enhancedReadmeData);
}
/** Resolve the paths and metdata for the data and meta files. */
async loadPaths() {
// Apply our determined paths for packages
const packageFiles = Object.keys(this.filenamesForPackageFiles);
const readmeFiles = Object.keys(this.filenamesForReadmeFiles);
// Load
const files = await (0, fs_list_1.default)(this.cwd);
for (const file of files) {
const filePath = (0, node_path_1.join)(this.cwd, file);
for (const key of packageFiles) {
const basename = file.toLowerCase().split('.').slice(0, -1).join('.');
if (basename === key) {
this.log('info', `Reading package file: ${filePath}`);
const data = await (0, json_1.readJSON)(filePath);
this.filenamesForPackageFiles[key] = file;
this.dataForPackageFiles[key] = data;
}
}
for (const key of readmeFiles) {
if (file.toLowerCase().startsWith(key)) {
this.log('info', `Reading meta file: ${filePath}`);
const data = await (0, fs_read_1.default)(filePath);
this.filenamesForReadmeFiles[key] = file;
this.dataForReadmeFiles[key] = data.toString();
}
}
}
}
/** Merge and enhance the data for the package files. */
async enhanceDataForPackageFiles() {
// ----------------------------------
// Combine
this.log('debug', 'Enhancing packages data');
// Combine the package data
const mergedPackageData = {
keywords: [],
editions: [],
badges: {
list: [],
config: {},
},
bugs: {},
readmes: {},
packages: {},
repository: {},
github: {},
dependencies: {},
devDependencies: {},
};
for (const key of Object.keys(this.filenamesForPackageFiles)) {
Object.assign(mergedPackageData, this.dataForPackageFiles[key]);
}
// ----------------------------------
// Validation
// Validate keywords field
if ((0, typechecker_1.isString)(mergedPackageData.keywords)) {
throw new Error('projectz: keywords field must be array instead of CSV');
}
// Validate people fields
for (const soloField of [
'maintainer',
'contributor',
'sponsor',
'funder',
'backer',
]) {
const pluralField = `${soloField}s`;
if (mergedPackageData[soloField]) {
throw new Error(`projectz: ${soloField} field is deprecated, use ${pluralField} field`);
}
if ((0, typechecker_1.isString)(mergedPackageData[pluralField])) {
throw new Error(`projectz: ${pluralField} field must be array instead of CSV`);
}
}
// Validate license SPDX string
if ((0, typechecker_1.isPlainObject)(mergedPackageData.license)) {
throw new Error('projectz: license field must now be a valid SPDX string: https://docs.npmjs.com/files/package.json#license');
}
if ((0, typechecker_1.isPlainObject)(mergedPackageData.licenses)) {
throw new Error('projectz: licenses field is deprecated, you must now use the license field as a valid SPDX string: https://docs.npmjs.com/files/package.json#license');
}
// Validate enhanced fields
for (const field of ['badges', 'readmes', 'packages', 'github', 'bugs']) {
if (!(0, typechecker_1.isPlainObject)(mergedPackageData[field])) {
throw new Error(`projectz: ${field} field must be an object`);
}
}
// Validate package values
for (const [key, value] of Object.entries(mergedPackageData.packages)) {
if (!(0, typechecker_1.isPlainObject)(value)) {
throw new Error(`projectz: custom package data for package ${key} must be an object`);
}
}
// Validate badges field
if (!Array.isArray(mergedPackageData.badges.list) ||
(mergedPackageData.badges.config &&
!(0, typechecker_1.isPlainObject)(mergedPackageData.badges.config))) {
throw new Error('projectz: badges field must be in the format of: {list: [], config: {}}\nSee https://github.com/bevry/badges for details.');
}
mergedPackageData.badges.config ??= {};
// ----------------------------------
// Ensure
// Ensure repository is an object
if (typeof mergedPackageData.repository === 'string') {
mergedPackageData.repository = {
type: 'git',
url: mergedPackageData.repository,
};
}
// Fallback name
if (!mergedPackageData.name) {
mergedPackageData.name = (0, node_path_1.dirname)(this.cwd);
}
// Fallback version
if (!mergedPackageData.version) {
mergedPackageData.version = '0.1.0';
}
// Fallback demo field, by scanning homepage
if (!mergedPackageData.demo && mergedPackageData.homepage) {
mergedPackageData.demo = mergedPackageData.homepage;
}
// Fallback title from name
if (!mergedPackageData.title) {
mergedPackageData.title = mergedPackageData.name;
}
// Fallback description
if (!mergedPackageData.description) {
mergedPackageData.description = 'no description was provided';
}
// Fallback browsers field, by checking if `component` or `bower` package files exists, or if the `browser` or `jspm` fields are defined
if (mergedPackageData.browsers == null) {
mergedPackageData.browsers = Boolean(this.filenamesForPackageFiles.bower ||
this.filenamesForPackageFiles.component ||
mergedPackageData.browser ||
mergedPackageData.jspm);
}
// ----------------------------------
// Enhance Repository
// Converge and extract repository information
let github;
if (mergedPackageData.repository) {
const githubSlug = (0, github_api_1.getGitHubSlugFromPackageData)(mergedPackageData);
if (githubSlug) {
// Extract parts
const [githubUsername, githubRepository] = githubSlug.split('/');
const githubRepositoryWebsiteUrl = (0, github_api_1.getRepositoryWebsiteUrlFromGitHubSlugOrUrl)(githubSlug) || '';
const githubRepositoryUrl = (0, github_api_1.getRepositoryUrlFromGitHubSlugOrUrl)(githubSlug) || '';
const githubIssuesUrl = (0, github_api_1.getRepositoryIssuesUrlFromGitHubSlugOrUrl)(githubSlug) || '';
// Github data
github = {
username: githubUsername,
repository: githubRepository,
slug: githubSlug,
url: githubRepositoryWebsiteUrl,
repositoryUrl: githubRepositoryUrl,
};
// Badges
Object.assign(mergedPackageData.badges.config, {
githubUsername,
githubRepository,
githubSlug,
});
// Fallback bugs field by use of slug
if (!mergedPackageData.bugs) {
mergedPackageData.bugs = githubIssuesUrl;
}
// Fallback repository field by use of slug
if (!mergedPackageData.repository?.url) {
mergedPackageData.repository = {
type: 'git',
url: githubRepositoryUrl,
};
}
}
}
// ----------------------------------
// Enhance Backers
const backers = await (0, github_api_1.getBackers)({
githubSlug: github?.slug,
packageData: mergedPackageData,
offline: this.offline,
});
const renderedBackersForPackage = await (0, github_api_1.renderBackers)(backers, {
format: github_api_1.BackersRenderFormat.string,
});
// ----------------------------------
// Enhance Packages
// Create the data for the `package.json` format
const pkg = Object.assign(
// New Object
{},
// Old Data
this.dataForPackageFiles.package || {},
// Enhanced Data
{
// meta
name: mergedPackageData.name,
version: mergedPackageData.version,
license: mergedPackageData.license,
description: mergedPackageData.description,
repository: mergedPackageData.repository,
bugs: mergedPackageData.bugs,
keywords: mergedPackageData.keywords,
// code
engines: mergedPackageData.engines,
dependencies: mergedPackageData.dependencies,
devDependencies: mergedPackageData.devDependencies,
main: mergedPackageData.main,
},
// Enhanced Backers
renderedBackersForPackage,
// Explicit data
mergedPackageData.packages.package || {});
// Trim
// @ts-ignore
if ((0, typechecker_1.isEmptyPlainObject)(pkg.dependencies))
delete pkg.dependencies;
// @ts-ignore
if ((0, typechecker_1.isEmptyPlainObject)(pkg.devDependencies))
delete pkg.devDependencies;
// Badges
if (!mergedPackageData.badges.config.npmPackageName && pkg.name) {
mergedPackageData.badges.config.npmPackageName = pkg.name;
}
// Create the data for the `jquery.json` format, which is essentially the same as the `package.json` format so just extend that
const jquery = Object.assign(
// New Object
{},
// Old Data
this.dataForPackageFiles.jquery || {},
// Enhanced Data
pkg,
// Explicit data
mergedPackageData.packages.jquery || {});
// Create the data for the `component.json` format
const component = Object.assign(
// New Object
{},
// Old Data
this.dataForPackageFiles.component || {},
// Enhanced Data
{
name: mergedPackageData.name,
version: mergedPackageData.version,
license: mergedPackageData.license,
description: mergedPackageData.description,
keywords: mergedPackageData.keywords,
demo: mergedPackageData.demo,
main: mergedPackageData.main,
scripts: [mergedPackageData.main],
},
// Explicit data
mergedPackageData.packages.component || {});
// Create the data for the `bower.json` format
const bower = Object.assign(
// New Object
{},
// Old Data
this.dataForPackageFiles.bower || {},
// Enhanced Data
{
name: mergedPackageData.name,
version: mergedPackageData.version,
license: mergedPackageData.license,
description: mergedPackageData.description,
keywords: mergedPackageData.keywords,
authors: mergedPackageData.authors,
main: mergedPackageData.main,
},
// Explicit data
mergedPackageData.packages.bower || {});
// ----------------------------------
// Enhance Combination
// only stored in memory
const enhancedPackageData = Object.assign({}, mergedPackageData, backers, {
// Add paths so that our helpers have access to them
filenamesForPackageFiles: this.filenamesForPackageFiles,
filenamesForReadmeFiles: this.filenamesForReadmeFiles,
// Other
github,
// Create the data for the `package.json` format
package: pkg,
jquery,
component,
bower,
});
// Return
return enhancedPackageData;
}
/** Merge and enhance the metadata from the meta files. */
async enhanceDataForReadmeFiles(data) {
const enhancedReadmeData = {};
/* eslint prefer-const: 0 */
for (let [key, value] of Object.entries(this.dataForReadmeFiles)) {
if (!value) {
this.log('debug', `Enhancing readme value: ${key} — skipped`);
continue;
}
value = (0, util_js_1.replaceSection)(['TITLE', 'NAME'], value, (0, render_1.mh1)(data.title));
value = (0, util_js_1.replaceSection)(['BADGES', 'BADGE'], value, badge_js_1.getBadgesSection.bind(null, data));
value = (0, util_js_1.replaceSection)(['DESCRIPTION'], value, data.description);
value = (0, util_js_1.replaceSection)(['INSTALL'], value, install_js_1.getInstallInstructions.bind(null, data));
value = (0, util_js_1.replaceSection)(['CONTRIBUTE', 'CONTRIBUTING'], value, data.github
? backer_js_1.getContributeSection.bind(null, data)
: '<!-- github projects only -->', Boolean(value.includes('BACKERS --')));
value = (0, util_js_1.replaceSection)(['BACKERS', 'BACKER', 'PEOPLE'], value, data.github
? backer_js_1.getBackersSection.bind(null, data)
: '<!-- github projects only -->');
value = (0, util_js_1.replaceSection)(['BACKERSFILE', 'BACKERFILE', 'PEOPLEFILE'], value, data.github
? backer_js_1.getBackersFile.bind(null, data)
: '<!-- github projects only -->');
value = (0, util_js_1.replaceSection)(['HISTORY', 'CHANGES', 'CHANGELOG'], value, data.github
? history_js_1.getHistorySection.bind(null, data)
: '<!-- github projects only -->');
value = (0, util_js_1.replaceSection)(['LICENSE', 'LICENSES'], value, data.github
? license_js_1.getLicenseSection.bind(null, data)
: '<!-- github projects only -->');
value = (0, util_js_1.replaceSection)(['LICENSEFILE'], value, license_js_1.getLicenseFile.bind(null, data));
enhancedReadmeData[key] = value.replaceAll('-->\n\n\n<!--', '-->\n\n<!--');
this.log('info', `Enhanced readme value: ${key}`);
}
return enhancedReadmeData;
}
/** Save the data and meta files with our enhancements. */
async save(enhancedPackageData, enhancedReadmeData) {
// Prepare
this.log('info', 'Writing changes...');
await Promise.all([
// save package files
...Object.entries(this.filenamesForPackageFiles).map(async ([key, filename]) => {
if (!filename || key === 'projectz')
return;
const filepath = (0, node_path_1.join)(this.cwd, filename);
this.log('info', `Saving package file: ${filepath}`);
const data = (0, trim_empty_keys_1.default)((0, arrange_package_json_1.default)(enhancedPackageData[key]));
return (0, json_1.writeJSON)(filepath, data);
}),
// save readme files
...Object.entries(this.filenamesForReadmeFiles).map(async ([key, filename]) => {
if (!filename)
return;
const filepath = (0, node_path_1.join)(this.cwd, filename);
this.log('info', `Saving readme file: ${filepath}`);
const content = (0, render_1.trim)(enhancedReadmeData[key]) + '\n';
return (0, fs_write_1.default)(filepath, content);
}),
]);
// log
this.log('info', 'Wrote changes');
}
}
exports.Projectz = Projectz;