postinstaller
Version:
Publish dev-friendly ❤️ zero-configuration packages.
153 lines (136 loc) • 4.34 kB
JavaScript
#! /usr/bin/env node
// Ue `npm_lifecycle_event`
// Use `npm_lifecycle_script`
const {join} = require('path')
const log = require('npmlog')
const {readFileSync, writeFileSync} = require('graceful-fs')
const get = require('lodash.get')
const set = require('lodash.set')
const yargs = require('yargs')
log.heading = 'postinstaller'
log.level = process.env.npm_config_loglevel || 'info'
const npmRunDir = process.env.INIT_CWD || process.cwd()
log.silly('', process.env.npm_lifecycle_event)
log.silly('', {
npmRunDir,
cwd: process.cwd()
})
if (npmRunDir === process.cwd()) {
log.silly('', 'Not installing on self, exiting.')
process.exit(0)
}
yargs // eslint-disable-line no-unused-expressions
.scriptName('postinstaller')
.options({
'recipe-file': {
alias: 'f',
desc: 'recipe JSON file',
default: join(process.cwd(), './package.json'),
type: 'string',
conflicts: 'deps',
global: true
},
'recipe-key': {
alias: 'k',
desc: 'key in `file` containing recipe',
default: 'postinstaller',
type: 'string',
conflicts: 'deps',
global: true
},
'target-file': {
alias: 't',
desc: 'target JSON file',
type: 'string',
default: join(npmRunDir, './package.json'),
global: true
},
'target-key': {
type: 'string'
},
force: {
desc: 'overwrite existing values',
boolean: true,
global: true
},
'dry-run': {
desc: 'just print out the results, don’t actually write to files',
boolean: true,
global: true
}
})
.command({
command: ['install [package]', ...process.env.npm_lifecycle_event === 'postinstall' ? ['$0'] : []],
aliases: ['i', 'add', 'a'],
desc: 'install',
handler: installOrUninstall('install')
})
.command({
command: ['uninstall [package]', ...process.env.npm_lifecycle_event === 'preuninstall' ? ['$0'] : []],
aliases: ['u', 'remove', 'r'],
desc: 'uninstall',
handler: installOrUninstall('uninstall')
})
.demandCommand(1, 'You need at least specify one command before moving on')
.epilogue('for more information, see the docs at https://github.com/postinstaller')
.wrap(yargs.terminalWidth())
.help()
.argv
// > Scripts are run from the root of the module, regardless of what your current
// > working directory is when you call npm run. If you want your script to use
// > different behavior based on what subdirectory you’re in, you can use the
// > INIT_CWD environment variable, which holds the full path you were in when
// > you ran npm run.
// https://docs.npmjs.com/cli/run-script
function installOrUninstall(method) {
return argv => {
log.silly('', {argv})
const packageFile = argv.package && (() => {
const pkg = require('resolve-pkg')(argv.package, {cwd: npmRunDir})
if (!pkg) {
throw new Error('Module not found', argv.package)
}
return join(pkg, 'package.json')
})()
const recipes = readJsonFile(packageFile || argv['recipe-file'], argv['recipe-key'])
if (!recipes.cwd) {
log.notice('skipped', 'No recipes found.')
return
}
log.info(method, recipes.data.name)
const target = readJsonFile(argv['target-file'], argv['target-key'])
const updated = require('./postinstaller')[method](recipes.cwd, target.cwd)
if (require('deep-equal')(updated, target.cwd)) {
log.verbose('', 'no changes.')
return
}
const path = 'data'
+ (argv['target-key']
? `.${argv['target-key']}`
: '')
set(target, path, updated)
if (argv['dry-run']) {
log.info('dry-run', target.toString())
} else {
writeFileSync(argv['target-file'], target.toString(), {encoding: 'utf-8'})
}
log.notice(`${method}ed`, recipes.data.name)
}
}
function readJsonFile(filename, cwdKey) {
const text = readFileSync(filename, 'utf-8')
const [/* unused */, ending] = text.match(/}(\s*)$/) || [null, '\n']
const [indent] = text.match(/^\s+/m) || [2]
const data = JSON.parse(text)
const cwd = cwdKey ? get(data, cwdKey) : data
const self = {
data,
cwd,
ending,
indent,
toString: () => JSON.stringify(self.data, null, self.indent) + self.ending
}
return self
}
// Accuracy creates clarity.
// Ambiguity creates confusion.