-
-
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 all 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 |
---|---|---|
|
@@ -16,6 +16,7 @@ const config = require('./utils/config'); | |
const emoji = require('./utils/emoji'); | ||
const loadEnv = require('./utils/env'); | ||
const PromiseQueue = require('./utils/PromiseQueue'); | ||
const installPackage = require('./utils/installPackage'); | ||
const bundleReport = require('./utils/bundleReport'); | ||
const prettifyTime = require('./utils/prettifyTime'); | ||
|
||
|
@@ -96,7 +97,8 @@ class Bundler extends EventEmitter { | |
hmrHostname: | ||
options.hmrHostname || | ||
(options.target === 'electron' ? 'localhost' : ''), | ||
detailedReport: options.detailedReport || false | ||
detailedReport: options.detailedReport || false, | ||
autoinstall: (options.autoinstall || false) && !isProduction | ||
}; | ||
} | ||
|
||
|
@@ -325,38 +327,70 @@ class Bundler extends EventEmitter { | |
} | ||
} | ||
|
||
async resolveDep(asset, dep) { | ||
async resolveDep(asset, dep, install = true) { | ||
try { | ||
return await this.resolveAsset(dep.name, asset.name); | ||
} catch (err) { | ||
let thrown = err; | ||
|
||
if (thrown.message.indexOf(`Cannot find module '${dep.name}'`) === 0) { | ||
// Check if dependency is a local file | ||
let isLocalFile = /^[/~.]/.test(dep.name); | ||
let fromNodeModules = asset.name.includes( | ||
`${Path.sep}node_modules${Path.sep}` | ||
); | ||
|
||
// If it's not a local file, attempt to install the dep | ||
if ( | ||
!isLocalFile && | ||
!fromNodeModules && | ||
this.options.autoinstall && | ||
install | ||
) { | ||
return await this.installDep(asset, dep); | ||
} | ||
|
||
// If the dep is optional, return before we throw | ||
if (dep.optional) { | ||
return; | ||
} | ||
|
||
thrown.message = `Cannot resolve dependency '${dep.name}'`; | ||
|
||
// Add absolute path to the error message if the dependency specifies a relative path | ||
if (dep.name.startsWith('.')) { | ||
if (isLocalFile) { | ||
const absPath = Path.resolve(Path.dirname(asset.name), dep.name); | ||
err.message += ` at '${absPath}'`; | ||
} | ||
|
||
// Generate a code frame where the dependency was used | ||
if (dep.loc) { | ||
await asset.loadIfNeeded(); | ||
thrown.loc = dep.loc; | ||
thrown = asset.generateErrorMessage(thrown); | ||
thrown.message += ` at '${absPath}'`; | ||
} | ||
|
||
thrown.fileName = asset.name; | ||
await this.throwDepError(asset, dep, thrown); | ||
} | ||
|
||
throw thrown; | ||
} | ||
} | ||
|
||
async installDep(asset, dep) { | ||
let [moduleName] = this.resolver.getModuleParts(dep.name); | ||
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. Used this function to get the module name to install. If you required e.g. |
||
try { | ||
await installPackage([moduleName], asset.name, {saveDev: false}); | ||
} catch (err) { | ||
await this.throwDepError(asset, dep, err); | ||
} | ||
|
||
return await this.resolveDep(asset, dep, false); | ||
} | ||
|
||
async throwDepError(asset, dep, err) { | ||
// Generate a code frame where the dependency was used | ||
if (dep.loc) { | ||
await asset.loadIfNeeded(); | ||
err.loc = dep.loc; | ||
err = asset.generateErrorMessage(err); | ||
} | ||
|
||
err.fileName = asset.name; | ||
throw err; | ||
} | ||
|
||
async processAsset(asset, isRebuild) { | ||
if (isRebuild) { | ||
asset.invalidate(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,54 +1,55 @@ | ||
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'); | ||
const emoji = require('./emoji'); | ||
const pipeSpawn = require('./pipeSpawn'); | ||
const PromiseQueue = require('./PromiseQueue'); | ||
const path = require('path'); | ||
const fs = require('./fs'); | ||
|
||
async function install(dir, modules, installPeers = true) { | ||
let location = await config.resolve(dir, ['yarn.lock', 'package.json']); | ||
|
||
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(); | ||
} | ||
|
||
try { | ||
await Promise.all(modules.map(m => installPeerDependencies(dir, m))); | ||
} catch (err) { | ||
return reject( | ||
new Error( | ||
`Failed to install peerDependencies for ${modules.join(', ')}.` | ||
) | ||
); | ||
} | ||
|
||
resolve(); | ||
}); | ||
}); | ||
} | ||
async function install(modules, filepath, options = {}) { | ||
let {installPeers = true, saveDev = true, packageManager} = options; | ||
|
||
logger.status(emoji.progress, `Installing ${modules.join(', ')}...`); | ||
|
||
let packageLocation = await config.resolve(filepath, ['package.json']); | ||
let cwd = packageLocation ? path.dirname(packageLocation) : process.cwd(); | ||
|
||
if (!packageManager) { | ||
packageManager = await determinePackageManager(filepath); | ||
} | ||
|
||
let commandToUse = packageManager === 'npm' ? 'install' : 'add'; | ||
let args = [commandToUse, ...modules]; | ||
if (saveDev) { | ||
args.push('-D'); | ||
} else if (packageManager === 'npm') { | ||
args.push('--save'); | ||
} | ||
|
||
// npm doesn't auto-create a package.json when installing, | ||
// so create an empty one if needed. | ||
if (packageManager === 'npm' && !packageLocation) { | ||
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. Added this to make sure we create a package.json when needed. |
||
await fs.writeFile(path.join(cwd, 'package.json'), '{}'); | ||
} | ||
|
||
try { | ||
await pipeSpawn(packageManager, args, {cwd}); | ||
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. Switched to using the |
||
} catch (err) { | ||
throw new Error(`Failed to install ${modules.join(', ')}.`); | ||
} | ||
|
||
async function installPeerDependencies(dir, name) { | ||
let basedir = path.dirname(dir); | ||
if (installPeers) { | ||
await Promise.all( | ||
modules.map(m => installPeerDependencies(filepath, m, options)) | ||
); | ||
} | ||
} | ||
|
||
async function installPeerDependencies(filepath, name, options) { | ||
let basedir = path.dirname(filepath); | ||
const [resolved] = await resolve(name, {basedir}); | ||
const pkg = await config.load(resolved, ['package.json']); | ||
const peers = pkg.peerDependencies || {}; | ||
|
@@ -59,8 +60,47 @@ async function installPeerDependencies(dir, name) { | |
} | ||
|
||
if (modules.length) { | ||
await install(dir, modules, false); | ||
await install( | ||
modules, | ||
filepath, | ||
Object.assign({}, options, {installPeers: false}) | ||
); | ||
} | ||
} | ||
|
||
module.exports = install; | ||
async function determinePackageManager(filepath) { | ||
let configFile = await config.resolve(filepath, [ | ||
'yarn.lock', | ||
'package-lock.json' | ||
]); | ||
let hasYarn = await checkForYarnCommand(); | ||
|
||
// If Yarn isn't available, or there is a package-lock.json file, use npm. | ||
let configName = configFile && path.basename(configFile); | ||
if (!hasYarn || configName === 'package-lock.json') { | ||
return 'npm'; | ||
} | ||
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 'yarn'; | ||
} | ||
|
||
let hasYarn = null; | ||
async function checkForYarnCommand() { | ||
if (hasYarn != null) { | ||
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. Caching if we have yarn or not |
||
return hasYarn; | ||
} | ||
|
||
try { | ||
hasYarn = await commandExists('yarn'); | ||
} catch (err) { | ||
hasYarn = false; | ||
} | ||
|
||
return hasYarn; | ||
} | ||
|
||
let queue = new PromiseQueue(install, {maxConcurrent: 1, retry: false}); | ||
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. Added a queue for install calls so that we can never have more than one install happening at the same time. Otherwise, npm will fail trying to write over the same files simultaneously. |
||
module.exports = function(...args) { | ||
queue.add(...args); | ||
return queue.run(); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,26 @@ | ||
const spawn = require('cross-spawn'); | ||
const logger = require('../Logger'); | ||
|
||
function pipeSpawn(cmd, params, opts) { | ||
const cp = spawn(cmd, params, opts); | ||
cp.stdout.pipe(process.stdout); | ||
cp.stderr.pipe(process.stderr); | ||
const cp = spawn(cmd, params, Object.assign({ | ||
env: Object.assign({ | ||
FORCE_COLOR: logger.color, | ||
npm_config_color: logger.color ? 'always': '', | ||
npm_config_progress: true | ||
}, process.env) | ||
}, opts)); | ||
|
||
cp.stdout.setEncoding('utf8').on('data', d => logger.writeRaw(d)); | ||
cp.stderr.setEncoding('utf8').on('data', d => logger.writeRaw(d)); | ||
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.
|
||
|
||
return new Promise((resolve, reject) => { | ||
cp.on('error', reject); | ||
cp.on('close', function(code) { | ||
if (code !== 0) { | ||
return reject(new Error(cmd + ' failed.')); | ||
} | ||
|
||
logger.clear(); | ||
return resolve(); | ||
}); | ||
}); | ||
|
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.
Added a check here to make sure we don't auto install dependencies of things inside node_modules, only application code. If a dependency is missing from within a node_modules folder, that is a bug in the package and so we should error.