diff --git a/components/framework/index.js b/components/framework/index.js index c0e46f1..aa0a8aa 100644 --- a/components/framework/index.js +++ b/components/framework/index.js @@ -15,6 +15,21 @@ const MINIMAL_FRAMEWORK_VERSION = '3.7.7'; const doesSatisfyRequiredFrameworkVersion = (version) => semver.gte(version, MINIMAL_FRAMEWORK_VERSION); +const COMMAND_PROGRESS_TEXT = Object.freeze({ + 'deploy:function': ['deploying function', 'deployed function'], + 'deploy:list': ['listing deployments', 'listed deployments'], + 'deploy:list:functions': ['listing function deployments', 'listed function deployments'], + 'rollback:function': ['rolling back function', 'rolled back function'], + 'invoke': ['invoking function', 'invoked function'], + 'invoke:local': ['invoking function locally', 'invoked function locally'], +}); + +const formatCommandProgressText = (text, options) => { + const functionName = options.function || options.f; + if (!functionName) return text; + return `${text} "${functionName}"`; +}; + const formatCliParam = (key, value) => { if (value === true) { // Support flags like `--verbose` @@ -61,6 +76,11 @@ class ServerlessFramework { // }; // For now the workaround is to just pray that the command is correct and rely on validation from the Framework async command(command, options) { + const progressText = COMMAND_PROGRESS_TEXT[command]; + if (progressText) { + this.context.startProgress(formatCommandProgressText(progressText[0], options)); + } + const cliparams = Object.entries(options) .filter(([key]) => key !== 'stage') .flatMap(([key, value]) => { @@ -68,7 +88,13 @@ class ServerlessFramework { return values.flatMap((singleValue) => formatCliParam(key, singleValue)); }); const args = [...command.split(':'), ...cliparams]; - return await this.exec('serverless', args, true); + const result = await this.exec('serverless', args, true); + + if (progressText) { + this.context.successProgress(formatCommandProgressText(progressText[1], options)); + } + + return result; } async deploy() { diff --git a/test/unit/components/framework/index.test.js b/test/unit/components/framework/index.test.js index 8e695a9..1be3798 100644 --- a/test/unit/components/framework/index.test.js +++ b/test/unit/components/framework/index.test.js @@ -499,6 +499,105 @@ describe('test/unit/components/framework/index.test.js', () => { ]); }); + it('shows command-specific progress for selected passthrough commands', async () => { + const cases = [ + { + command: 'deploy:function', + options: { function: 'handler' }, + start: 'deploying function "handler"', + success: 'deployed function "handler"', + }, + { + command: 'deploy:list', + options: {}, + start: 'listing deployments', + success: 'listed deployments', + }, + { + command: 'deploy:list:functions', + options: {}, + start: 'listing function deployments', + success: 'listed function deployments', + }, + { + command: 'rollback:function', + options: { 'function': 'handler', 'function-version': '23' }, + start: 'rolling back function "handler"', + success: 'rolled back function "handler"', + }, + { + command: 'invoke', + options: { function: 'handler' }, + start: 'invoking function "handler"', + success: 'invoked function "handler"', + }, + { + command: 'invoke', + options: { f: 'handler' }, + start: 'invoking function "handler"', + success: 'invoked function "handler"', + }, + { + command: 'invoke:local', + options: { function: 'handler' }, + start: 'invoking function locally "handler"', + success: 'invoked function locally "handler"', + }, + ]; + + for (const testCase of cases) { + const spawnStub = sinon.stub().returns({ + on: (arg, cb) => { + if (arg === 'close') cb(0); + }, + kill: () => {}, + }); + const FrameworkComponent = proxyquire('../../../../components/framework/index.js', { + 'cross-spawn': spawnStub, + }); + + const context = await getContext(); + sinon.spy(context, 'startProgress'); + sinon.spy(context, 'successProgress'); + + const component = new FrameworkComponent('some-id', context, { path: 'path' }); + context.state.detectedFrameworkVersion = '9.9.9'; + + await component.command(testCase.command, testCase.options); + + expect(context.startProgress).to.have.been.calledOnceWithExactly(testCase.start); + expect(context.successProgress).to.have.been.calledOnceWithExactly(testCase.success); + expect(spawnStub.getCall(0).args[2].stdio).to.equal('inherit'); + } + }); + + it('does not show special progress for unknown passthrough commands', async () => { + const spawnStub = sinon.stub().returns({ + on: (arg, cb) => { + if (arg === 'close') cb(0); + }, + kill: () => {}, + }); + + const FrameworkComponent = proxyquire('../../../../components/framework/index.js', { + 'cross-spawn': spawnStub, + }); + + const context = await getContext(); + sinon.spy(context, 'startProgress'); + sinon.spy(context, 'successProgress'); + + const component = new FrameworkComponent('some-id', context, { path: 'path' }); + context.state.detectedFrameworkVersion = '9.9.9'; + + await component.command('print', {}); + + expect(context.startProgress.called).to.equal(false); + expect(context.successProgress.called).to.equal(false); + expect(spawnStub.getCall(0).args[1]).to.deep.equal(['print', '--stage', 'dev']); + expect(spawnStub.getCall(0).args[2].stdio).to.equal('inherit'); + }); + it('correctly ignores `stage` from options to not duplicate it when executing command', async () => { const spawnStub = sinon.stub().returns({ on: (arg, cb) => {