-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Automatically install missing dependencies, part 2 #805
Changes from 30 commits
866e2b7
60e160d
1cacb59
bcbb07d
0e80fe3
184e6f9
1d30d0c
8b98fee
9f27b5d
2bd5410
9308335
8354b61
b3ba099
b076b7d
8a3dc51
9680030
6f4a194
949c1df
b3ba28e
63c34f5
b26cb28
11a5c35
9c91de6
9e06479
bdb6485
2356c2a
4cf5d2d
06eecdd
40efbb3
89a668d
674d458
2030211
eb9c263
b32e731
37c412c
3343f28
6e07e6d
c462a4e
d2716cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,55 +1,32 @@ | ||
const spawn = require('cross-spawn'); | ||
const config = require('./config'); | ||
const path = require('path'); | ||
const promisify = require('./promisify'); | ||
const resolve = promisify(require('resolve')); | ||
const commandExists = require('command-exists'); | ||
const logger = require('../Logger'); | ||
|
||
async function install(dir, modules, installPeers = true) { | ||
let location = await config.resolve(dir, ['yarn.lock', 'package.json']); | ||
async function install(configObj) { | ||
let { | ||
dir: cwd, | ||
modules, | ||
installPeers = true, | ||
saveDev = true, | ||
packageManager | ||
} = configObj; | ||
|
||
return new Promise((resolve, reject) => { | ||
let install; | ||
let options = { | ||
cwd: location ? path.dirname(location) : dir | ||
}; | ||
|
||
if (location && path.basename(location) === 'yarn.lock') { | ||
install = spawn('yarn', ['add', ...modules, '--dev'], options); | ||
} else { | ||
install = spawn('npm', ['install', ...modules, '--save-dev'], options); | ||
} | ||
|
||
install.stdout.pipe(process.stdout); | ||
install.stderr.pipe(process.stderr); | ||
|
||
install.on('close', async code => { | ||
if (code !== 0) { | ||
return reject(new Error(`Failed to install ${modules.join(', ')}.`)); | ||
} | ||
|
||
if (!installPeers) { | ||
return resolve(); | ||
} | ||
packageManager = packageManager || (await determinePackageManager(cwd)); | ||
let commandToUse = packageManager === 'npm' ? 'install' : 'add'; | ||
let args = [commandToUse, ...modules, saveDev ? '-D' : null]; | ||
|
||
try { | ||
await Promise.all(modules.map(m => installPeerDependencies(dir, m))); | ||
} catch (err) { | ||
return reject( | ||
new Error( | ||
`Failed to install peerDependencies for ${modules.join(', ')}.` | ||
) | ||
); | ||
} | ||
await run(packageManager, args, {cwd}); | ||
|
||
resolve(); | ||
}); | ||
}); | ||
if (installPeers) { | ||
await Promise.all(modules.map(m => installPeerDependencies(cwd, m))); | ||
} | ||
} | ||
|
||
async function installPeerDependencies(dir, name) { | ||
let basedir = path.dirname(dir); | ||
|
||
const [resolved] = await resolve(name, {basedir}); | ||
const [resolved] = await resolve(name, {basedir: dir}); | ||
const pkg = await config.load(resolved, ['package.json']); | ||
const peers = pkg.peerDependencies || {}; | ||
|
||
|
@@ -59,8 +36,54 @@ async function installPeerDependencies(dir, name) { | |
} | ||
|
||
if (modules.length) { | ||
await install(dir, modules, false); | ||
await install({dir, modules, installPeers: false}); | ||
} | ||
} | ||
|
||
async function determinePackageManager(cwd) { | ||
let yarnLockFile = await config.resolve(cwd, ['yarn.lock']); | ||
let yarnCommandExists = await checkForYarnCommand(); | ||
|
||
// If the yarn command exists and we find a yarn lockfile, use yarn | ||
if (yarnCommandExists) { | ||
if (yarnLockFile) { | ||
return 'yarn'; | ||
} else { | ||
logger.warn( | ||
"Using NPM instead of Yarn. No 'yarn.lock' found in project directory, use the --package-manager flag to explicitly specify which package manager to use." | ||
); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed this logic slightly. Now we default to |
||
|
||
return 'npm'; | ||
} | ||
|
||
async function checkForYarnCommand() { | ||
try { | ||
return await commandExists('yarn'); | ||
} catch (err) { | ||
return false; | ||
} | ||
} | ||
|
||
function run(...args) { | ||
return new Promise((resolve, reject) => { | ||
// Spawn the process | ||
let childProcess = spawn(...args); | ||
|
||
// Setup outputs | ||
childProcess.stdout.pipe(process.stdout); | ||
childProcess.stderr.pipe(process.stderr); | ||
|
||
// Resolve the promise when the process finishes | ||
childProcess.on('close', statusCode => { | ||
if (statusCode === 0) { | ||
resolve(); | ||
} else { | ||
reject(new Error(`Install failure: ${args}`)); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
module.exports = install; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
const assert = require('assert'); | ||
const install = require('../src/utils/installPackage'); | ||
const fs = require('fs'); | ||
const rimraf = require('rimraf'); | ||
const promisify = require('../src/utils/promisify'); | ||
const primraf = promisify(rimraf); | ||
const ncp = promisify(require('ncp')); | ||
const inputDirPath = __dirname + '/input'; | ||
|
||
describe('autoinstall', function() { | ||
beforeEach(async function() { | ||
// Setup (clear the input dir and move integration test in) | ||
await primraf(inputDirPath, {}); | ||
await ncp(__dirname + '/integration/babel-default', inputDirPath); | ||
}); | ||
|
||
it('should install lodash using npm and save dev dependency to package.json', async function() { | ||
let pkgName = 'lodash'; | ||
|
||
await install({ | ||
dir: inputDirPath, | ||
modules: [pkgName], | ||
saveDev: true, | ||
packageManager: 'npm' | ||
}); | ||
|
||
let expectedModulePath = inputDirPath + '/node_modules/' + pkgName; | ||
assert(fs.existsSync(expectedModulePath), 'lodash is in node_modules'); | ||
|
||
let pkg = fs.readFileSync(inputDirPath + '/package.json'); | ||
pkg = JSON.parse(pkg); | ||
assert(pkg.devDependencies[pkgName], 'lodash is saved as a dev dep'); | ||
}); | ||
|
||
it('should install lodash using yarn and save dev dependency to package.json', async function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps also add a test that tests the autoinstall from require.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be good to do in a followup PR. Maybe we can cover a few more of the different cases as well. |
||
let pkgName = 'lodash'; | ||
|
||
await install({ | ||
dir: inputDirPath, | ||
modules: [pkgName], | ||
saveDev: true, | ||
packageManager: 'yarn' | ||
}); | ||
|
||
let expectedModulePath = inputDirPath + '/node_modules/' + pkgName; | ||
assert(fs.existsSync(expectedModulePath), 'lodash is in node_modules'); | ||
|
||
let pkg = fs.readFileSync(inputDirPath + '/package.json'); | ||
pkg = JSON.parse(pkg); | ||
assert(pkg.devDependencies[pkgName], 'lodash is saved as a dev dep'); | ||
}); | ||
|
||
afterEach(async function() { | ||
await primraf(inputDirPath); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this option really necessary? Our approach to inferring the package manager seems pretty solid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’s true, but I don’t really see any downsides to giving the users manual control if necessary.
One scenario that I would imagine will be particularly problematic is if a user is starting a brand new project with Parcel and has no dependencies yet. They try to add their first import statement, and Parcel automatically uses npm because it doesn’t see a
yarn.lock
, even though the user would rather use yarn. In that case they would want to use the-m yarn
flag to force it to use yarn.(I mean I suppose they could just
touch yarn.lock
to generate an empty one, but that’s kinda hacky and just another point for confusion)I can remove it if you think we should, but I just don’t see any downsides to leaving it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to remove it for now. I think our approach is pretty solid. If someone wants to open a bug with a good reason to add an option for it, they can.