UNPKG

projectz

Version:

Stop wasting time syncing and updating your project's README and Package Files!

438 lines (437 loc) 19.5 kB
"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;