pacote
Version:
JavaScript package downloader
161 lines (140 loc) • 5.6 kB
JavaScript
const { resolve, join } = require('node:path')
const { mkdtemp, writeFile, rm } = require('node:fs/promises')
const { tmpdir } = require('node:os')
const packlist = require('npm-packlist')
const runScript = require('@npmcli/run-script')
const tar = require('tar')
const { Minipass } = require('minipass')
const PackageJson = require('@npmcli/package-json')
const Fetcher = require('./fetcher.js')
const FileFetcher = require('./file.js')
const _ = require('./util/protected.js')
const tarCreateOptions = require('./util/tar-create-options.js')
class DirFetcher extends Fetcher {
constructor (spec, opts) {
super(spec, opts)
// just the fully resolved filename
this.resolved = this.spec.fetchSpec
this.tree = opts.tree || null
this.Arborist = opts.Arborist || null
}
// exposes tarCreateOptions as public API
static tarCreateOptions (manifest) {
return tarCreateOptions(manifest)
}
get types () {
return ['directory']
}
#prepareDir () {
return this.manifest().then(mani => {
if (!mani.scripts || !mani.scripts.prepare) {
return
}
if (this.opts.ignoreScripts) {
return
}
// we *only* run prepare.
// pre/post-pack is run by the npm CLI for publish and pack,
// but this function is *also* run when installing git deps
const stdio = this.opts.foregroundScripts ? 'inherit' : 'pipe'
return runScript({
// this || undefined is because runScript will be unhappy with the default null value
scriptShell: this.opts.scriptShell || undefined,
pkg: mani,
event: 'prepare',
path: this.resolved,
stdio,
env: {
npm_package_resolved: this.resolved,
npm_package_integrity: this.integrity,
npm_package_json: resolve(this.resolved, 'package.json'),
},
})
})
}
[_.tarballFromResolved] () {
if (!this.tree && !this.Arborist) {
throw new Error('DirFetcher requires either a tree or an Arborist constructor to pack')
}
const stream = new Minipass()
stream.resolved = this.resolved
stream.integrity = this.integrity
const { prefix, workspaces, globalIgnoreFile } = this.opts
// run the prepare script, get the list of files, and tar it up
// pipe to the stream, and proxy errors the chain.
this.#prepareDir()
.then(async () => {
if (!this.tree) {
const arb = new this.Arborist({ path: this.resolved })
this.tree = await arb.loadActual()
}
return packlist(this.tree, { path: this.resolved, prefix, workspaces, globalIgnoreFile })
})
.then(async files => {
const { options, cleanup } = await this.#tarOptions()
const source = tar.c(options, files)
// the strip temp file must outlive content consumption, so clean up once the stream is done
source.once('end', cleanup)
source.once('error', cleanup)
return source.on('error', er => stream.emit('error', er)).pipe(stream)
})
.catch(er => stream.emit('error', er))
return stream
}
// Build the tar create options.
// When the packed package.json declares patchedDependencies, redirect it to a stripped copy so project-local patches never ship.
// Non-patched packs are unchanged.
async #tarOptions () {
const options = tarCreateOptions(this.package)
// read package.json from disk after prepare so the strip reflects the actually-packed manifest.
const pkgJson = await PackageJson.load(this.resolved)
if (!('patchedDependencies' in pkgJson.content)) {
return { options, cleanup: () => {} }
}
// serialize the package.json minus patchedDependencies, preserving its indent and newline.
// JSON.stringify ignores the indent and newline symbols @npmcli/package-json attaches to content.
delete pkgJson.content.patchedDependencies
const { content } = pkgJson
const indent = content[Symbol.for('indent')]
const newline = content[Symbol.for('newline')]
const stripped = `${JSON.stringify(content, null, indent)}\n`.replace(/\n/g, newline)
// write the stripped copy to a temp dir, removing it if the write itself fails.
const dir = await mkdtemp(join(tmpdir(), 'pacote-pack-'))
const strippedPath = join(dir, 'package.json')
try {
await writeFile(strippedPath, stripped)
} catch (er) {
/* istanbul ignore next - writing to a freshly created temp dir is not deterministically failable */
await rm(dir, { recursive: true, force: true })
/* istanbul ignore next */
throw er
}
const size = Buffer.byteLength(stripped)
// point only the top-level package.json entry at the stripped copy; every other file is untouched.
// onWriteEntry runs before the tar header and the file's hardlink check, so size and nlink here are honored.
options.onWriteEntry = (entry) => {
if (entry.path === 'package.json') {
entry.absolute = strippedPath
entry.stat.size = size
entry.stat.nlink = 1
}
}
return { options, cleanup: () => rm(dir, { recursive: true, force: true }) }
}
manifest () {
if (this.package) {
return Promise.resolve(this.package)
}
return this[_.readPackageJson](this.resolved)
.then(mani => this.package = {
...mani,
_integrity: this.integrity && String(this.integrity),
_resolved: this.resolved,
_from: this.from,
})
}
packument () {
return FileFetcher.prototype.packument.apply(this)
}
}
module.exports = DirFetcher