Last active
August 4, 2021 11:26
-
-
Save allex/5ca7aa7bae0dc160c2e651611cc801ad to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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