From 2e06694c3d168feb6811f1a8b715b1eda67346c0 Mon Sep 17 00:00:00 2001 From: Christian Lewis Date: Sat, 23 Jan 2021 18:26:13 -0600 Subject: [PATCH] feat(errors): add more verbose InternalCliError class with stack trace --- src/detectors/utils/jsdetect.js | 57 ++++++++++++++++++--------------- src/utils/detect-server.js | 30 +++++++++++------ src/utils/detect-server.test.js | 2 +- src/utils/error.js | 27 ++++++++++++++++ 4 files changed, 80 insertions(+), 36 deletions(-) create mode 100644 src/utils/error.js diff --git a/src/detectors/utils/jsdetect.js b/src/detectors/utils/jsdetect.js index 462abe1010f..0d2d32025b3 100644 --- a/src/detectors/utils/jsdetect.js +++ b/src/detectors/utils/jsdetect.js @@ -5,10 +5,12 @@ */ const { existsSync, readFileSync } = require('fs') +const { InternalCliError } = require('../../utils/error') +const { NETLIFYDEVWARN } = require('../../utils/logo') + let pkgJSON = null let yarnExists = false let warnedAboutEmptyScript = false -const { NETLIFYDEVWARN } = require('../../utils/logo') /** hold package.json in a singleton so we dont do expensive parsing repeatedly */ const getPkgJSON = function () { @@ -28,9 +30,7 @@ const getYarnOrNPMCommand = function () { /** * real utiltiies are down here - * */ - const hasRequiredDeps = function (requiredDepArray) { const { dependencies, devDependencies } = getPkgJSON() for (const depName of requiredDepArray) { @@ -53,9 +53,9 @@ const hasRequiredFiles = function (filenameArr) { // preferredScriptsArr is in decreasing order of preference const scanScripts = function ({ preferredScriptsArr, preferredCommand }) { - const { scripts } = getPkgJSON() + const packageJsonScripts = getPkgJSON().scripts - if (!scripts && !warnedAboutEmptyScript) { + if (!packageJsonScripts && !warnedAboutEmptyScript) { console.log(`${NETLIFYDEVWARN} You have a package.json without any npm scripts.`) console.log( `${NETLIFYDEVWARN} Netlify Dev's detector system works best with a script, or you can specify a command to run in the netlify.toml [dev] block `, @@ -65,26 +65,33 @@ const scanScripts = function ({ preferredScriptsArr, preferredCommand }) { // not going to match any scripts anyway return [] } - // - // - // NOTE: we return an array of arrays (args) - // because we may want to supply extra args in some setups - // - // e.g. ['eleventy', '--serve', '--watch'] - // - // array will in future be sorted by likelihood of what we want - // - // - // this is very simplistic logic, we can offer far more intelligent logic later - // eg make a dependency tree of npm scripts and offer the parentest node first - return Object.entries(scripts) - .filter( - ([scriptName, scriptCommand]) => - (preferredScriptsArr.includes(scriptName) || scriptCommand.includes(preferredCommand)) && - // prevent netlify dev calling netlify dev - !scriptCommand.includes('netlify dev'), - ) - .map(([scriptName]) => [scriptName]) + /** + * NOTE: we return an array of arrays (args) because we may want to supply + * extra args in some setups, e.g. + * + * ['eleventy', '--serve', '--watch'] + * + * array will be sorted by likelihood of what we want in the future. this is + * very simplistic logic, we can offer far more intelligent logic later, e.g. + * make a dependency tree of npm scripts and offer the parentest node first + */ + const matchedScripts = [] + for (const [scriptName, scriptCommand] of Object.entries(packageJsonScripts)) { + /** + * Throw if trying to call Netlify dev from within Netlify dev. Include + * detailed information about the CLI setup in the error text. + */ + if (scriptCommand.includes('netlify dev')) { + throw new InternalCliError('Cannot call `netlify dev` inside `netlify dev`.', { packageJsonScripts }) + } + /** + * Otherwise, push the match. + */ + if (preferredScriptsArr.includes(scriptName) || scriptCommand.includes(preferredCommand)) { + matchedScripts.push([scriptName]) + } + } + return matchedScripts } module.exports = { diff --git a/src/utils/detect-server.js b/src/utils/detect-server.js index 099261bba9a..3902fb8a855 100644 --- a/src/utils/detect-server.js +++ b/src/utils/detect-server.js @@ -8,6 +8,7 @@ const getPort = require('get-port') const inquirer = require('inquirer') const inquirerAutocompletePrompt = require('inquirer-autocomplete-prompt') +const { InternalCliError } = require('./error') const { NETLIFYDEVLOG, NETLIFYDEVWARN } = require('./logo') const serverSettings = async (devConfig, flags, projectDir, log) => { @@ -222,20 +223,29 @@ const loadDetector = function (detectorName) { } } +/** + * Get the default args from an array of possible args. + * + * @param {Array?} possibleArgsArrs + * @returns {Array} + */ const chooseDefaultArgs = function (possibleArgsArrs) { - // vast majority of projects will only have one matching detector - // just pick the first one + /** + * Select first set of possible args. + */ const [args] = possibleArgsArrs if (!args) { - const { scripts } = JSON.parse(fs.readFileSync('package.json', { encoding: 'utf8' })) - const err = new Error( - 'Empty args assigned, this is an internal Netlify Dev bug, please report your settings and scripts so we can improve', - ) - err.scripts = scripts - err.possibleArgsArrs = possibleArgsArrs - throw err + /** + * Load scripts from package.json (if it exists) to display them in the + * error. Initialize `scripts` to `null` so it is not omitted by + * JSON.stringify() in the case it isn't reassigned. + */ + let packageJsonScripts = null + if (fs.existsSync('package.json')) { + packageJsonScripts = JSON.parse(fs.readFileSync('package.json')).scripts + } + throw new InternalCliError('No possible args found.', { packageJsonScripts, possibleArgsArrs }) } - return args } diff --git a/src/utils/detect-server.test.js b/src/utils/detect-server.test.js index 1e2e10b0b5e..4136f61ff4a 100644 --- a/src/utils/detect-server.test.js +++ b/src/utils/detect-server.test.js @@ -188,7 +188,7 @@ test('serverSettings: no config', async (t) => { t.is(settings.noCmd, true) }) -test('chooseDefaultArgs', (t) => { +test('chooseDefaultArgs: select first from possibleArgsArrs', (t) => { const possibleArgsArrs = [['run', 'dev'], ['run develop']] const args = chooseDefaultArgs(possibleArgsArrs) t.deepEqual(args, possibleArgsArrs[0]) diff --git a/src/utils/error.js b/src/utils/error.js new file mode 100644 index 00000000000..4199de44a44 --- /dev/null +++ b/src/utils/error.js @@ -0,0 +1,27 @@ +/** + * An unrecoverable internal CLI error which should be reported. + */ +class InternalCliError extends Error { + /** + * Log a stack trace and the given context object for opening an issue, then + * throw. + * + * @param {string!} message + * @param {Object!} context + */ + constructor(message, context) { + super(message) + this.name = 'InternalCliError' + + console.trace( + `INTERNAL CLI ERROR. ${message}\n` + + 'Please open an issue at https://github.com/netlify/cli/issues/new ' + + 'and include the following information:' + + `\n${JSON.stringify(context, null, 2)}\n`, + ) + } +} + +module.exports = { + InternalCliError, +}