UNPKG

publish-please

Version:

Safe and highly functional replacement for `npm publish`.

295 lines (264 loc) 10.7 kB
'use strict'; const exec = require('cp-sugar').exec; const pathJoin = require('path').join; const readFile = require('fs').readFileSync; const sep = require('path').sep; const tempFolder = require('osenv').tmpdir(); const path = require('path'); const globMatching = require('micromatch'); const unlink = require('fs').unlinkSync; /** * Audit the package that will be sent to the registry * @param {(string | undefined)} projectDir - project directory to be analyzed by npm-pack command * @returns {Object} returns an object with sensitive/non-essential files found in the package that will be sent to the registry */ module.exports = function auditPackage(projectDir) { return Promise.resolve().then(() => getDefaultOptionsFor(projectDir)) // prettier-ignore .then(options => { process.chdir(options.directoryToPackage); const command = `npm pack --json > ${options.npmPackLogFilepath}`; return exec(command).then(() => createResponseFromNpmPackLog(options.npmPackLogFilepath)).then(response => addSensitiveDataInfosIn(response)).then(response => updateSensitiveDataInfosOfIgnoredFilesIn(response, options)).then(response => removePackageTarFileFrom(options.directoryToPackage, response)); }); }; function createResponseFromNpmPackLog(logFilePath) { const content = readFile(logFilePath).toString(); const extractedJson = extractJsonDataFrom(content); const response = JSON.parse(extractedJson); // prettier-ignore return Array.isArray(response) ? response[0] : response; } /** * Extract the Json data from input content. * The problem: the 'npm pack --json > output.log' command will run any 'prepublish' script * defined in the package.json file before executing the npm pack command itself. * In the context of publish-please, any already-installed publish-please package * has already a prepublish script: "prepublish": "publish-please guard" * this will give the following output: * > testing-repo@1.3.77 prepublish ... * > publish-please guard * [{real ouput of npm pack}] * * So the input content may be either a valid json file * or valid json data prefixed by the result of the prepublish script execution * @param {string} content */ function extractJsonDataFrom(content) { let firstValidIndex = 0; for (let index = 0; index <= content.length - 1; index++) { const char = content.charAt(index); if (char === '[' || char === '{') { firstValidIndex = index; break; } } return content.substr(firstValidIndex); } /** * exported for testing purposes */ module.exports.extractJsonDataFrom = extractJsonDataFrom; /** * Add sensitive data infos for each file included in the package * @param {Object} response - result of the npm pack command (eventually modified by previous middlewares execution) * @returns {Object} returns a new response object that is a deep copy of input response * with each file being tagged with a new boolean property 'isSensitiveData'. */ function addSensitiveDataInfosIn(response) { const allSensitiveDataPatterns = getSensitiveData(process.cwd()); const augmentedResponse = JSON.parse(JSON.stringify(response, null, 2)); const files = augmentedResponse.files || []; files.forEach(file => { if (file && isSensitiveData(file.path, allSensitiveDataPatterns)) { file.isSensitiveData = true; return; } if (file) { file.isSensitiveData = false; } }); return augmentedResponse; } /** * exported for testing purposes */ module.exports.addSensitiveDataInfosIn = addSensitiveDataInfosIn; /** * Check if file in filepath is sensitive data * @param {string} filepath * @param {DefaultSensitiveData} allSensitiveDataPatterns * @returns {boolean} */ function isSensitiveData(filepath, allSensitiveDataPatterns) { if (filepath && allSensitiveDataPatterns && allSensitiveDataPatterns.ignoredData && globMatching.any(filepath, allSensitiveDataPatterns.ignoredData, { matchBase: true, nocase: true })) { return false; } if (filepath && allSensitiveDataPatterns && allSensitiveDataPatterns.sensitiveData && globMatching.any(filepath, allSensitiveDataPatterns.sensitiveData, { matchBase: true, nocase: true })) { return true; } return false; } /** * Update sensitive data infos for each ignored file included in the package * @param {Object} response - result of the npm pack command (eventually modified by previous middlewares execution) * @returns {Object} returns a new response object that is a deep copy of input response * with each ignored file being tagged with 'isSensitiveData=false'. */ function updateSensitiveDataInfosOfIgnoredFilesIn(response, options) { const ignoredData = getIgnoredSensitiveData(options); const updatedResponse = JSON.parse(JSON.stringify(response, null, 2)); const files = updatedResponse.files || []; files.forEach(file => { if (file && file.isSensitiveData && isIgnoredData(file.path, ignoredData)) { file.isSensitiveData = false; return; } }); return updatedResponse; } function isIgnoredData(filepath, ignoredData) { return filepath && globMatching.any(filepath, ignoredData, { matchBase: true, nocase: true }); } /** * @typedef DefaultOptions * @type {Object} * @property {string} directoryToPackage - Folder to package. Defaults to process.cwd() * @property {string} npmPackLogFilepath - Path of the file that will receive all output of the npm-pack command. */ /** * Get default options of this module * @param {string} projectDir - path to the directory that will be packaged by npm pack * @returns {DefaultOptions} */ function getDefaultOptionsFor(projectDir) { const directoryToPackage = projectDir || process.cwd(); const projectName = directoryToPackage.split(sep).pop() || ''; const packLogFilename = `npm-pack-${projectName.trim()}.log`; const npmPackLogFilepath = path.resolve(tempFolder, packLogFilename); return { directoryToPackage, npmPackLogFilepath }; } /** * exported for testing purposes */ module.exports.getDefaultOptionsFor = getDefaultOptionsFor; /** * @typedef DefaultSensitiveData * @type {Object} * @property {[string]} sensitiveData - all patterns that defines sensitive data * @property {[string]} ignoredData - all patterns that defines data that is not sensitive */ /** * get all sensitive data from the '.sensitivedata' file * If a .sensitivedata file is found in input projectDir, this one is taken * otherwise the default publish-please .sensitivedata file is taken. * @param {string} projectDir - project directory to be analyzed by npm-pack command * @returns {DefaultSensitiveData} */ function getSensitiveData(projectDir) { try { return getCustomSensitiveData(projectDir); } catch (error) { return getDefaultSensitiveData(); } } /** * exported for testing purposes */ module.exports.getSensitiveData = getSensitiveData; /** * @typedef DefaultSensitiveData * @type {Object} * @property {[string]} sensitiveData - all patterns that defines sensitive data * @property {[string]} ignoredData - all patterns that defines data that is not sensitive */ /** * get all sensitive data from the '.sensitivedata' file * @returns {DefaultSensitiveData} */ function getDefaultSensitiveData() { const sensitiveDataDirectory = pathJoin(__dirname, '..', '..'); return getCustomSensitiveData(sensitiveDataDirectory); } /** * exported for testing purposes */ module.exports.getDefaultSensitiveData = getDefaultSensitiveData; /** * Get files, in .publishrc configuration file, that should be ignored within the npm package * @param {DefaultOptions} options * @returns {[string]} returns the list of files that should be ignored */ function getIgnoredSensitiveData(options) { try { const sensitiveDataIgnoreFile = pathJoin(options.directoryToPackage, '.publishrc'); const publishPleaseConfiguration = JSON.parse(readFile(sensitiveDataIgnoreFile).toString()); const result = publishPleaseConfiguration && publishPleaseConfiguration.validations && publishPleaseConfiguration.validations.sensitiveData && Array.isArray(publishPleaseConfiguration.validations.sensitiveData.ignore) ? publishPleaseConfiguration.validations.sensitiveData.ignore : []; return result; } catch (error) { return []; } } /** * exported for testing purposes */ module.exports.getIgnoredSensitiveData = getIgnoredSensitiveData; /** * Middleware that removes the auto-generated package tar file * @param {string} projectDir - folder where the tar file has been generated * @param {*} response - result of the npm pack command (eventually modified by previous middlewares execution) * @returns input response if file removal is ok * In case of error it adds or updates into the input response object the property 'internalErrors' */ function removePackageTarFileFrom(projectDir, response) { try { const file = pathJoin(projectDir, response.filename); unlink(file); return response; } catch (error) { if (error.code === 'ENOENT') { return response; } if (response) { response.internalErrors = response.internalErrors || []; response.internalErrors.push(error); return response; } return response; } } /** * exported for testing purposes */ module.exports.removePackageTarFileFrom = removePackageTarFileFrom; /** * get all sensitive data from the '.sensitivedata' file * @param {string} projectDir - directory that contains a .sensitivedata file * @returns {DefaultSensitiveData} */ function getCustomSensitiveData(projectDir) { const sensitiveDataFile = pathJoin(projectDir, '.sensitivedata'); const content = readFile(sensitiveDataFile).toString(); const allPatterns = content.split(/\n|\r/).map(line => line.replace(/[\t]/g, ' ')).map(line => line.trim()).filter(line => line && line.length > 0).filter(line => !line.startsWith('#')); const sensitiveData = allPatterns.filter(pattern => !pattern.startsWith('!')); const ignoredData = allPatterns.filter(pattern => pattern.startsWith('!')).map(pattern => pattern.replace('!', '')).map(pattern => pattern.trim()); return { sensitiveData, ignoredData }; } /** * exported for testing purposes */ module.exports.getCustomSensitiveData = getCustomSensitiveData;