UNPKG

@pika/pack

Version:
265 lines (264 loc) 11 kB
import { MANIFEST_FIELDS } from '../../constants.js'; import { isValidLicense } from './util.js'; import { normalizePerson, extractDescription } from './util.js'; import inferLicense from './infer-license.js'; import * as fs from '../fs.js'; import semver from 'semver'; import * as path from 'path'; import * as nodeUrl from 'url'; const LICENSE_RENAMES = { 'MIT/X11': 'MIT', X11: 'MIT', }; export default (async function (info, moduleLoc, reporter, warn) { const files = await fs.readdir(moduleLoc); // clean info.version if (typeof info.version === 'string') { info.version = semver.clean(info.version) || info.version; } // if name or version aren't set then set them to empty strings info.name = info.name || ''; info.version = info.version || ''; // if the man field is a string then coerce it to an array if (typeof info.man === 'string') { info.man = [info.man]; } // if the keywords field is a string then split it on any whitespace if (typeof info.keywords === 'string') { info.keywords = info.keywords.split(/\s+/g); } // if there's no contributors field but an authors field then expand it if (!info.contributors && files.indexOf('AUTHORS') >= 0) { const authorsFilepath = path.join(moduleLoc, 'AUTHORS'); const authorsFilestats = await fs.stat(authorsFilepath); if (authorsFilestats.isFile()) { let authors = await fs.readFile(authorsFilepath); info.contributors = authors .split(/\r?\n/g) // split on lines .map((line) => line.replace(/^\s*#.*$/, '').trim()) // remove comments .filter((line) => !!line); // remove empty lines; } } // expand people fields to objects if (typeof info.author === 'string' || typeof info.author === 'object') { info.author = normalizePerson(info.author); } if (Array.isArray(info.contributors)) { info.contributors = info.contributors.map(normalizePerson); } if (Array.isArray(info.maintainers)) { info.maintainers = info.maintainers.map(normalizePerson); } // if there's no readme field then load the README file from the cwd if (!info.readme) { const readmeCandidates = files .filter((filename) => { const lower = filename.toLowerCase(); return lower === 'readme' || lower.indexOf('readme.') === 0; }) .sort((filename1, filename2) => { // favor files with extensions return filename2.indexOf('.') - filename1.indexOf('.'); }); for (const readmeFilename of readmeCandidates) { const readmeFilepath = path.join(moduleLoc, readmeFilename); const readmeFileStats = await fs.stat(readmeFilepath); if (readmeFileStats.isFile()) { info.readmeFilename = readmeFilename; info.readme = await fs.readFile(readmeFilepath); break; } } } // if there's no description then take the first paragraph from the readme if (!info.description && info.readme) { const desc = extractDescription(info.readme); if (desc) { info.description = desc; } } // support array of engine keys if (Array.isArray(info.engines)) { const engines = {}; for (const str of info.engines) { if (typeof str === 'string') { const [name, ...patternParts] = str.trim().split(/ +/g); engines[name] = patternParts.join(' '); } } info.engines = engines; } // allow bugs to be specified as a string, expand it to an object with a single url prop if (typeof info.bugs === 'string') { info.bugs = { url: info.bugs }; } // normalize homepage url to http if (typeof info.homepage === 'string') { const parts = nodeUrl.parse(info.homepage); parts.protocol = parts.protocol || 'http:'; if (parts.pathname && !parts.hostname) { parts.hostname = parts.pathname; parts.pathname = ''; } info.homepage = nodeUrl.format(parts); } // if the `bin` field is as string then expand it to an object with a single property // based on the original `bin` field and `name field` // { name: "foo", bin: "cli.js" } -> { name: "foo", bin: { foo: "cli.js" } } if (typeof info.name === 'string' && typeof info.bin === 'string' && info.bin.length > 0) { // Remove scoped package name for consistency with NPM's bin field fixing behaviour const name = info.name.replace(/^@[^\/]+\//, ''); info.bin = { [name]: info.bin }; } // bundleDependencies is an alias for bundledDependencies if (info.bundledDependencies) { info.bundleDependencies = info.bundledDependencies; delete info.bundledDependencies; } let scripts; // dummy script object to shove file inferred scripts onto if (info.scripts && typeof info.scripts === 'object') { scripts = info.scripts; } else { scripts = {}; } // if there's a server.js file and no start script then set it to `node server.js` if (!scripts.start && files.indexOf('server.js') >= 0) { scripts.start = 'node server'; } // if there's a binding.gyp file and no install script then set it to `node-gyp rebuild` if (!scripts.install && files.indexOf('binding.gyp') >= 0) { scripts.install = 'node-gyp rebuild'; } // set scripts if we've polluted the empty object if (Object.keys(scripts).length) { info.scripts = scripts; } const dirs = info.directories; if (dirs && typeof dirs === 'object') { const binDir = dirs.bin; if (!info.bin && binDir && typeof binDir === 'string') { const bin = (info.bin = {}); const fullBinDir = path.join(moduleLoc, binDir); if (await fs.exists(fullBinDir)) { for (const scriptName of await fs.readdir(fullBinDir)) { if (scriptName[0] === '.') { continue; } bin[scriptName] = path.join('.', binDir, scriptName); } } else { warn(reporter.lang('manifestDirectoryNotFound', binDir, info.name)); } } const manDir = dirs.man; if (!info.man && typeof manDir === 'string') { const man = (info.man = []); const fullManDir = path.join(moduleLoc, manDir); if (await fs.exists(fullManDir)) { for (const filename of await fs.readdir(fullManDir)) { if (/^(.*?)\.[0-9]$/.test(filename)) { man.push(path.join('.', manDir, filename)); } } } else { warn(reporter.lang('manifestDirectoryNotFound', manDir, info.name)); } } } delete info.directories; // normalize licenses field const licenses = info.licenses; if (Array.isArray(licenses) && !info.license) { let licenseTypes = []; for (let license of licenses) { if (license && typeof license === 'object') { license = license.type; } if (typeof license === 'string') { licenseTypes.push(license); } } licenseTypes = licenseTypes.filter(isValidLicense); if (licenseTypes.length === 1) { info.license = licenseTypes[0]; } else if (licenseTypes.length) { info.license = `(${licenseTypes.join(' OR ')})`; } } const license = info.license; // normalize license if (license && typeof license === 'object') { info.license = license.type; } // get license file const licenseFile = files.find((filename) => { const lower = filename.toLowerCase(); return (lower === 'license' || lower.startsWith('license.') || lower === 'unlicense' || lower.startsWith('unlicense.')); }); if (licenseFile) { const licenseFilepath = path.join(moduleLoc, licenseFile); const licenseFileStats = await fs.stat(licenseFilepath); if (licenseFileStats.isFile()) { const licenseContent = await fs.readFile(licenseFilepath); const inferredLicense = inferLicense(licenseContent); info.licenseText = licenseContent; const license = info.license; if (typeof license === 'string') { if (inferredLicense && isValidLicense(inferredLicense) && !isValidLicense(license)) { // some packages don't specify their license version but we can infer it based on their license file const basicLicense = license.toLowerCase().replace(/(-like|\*)$/g, ''); const expandedLicense = inferredLicense.toLowerCase(); if (expandedLicense.startsWith(basicLicense)) { // TODO consider doing something to notify the user info.license = inferredLicense; } } } else if (inferredLicense) { // if there's no license then infer it based on the license file info.license = inferredLicense; } else { // valid expression to refer to a license in a file info.license = `SEE LICENSE IN ${licenseFile}`; } } } if (typeof info.license === 'string') { // sometimes licenses are known by different names, reduce them info.license = LICENSE_RENAMES[info.license] || info.license; } else if (typeof info.readme === 'string') { // the license might be at the bottom of the README const inferredLicense = inferLicense(info.readme); if (inferredLicense) { info.license = inferredLicense; } } // get notice file const noticeFile = files.find((filename) => { const lower = filename.toLowerCase(); return lower === 'notice' || lower.startsWith('notice.'); }); if (noticeFile) { const noticeFilepath = path.join(moduleLoc, noticeFile); const noticeFileStats = await fs.stat(noticeFilepath); if (noticeFileStats.isFile()) { info.noticeText = await fs.readFile(noticeFilepath); } } for (const dependencyType of MANIFEST_FIELDS) { const dependencyList = info[dependencyType]; if (dependencyList && typeof dependencyList === 'object') { delete dependencyList['//']; for (const name in dependencyList) { dependencyList[name] = dependencyList[name] || ''; } } } });