Skip to content

Instantly share code, notes, and snippets.

@allex
Last active August 4, 2021 11:26
Show Gist options
  • Save allex/5ca7aa7bae0dc160c2e651611cc801ad to your computer and use it in GitHub Desktop.
Save allex/5ca7aa7bae0dc160c2e651611cc801ad to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
// GistID: 5ca7aa7bae0dc160c2e651611cc801ad
// GistURL: https://gist.github.com/allex/5ca7aa7bae0dc160c2e651611cc801ad
// by allex | MIT Licensed
// Usage: pack.js /foo/path -o - |tar tzf -
const sh = (cmd) => require('child_process').execSync(cmd).toString().trim()
const npmRoot = sh('npm root -g')
require('app-module-path').addPath(`${npmRoot}/npm/node_modules`)
const fs = require('fs')
const path = require('path')
const pack = require('libnpmpack')
const tar = require('tar')
const packlist = require('npm-packlist')
const error = (e) => console.error(e)
const argv = process.argv.slice(2)
const aliases = { 'o': 'out', 's': 'dir' }
const camelCase = s => s.replace(/-([a-z])/g, (_, $1) => $1.toUpperCase())
const getkey = k => aliases[k] || camelCase(k)
const opts = {
dir: '',
out: '',
prefix: 'package/',
prepare: false // ignore script:prepare, see runScript('prepare')
}
const remains = []
let k = ''
while (argv.length) {
let x = argv.shift()
// keep any remaining arguments to the positional parameters
if (x === '--') break
// -a, --foo
if (/^--?[a-z\d]/.test(x)) {
k = getkey(x.replace(/^--?/, ''))
// parse k/v tuple, eg. --compress=false
const match = /(\w+)=(\w+)/.exec(k)
if (match) {
k = getkey(match[1])
opts[k] = match[2] || ''
} else {
opts[k] = null
}
} else {
if (k && opts[k] == null) {
opts[k] = x
} else {
remains.push(x)
}
}
}
if (!opts.dir && remains[0]) {
opts.dir = remains[0]
}
if (!opts.out && process.stdout.isTTY) {
error('cowardly refusing to save to a terminal. Use the -o flag or redirect')
process.exit(1)
} else {
// defaults to stdout
opts.out = opts.out || '-'
}
opts.dir = path.resolve(opts.dir || process.cwd())
let package = null
const pkgfile = `${opts.dir}/package.json`
if (fs.existsSync(pkgfile)) {
package = require(pkgfile)
}
// Function to determine whether a path is in the package.bin set.
// Used to prevent issues when people publish a package from a
// windows machine, and then install with --no-bin-links.
//
// Note: this is not possible in remote or file fetchers, since
// we don't have the manifest until AFTER we've unpacked. But the
// main use case is registry fetching with git a distant second,
// so that's an acceptable edge case to not handle.
const binObj = (name, bin) =>
typeof bin === 'string' ? { [name]: bin } : bin
const hasBin = (pkg, path) => {
const bin = binObj(pkg.name, pkg.bin)
const p = path.replace(/^[^\\/]*\//, '')
for (const [_k, v] of Object.entries(bin)) {
if (v === p) return true
}
return false
}
const isPackageBin = (pkg, path) => pkg && pkg.bin ? hasBin(pkg, path) : false
const tarCreateOptions = (opts, manifest) => ({
cwd: manifest._resolved,
prefix: opts.prefix,
portable: true,
gzip: {
// forcing the level to 9 seems to avoid some
// platform specific optimizations that cause
// integrity mismatch errors due to differing
// end results after compression
level: 9
},
// ensure that package bins are always executable
// Note that npm-packlist is already filtering out
// anything that is not a regular file, ignored by
// .npmignore or package.json "files", etc.
filter: (path, stat) => {
if (isPackageBin(manifest, path)) stat.mode |= 0o111
return true
},
// Provide a specific date in the 1980s for the benefit of zip,
// which is confounded by files dated at the Unix epoch 0.
mtime: new Date('1985-10-26T08:15:00.000Z')
})
const packNpm = (opts, stream) => {
// run the prepare script, get the list of files, and tar it up
// pipe to the stream, and proxy errors the chain.
return packlist({ path: opts.dir })
.then(files => tar.c(tarCreateOptions(opts, package), files)
.on('error', er => error(er)).pipe(stream))
.catch(er => error(er))
}
async function main () {
const outfile = opts.out
let stdout = (outfile !== '-')
? fs.createWriteStream(outfile)
: process.stdout
if (!opts.prepare || !package) {
// ignore-script prepare
packNpm(opts, stdout)
} else {
// with script execute, runScript('prepare')
const tarballData = await pack(opts.dir)
stdout.write(tarballData)
stdout.end()
}
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment