diff --git a/commands/doctor.js b/commands/doctor.js new file mode 100644 index 00000000000..94c57bfc1fe --- /dev/null +++ b/commands/doctor.js @@ -0,0 +1,19 @@ +'use strict'; + +const fsp = require('fs').promises; +const { writeText, log } = require('@serverless/utils/log'); +const healthStatusFilename = require('../lib/utils/health-status-filename'); + +module.exports = async () => { + const healthStatus = await (async () => { + try { + return await fsp.readFile(healthStatusFilename); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } + })(); + + if (healthStatus) writeText(healthStatus); + else log.notice('No deprecations were reported in the last command'); +}; diff --git a/lib/cli/commands-schema/no-service.js b/lib/cli/commands-schema/no-service.js index 3f3f27b224e..24fd3105113 100644 --- a/lib/cli/commands-schema/no-service.js +++ b/lib/cli/commands-schema/no-service.js @@ -135,6 +135,12 @@ commands.set('dashboard', { serviceDependencyMode: 'optional', }); +commands.set('doctor', { + usage: 'Print status on reported deprecations triggered in the last command run', + // TODO: Expose in v3 + isHidden: true, +}); + commands.set('generate-event', { usage: 'Generate event', lifecycleEvents: ['generate-event'], diff --git a/lib/cli/handle-error.js b/lib/cli/handle-error.js index 24b334fddea..902a4fb6c6a 100644 --- a/lib/cli/handle-error.js +++ b/lib/cli/handle-error.js @@ -210,7 +210,7 @@ module.exports = async (exception, options = {}) => { legacy.consoleLog(chalk.yellow(` Components Version: ${componentsVersion}`)); legacy.consoleLog(' '); - logDeprecation.printSummary(); + await logDeprecation.printSummary(); if ( !isTelemetryDisabled && diff --git a/lib/utils/health-status-filename.js b/lib/utils/health-status-filename.js new file mode 100644 index 00000000000..d3491cb80e8 --- /dev/null +++ b/lib/utils/health-status-filename.js @@ -0,0 +1,6 @@ +'use strict'; + +const path = require('path'); +const os = require('os'); + +module.exports = path.resolve(os.homedir(), '.serverless/last-command-health-status'); diff --git a/lib/utils/logDeprecation.js b/lib/utils/logDeprecation.js index b64aa0a666d..143ccc0cb84 100644 --- a/lib/utils/logDeprecation.js +++ b/lib/utils/logDeprecation.js @@ -1,10 +1,15 @@ 'use strict'; +const path = require('path'); +const fse = require('fs-extra'); +const fsp = require('fs').promises; const chalk = require('chalk'); const weakMemoizee = require('memoizee/weak'); const _ = require('lodash'); +const resolveTmpdir = require('process-utils/tmpdir'); const ServerlessError = require('../serverless-error'); const { style, legacy, log } = require('@serverless/utils/log'); +const healthStatusFilename = require('./health-status-filename'); const disabledDeprecationCodesByEnv = extractCodes(process.env.SLS_DEPRECATION_DISABLE); @@ -112,27 +117,58 @@ module.exports.flushBuffered = () => { } }; -module.exports.printSummary = () => { - if (!bufferedDeprecations.length) return; +module.exports.printSummary = async () => { + if (!bufferedDeprecations.length) { + try { + await fsp.unlink(healthStatusFilename); + } catch { + // ignore + } + return; + } try { + const healthStatus = []; + deprecationLogger.warning(); if (bufferedDeprecations.length === 1) { + healthStatus.push('1 deprecation triggered in the last command:', ''); deprecationLogger.warning( style.aside("1 deprecation found: run 'serverless doctor' for more details") ); + } else { + healthStatus.push( + `${bufferedDeprecations.length} deprecations triggered in the last command:`, + '' + ); + deprecationLogger.warning( + style.aside( + `${bufferedDeprecations.length} deprecations found: run 'serverless doctor' for more details` + ) + ); + } + for (const { code, message } of bufferedDeprecations) { + healthStatus.push(message); + if (!code.startsWith('EXT_')) { + healthStatus.push( + style.aside(`More info: https://serverless.com/framework/docs/deprecations/#${code}`) + ); + } + } + + const tmpHealthStatusFilename = path.resolve(await resolveTmpdir(), 'health-status'); + await Promise.all([ + fse.ensureDir(path.dirname(healthStatusFilename)), + fsp.writeFile(tmpHealthStatusFilename, healthStatus.join('\n')), + ]); + await fsp.rename(tmpHealthStatusFilename, healthStatusFilename); + + if (bufferedDeprecations.length === 1) { const { code, message } = bufferedDeprecations[0]; writeDeprecation(code, message); return; } - - deprecationLogger.warning( - style.aside( - `${bufferedDeprecations.length} deprecations found: run 'serverless doctor' for more details` - ) - ); - const prefix = 'Serverless: '; legacy.write(`${prefix}${chalk.bold.keyword('orange')('Deprecation warnings:')}\n\n`); for (const { code, message } of bufferedDeprecations) { diff --git a/scripts/serverless.js b/scripts/serverless.js index f9e2a2c6984..2f1b865ca33 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -41,7 +41,7 @@ let hasTelemetryBeenReported = false; // to properly handle e.g. `SIGINT` interrupt const keepAliveTimer = setTimeout(() => {}, 60 * 60 * 1000); -const standaloneCommands = new Set(['plugin install', 'plugin uninstall']); +const standaloneCommands = new Set(['doctor', 'plugin install', 'plugin uninstall']); process.once('uncaughtException', (error) => { clearTimeout(keepAliveTimer); @@ -123,7 +123,7 @@ const processSpanPromise = (async () => { // If version number request, show it and abort if (options.version) { await require('../lib/cli/render-version')(); - logDeprecation.printSummary(); + await logDeprecation.printSummary(); return; } @@ -542,7 +542,7 @@ const processSpanPromise = (async () => { progress.clear(); - logDeprecation.printSummary(); + await logDeprecation.printSummary(); if (!hasTelemetryBeenReported) { hasTelemetryBeenReported = true; @@ -831,7 +831,7 @@ const processSpanPromise = (async () => { } progress.clear(); - logDeprecation.printSummary(); + await logDeprecation.printSummary(); if (!hasTelemetryBeenReported) { hasTelemetryBeenReported = true; diff --git a/test/unit/commands/doctor.test.js b/test/unit/commands/doctor.test.js new file mode 100644 index 00000000000..c351569d9a7 --- /dev/null +++ b/test/unit/commands/doctor.test.js @@ -0,0 +1,35 @@ +'use strict'; + +const chai = require('chai'); +const path = require('path'); +const spawn = require('child-process-ext/spawn'); +const { expect } = require('chai'); + +chai.use(require('chai-as-promised')); + +const serverlessPath = path.resolve(__dirname, '../../../scripts/serverless.js'); + +describe('test/unit/commands/doctor.test.js', async () => { + before(() => { + process.env.SLS_DEPRECATION_NOTIFICATION_MODE = 'warn:summary'; + process.env.SLS_DEV_LOG_MODE = '3'; + }); + + it('should print health status after command which triggered deprecation', async () => { + // Trigger deprecation + await spawn('node', [serverlessPath, 'config', '--foo']); + + // Gather Health status + expect(String((await spawn('node', [serverlessPath, 'doctor'])).stdoutBuffer)).to.include( + 'deprecation triggered in the last command' + ); + }); + + it('should inform of no issues when no health status found', async () => { + // Trigger command that reports no issues + await spawn('node', [serverlessPath, 'config', '--help']); + + // Gather Health status + expect(String((await spawn('node', [serverlessPath, 'doctor'])).stdoutBuffer)).to.be.empty; + }); +}); diff --git a/test/unit/lib/utils/logDeprecation.test.js b/test/unit/lib/utils/logDeprecation.test.js index 4fabf61f850..013607d4746 100644 --- a/test/unit/lib/utils/logDeprecation.test.js +++ b/test/unit/lib/utils/logDeprecation.test.js @@ -13,7 +13,9 @@ describe('test/unit/lib/utils/logDeprecation.test.js', () => { beforeEach(() => { delete require.cache[require.resolve('../../../../lib/utils/logDeprecation')]; - ({ originalEnv, restoreEnv } = overrideEnv()); + ({ originalEnv, restoreEnv } = overrideEnv({ + whitelist: ['APPDATA', 'HOME', 'PATH', 'TEMP', 'TMP', 'TMPDIR', 'USERPROFILE'], + })); }); afterEach(() => { @@ -147,17 +149,17 @@ describe('test/unit/lib/utils/logDeprecation.test.js', () => { expect(stdoutData).to.include('Second deprecation'); }); - it('should expose working `printSummary` method', () => { + it('should expose working `printSummary` method', async () => { let stdoutData = ''; - overrideStdoutWrite( + await overrideStdoutWrite( (data) => (stdoutData += data), - () => { + async () => { const logDeprecation = require('../../../../lib/utils/logDeprecation'); logDeprecation('CODE1', 'First deprecation'); expect(stdoutData).to.not.include('First deprecation'); logDeprecation('CODE2', 'Second deprecation'); expect(stdoutData).to.not.include('Second deprecation'); - logDeprecation.printSummary(); + await logDeprecation.printSummary(); } ); expect(stdoutData).to.include('First deprecation');