UNPKG

clean-publish-fix

Version:
289 lines (243 loc) 7.29 kB
import { promises as fs } from 'fs' import { join, basename } from 'path' import spawn from 'cross-spawn' import glob from 'fast-glob' import micromatch from 'micromatch' import { writeJSON, readJSON, copy, remove, isObject, filterObjectByKey } from './utils.js' import IGNORE_FILES from './exception/ignore-files.js' import IGNORE_FIELDS from './exception/ignore-fields.js' import NPM_SCRIPTS from './exception/npm-scripts.js' const PUBLISH_CONFIG_FIELDS = [ 'bin', 'main', 'exports', 'types', 'typings', 'module', 'browser', 'esnext', 'es2015', 'unpkg', 'umd:main', 'cpu', 'os' ] export function readPackageJSON(directoryName) { return readJSON(join(directoryName, 'package.json')) } export function writePackageJSON(directoryName, packageJSON) { return writeJSON(join(directoryName, 'package.json'), packageJSON) } function applyPublishConfig(packageJson) { if (!packageJson.publishConfig) { return } const publishConfig = { ...packageJson.publishConfig } PUBLISH_CONFIG_FIELDS.forEach(field => { if (publishConfig[field]) { packageJson[field] = publishConfig[field] delete publishConfig[field] } }) if (!Object.keys(publishConfig).length) { delete packageJson.publishConfig } else { packageJson.publishConfig = publishConfig } } export function clearPackageJSON(packageJson, inputIgnoreFields) { const ignoreFields = inputIgnoreFields ? IGNORE_FIELDS.concat(inputIgnoreFields) : IGNORE_FIELDS const cleanPackageJSON = filterObjectByKey( packageJson, key => !ignoreFields.includes(key) && key !== 'scripts' ) if (packageJson.scripts && !ignoreFields.includes('scripts')) { cleanPackageJSON.scripts = filterObjectByKey(packageJson.scripts, script => NPM_SCRIPTS.includes(script) ) if ( cleanPackageJSON.scripts.publish && (cleanPackageJSON.scripts.publish === 'clean-publish' || cleanPackageJSON.scripts.publish.startsWith('clean-publish ')) ) { // "custom" publish script is actually calling clean-publish delete cleanPackageJSON.scripts.publish } } applyPublishConfig(cleanPackageJSON) for (const i in cleanPackageJSON) { if ( isObject(cleanPackageJSON[i]) && Object.keys(cleanPackageJSON[i]).length === 0 ) { delete cleanPackageJSON[i] } } return cleanPackageJSON } export function createIgnoreMatcher(ignorePattern) { if (ignorePattern instanceof RegExp) { return filename => !ignorePattern.test(filename) } if (glob.isDynamicPattern(ignorePattern)) { const isMatch = micromatch.matcher(ignorePattern) return (_filename, path) => !isMatch(path) } return filename => filename !== ignorePattern } export function createFilesFilter(ignoreFiles) { const ignorePatterns = ignoreFiles ? IGNORE_FILES.concat(ignoreFiles).filter(Boolean) : IGNORE_FILES const filter = ignorePatterns.reduce((next, ignorePattern) => { const ignoreMatcher = createIgnoreMatcher(ignorePattern) if (!next) { return ignoreMatcher } return (filename, path) => ignoreMatcher(filename, path) && next(filename, path) }, null) return path => { const filename = basename(path) return filter(filename, path) } } export async function copyFiles(cwd = './', tempDir, filter) { const rootFiles = await fs.readdir(cwd) // console.log('rootFiles', rootFiles) // console.log('tempDir', tempDir) return Promise.all( rootFiles.map(async file => { if (file !== tempDir) { const from = join(cwd, file) const to = join(cwd, tempDir, file) // console.log('[copy]', from, to) await copy(from, to, { filter }) } }) ) } export function publish( cwd, { packageManager, packageManagerOptions = [], access, tag, dryRun } ) { return new Promise((resolve, reject) => { const args = ['publish', ...packageManagerOptions] if (access) args.push('--access', access) if (tag) args.push('--tag', tag) if (dryRun) args.push('--dry-run') spawn(packageManager, args, { stdio: 'inherit', cwd }) .on('close', (code, signal) => { resolve({ code, signal }) }) .on('error', reject) }) } export async function createTempDirectory(cwd, name) { if (name) { try { await fs.mkdir(join(cwd, name)) } catch (err) { if (err.code === 'EEXIST') { throw new Error(`Temporary directory "${name}" already exists.`) } } return name } return await fs.mkdtemp(join(cwd, 'tmp')) } export function removeTempDirectory(directoryName) { return remove(directoryName) } export function runScript(script, ...args) { return new Promise((resolve, reject) => { spawn(script, args, { stdio: 'inherit' }) .on('close', code => { resolve(code === 0) }) .on('error', reject) }) } export function getReadmeUrlFromRepository(repository) { const repoUrl = typeof repository === 'object' ? repository.url : repository if (repoUrl) { const name = repoUrl.match(/[^/:]+\/[^/:]+$/)?.[0]?.replace(/\.git$/, '') return `https://github.com/${name}#readme` } return null } export async function cleanDocs(drectoryName, repository, homepage) { const readmePath = join(drectoryName, 'README.md') const readme = await fs.readFile(readmePath) const readmeUrl = getReadmeUrlFromRepository(repository) if (homepage || readmeUrl) { const cleaned = readme.toString().split(/\n##\s*\w/m)[0] + '\n## Docs\n' + `Read full docs **[here](${homepage || readmeUrl})**.\n` await fs.writeFile(readmePath, cleaned) } } export async function cleanComments(drectoryName) { const files = await glob(['**/*.js'], { cwd: drectoryName }) await Promise.all( files.map(async i => { const file = join(drectoryName, i) const content = await fs.readFile(file) const cleaned = content .toString() .replace(/\s*\/\/.*\n/gm, '\n') .replace(/\s*\/\*[^/]+\*\/\n?/gm, '\n') .replace(/\n+/gm, '\n') .replace(/^\n+/gm, '') await fs.writeFile(file, cleaned) }) ) } export async function cleanPublish(options) { const cwd = options.cwd || process.cwd() const tempDirectoryName = await createTempDirectory(cwd, options.tempDir) // console.log({tempDirectoryName}) const filesFilter = createFilesFilter(options.files) await copyFiles(cwd, tempDirectoryName, filesFilter) const packageJson = await readPackageJSON(cwd) if (options.cleanDocs) { await cleanDocs( tempDirectoryName, packageJson.repository, packageJson.homepage ) } if (options.cleanComments) { await cleanComments(tempDirectoryName) } const cleanPackageJSON = clearPackageJSON(packageJson, options.fields) await writePackageJSON(tempDirectoryName, cleanPackageJSON) let prepublishSuccess = true if (options.beforeScript) { prepublishSuccess = await runScript(options.beforeScript, tempDirectoryName) } if (!options.withoutPublish && prepublishSuccess) { await publish(tempDirectoryName, options) } if (!options.withoutPublish) { await removeTempDirectory(tempDirectoryName) } }