diff --git a/packages/webpack-cli/__tests__/CLI.test.js b/packages/webpack-cli/__tests__/CLI.test.js new file mode 100644 index 00000000000..cf64bbf55b0 --- /dev/null +++ b/packages/webpack-cli/__tests__/CLI.test.js @@ -0,0 +1,1228 @@ +const CLI = require('../lib/webpack-cli'); + +describe('CLI API', () => { + let cli; + + beforeEach(() => { + cli = new CLI(); + }); + + describe('makeCommand', () => { + it('should make command', async (done) => { + const command = await cli.makeCommand({ name: 'command' }, [], (program) => { + expect(program.opts()).toEqual({}); + + done(); + }); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with Boolean option by default', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean'], { from: 'user' }); + }); + + it('should make command with Boolean option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + type: Boolean, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean'], { from: 'user' }); + }); + + it('should make command with Boolean option and negative value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + type: Boolean, + description: 'description', + negative: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: false }); + + done(); + }, + ); + + command.parseAsync(['--no-boolean'], { from: 'user' }); + }); + + it('should make command with Boolean option and negative value #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + type: Boolean, + description: 'description', + negative: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: false }); + + done(); + }, + ); + + command.parseAsync(['--boolean', '--no-boolean'], { from: 'user' }); + }); + + it('should make command with Boolean option and negative value #3', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + type: Boolean, + description: 'description', + negative: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: true }); + + done(); + }, + ); + + command.parseAsync(['--no-boolean', '--boolean'], { from: 'user' }); + }); + + it('should make command with Boolean option with default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + type: Boolean, + description: 'description', + defaultValue: false, + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: false }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with String option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + type: String, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: 'bar' }); + + done(); + }, + ); + + command.parseAsync(['--string', 'bar'], { from: 'user' }); + }); + + it('should make command with String option with alias', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + alias: 's', + type: String, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: 'foo' }); + + done(); + }, + ); + + command.parseAsync(['-s', 'foo'], { from: 'user' }); + }); + + it('should make command with String option with default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + type: String, + description: 'description', + defaultValue: 'default-value', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: 'default-value' }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with String option with default value #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + type: String, + description: 'description', + defaultValue: 'default-value', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: 'foo' }); + + done(); + }, + ); + + command.parseAsync(['--string', 'foo'], { from: 'user' }); + }); + + it('should make command with String option using "=" syntax', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + type: String, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: 'bar' }); + + done(); + }, + ); + + command.parseAsync(['--string=bar'], { from: 'user' }); + }); + + it('should make command with multiple String option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + multiple: true, + type: String, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: ['foo', 'bar'] }); + + done(); + }, + ); + + command.parseAsync(['--string', 'foo', 'bar'], { from: 'user' }); + }); + + it('should make command with multiple String option with default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + multiple: true, + type: String, + description: 'description', + defaultValue: 'string', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: 'string' }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with multiple String option with default value #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + multiple: true, + type: String, + description: 'description', + defaultValue: 'string', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: ['foo', 'bar'] }); + + done(); + }, + ); + + command.parseAsync(['--string', 'foo', '--string', 'bar'], { from: 'user' }); + }); + + it('should make command with multiple String option #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'string', + multiple: true, + type: String, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ string: ['foo', 'bar'] }); + + done(); + }, + ); + + command.parseAsync(['--string', 'foo', '--string', 'bar'], { from: 'user' }); + }); + + it('should make command with Number option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'number', + type: Number, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ number: 12 }); + + done(); + }, + ); + + command.parseAsync(['--number', '12'], { from: 'user' }); + }); + + it('should make command with Number option with default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'number', + type: Number, + description: 'description', + defaultValue: 20, + }, + ], + (program) => { + expect(program.opts()).toEqual({ number: 20 }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with multiple Number option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'number', + multiple: true, + type: Number, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ number: [1, 2] }); + + done(); + }, + ); + + command.parseAsync(['--number', '1', '--number', '2'], { from: 'user' }); + }); + + it('should make command with multiple Number option and default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'number', + multiple: true, + type: Number, + description: 'description', + defaultValue: 50, + }, + ], + (program) => { + expect(program.opts()).toEqual({ number: [1, 2] }); + + done(); + }, + ); + + command.parseAsync(['--number', '1', '--number', '2'], { from: 'user' }); + }); + + it('should make command with multiple Number option and default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'number', + multiple: true, + type: Number, + description: 'description', + defaultValue: 50, + }, + ], + (program) => { + expect(program.opts()).toEqual({ number: 50 }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with custom function type', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'custom', + type: () => { + return 'function'; + }, + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ custom: 'function' }); + + done(); + }, + ); + + command.parseAsync(['--custom', 'value'], { from: 'user' }); + }); + + it('should make command with custom function type and default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'custom', + type: () => { + return 'function'; + }, + description: 'description', + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ custom: 'default' }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with multiple custom function type', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'custom', + type: (value, previous = []) => { + return previous.concat([value]); + }, + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ custom: ['value', 'other'] }); + + done(); + }, + ); + + command.parseAsync(['--custom', 'value', '--custom', 'other'], { from: 'user' }); + }); + + it('should make command with multiple custom function type and default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'custom', + type: (value, previous = []) => { + return previous.concat([value]); + }, + description: 'description', + multiple: true, + defaultValue: 50, + }, + ], + (program) => { + expect(program.opts()).toEqual({ custom: 50 }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with multiple custom function type and default value #2', async (done) => { + let skipDefault = true; + + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'custom', + type: (value, previous = []) => { + if (skipDefault) { + previous = []; + skipDefault = false; + } + + return [].concat(previous).concat([value]); + }, + description: 'description', + multiple: true, + defaultValue: 50, + }, + ], + (program) => { + expect(program.opts()).toEqual({ custom: ['foo'] }); + + done(); + }, + ); + + command.parseAsync(['--custom', 'foo'], { from: 'user' }); + }); + + it('should make command with Boolean and String option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-string'], { from: 'user' }); + }); + + it('should make command with Boolean and String option #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: 'value' }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-string', 'value'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and String option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-string'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and String option #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: ['bar', 'baz'] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-string', 'bar', '--boolean-and-string', 'baz'], { from: 'user' }); + }); + + it('should make command with Boolean and String option and negative', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + negative: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-string'], { from: 'user' }); + }); + + it('should make command with Boolean and String option and negative #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + negative: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: 'foo' }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-string', 'foo'], { from: 'user' }); + }); + + it('should make command with Boolean and String option and negative #3', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-string', + type: [Boolean, String], + description: 'description', + negative: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndString: false }); + + done(); + }, + ); + + command.parseAsync(['--no-boolean-and-string'], { from: 'user' }); + }); + + it('should make command with Boolean and Number option', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number', + type: [Boolean, Number], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumber: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number'], { from: 'user' }); + }); + + it('should make command with Boolean and Number option #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number', + type: [Boolean, Number], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumber: 12 }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number', '12'], { from: 'user' }); + }); + + it('should make command with array Boolean type', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean', + type: [Boolean], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ boolean: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean'], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string'], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 12 }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', '12'], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type #3', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 'bar' }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'bar'], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type and default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 'default' }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type and default value #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 'foo' }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'foo'], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type and default value #3', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 12 }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', '12'], { from: 'user' }); + }); + + it('should make command with Boolean and Number and String type and default value #4', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 'default' }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String type', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: true }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String type #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: ['foo'] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'foo'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String type #3', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: [12] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', '12'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String type #4', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: ['foo', 'bar'] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'foo', '--boolean-and-number-and-string', 'bar'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String type #5', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: ['foo', 12] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'foo', '--boolean-and-number-and-string', '12'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String and default value', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: 'default' }); + + done(); + }, + ); + + command.parseAsync([], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String and default value #2', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: ['foo'] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'foo'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String and default value #3', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: [12] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', '12'], { from: 'user' }); + }); + + it('should make command with multiple Boolean and Number and String and default value #4', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'boolean-and-number-and-string', + type: [Boolean, Number, String], + description: 'description', + multiple: true, + defaultValue: 'default', + }, + ], + (program) => { + expect(program.opts()).toEqual({ booleanAndNumberAndString: ['foo', 12] }); + + done(); + }, + ); + + command.parseAsync(['--boolean-and-number-and-string', 'foo', '--boolean-and-number-and-string', '12'], { from: 'user' }); + }); + + it('should make command with array of unknown types', async (done) => { + const command = await cli.makeCommand( + { + name: 'command', + }, + [ + { + name: 'unknown', + type: [Boolean, Symbol], + description: 'description', + }, + ], + (program) => { + expect(program.opts()).toEqual({ unknown: 'foo' }); + + done(); + }, + ); + + command.parseAsync(['--unknown', 'foo'], { from: 'user' }); + }); + }); +}); diff --git a/packages/webpack-cli/__tests__/arg-parser.test.js b/packages/webpack-cli/__tests__/arg-parser.test.js deleted file mode 100644 index 55956e85d88..00000000000 --- a/packages/webpack-cli/__tests__/arg-parser.test.js +++ /dev/null @@ -1,472 +0,0 @@ -const warnMock = jest.fn(); -const rawMock = jest.fn(); -jest.mock('../lib/utils/logger', () => { - return { - warn: warnMock, - raw: rawMock, - }; -}); - -const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {}); -const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - -const argParser = () => {}; -const { flags } = require('../lib/utils/cli-flags'); - -const basicOptions = [ - { - name: 'bool-flag', - alias: 'b', - usage: '--bool-flag', - type: Boolean, - description: 'boolean flag', - }, - { - name: 'num-flag', - usage: '--num-flag ', - type: Number, - description: 'number flag', - }, - { - name: 'neg-flag', - alias: 'n', - usage: '--neg-flag', - type: Boolean, - negative: true, - description: 'boolean flag', - }, - { - name: 'string-flag', - alias: 's', - usage: '--string-flag ', - type: String, - description: 'string flag', - }, - { - name: 'string-flag-with-default', - usage: '--string-flag-with-default ', - type: String, - description: 'string flag', - defaultValue: 'default-value', - }, - { - name: 'multi-type', - alias: 'm', - usage: '--multi-type | --multi-type ', - type: [String, Boolean], - negative: true, - description: 'flag with multiple types', - }, - { - name: 'multi-type-different-order', - usage: '--multi-type-different-order | --multi-type-different-order ', - // duplicates and a different ordering should be handled correctly - type: [Boolean, String, Boolean], - description: 'flag with multiple types in different order', - }, - { - name: 'multi-type-empty', - usage: '--multi-type-empty', - // should default to Boolean type - type: [], - description: 'flag with empty multi type array', - }, - { - name: 'multi-type-number', - usage: '--multi-type-number', - // should use only Number type (the first in the array), - // because Number and Boolean together are not supported - type: [Number, Boolean], - description: 'flag with number multi type', - }, - { - name: 'custom-type-flag', - usage: '--custom-type-flag ', - type: (val) => { - return val.split(','); - }, - description: 'custom type flag', - }, - { - name: 'multi-flag', - usage: '--multi-flag ', - type: String, - multiple: true, - description: 'multi flag', - }, - { - name: 'processor-flag', - usage: '--processor-flag', - type: Boolean, - description: 'flag with processor', - processor(opts) { - opts.processed = opts.processorFlag; - delete opts.processorFlag; - }, - }, - { - name: 'env', - usage: '--env', - type: String, - multipleType: true, - description: 'Environment passed to the configuration when it is a function', - }, -]; - -const helpAndVersionOptions = basicOptions.slice(0); -helpAndVersionOptions.push( - { - name: 'help', - usage: '--help', - type: Boolean, - description: 'help', - }, - { - name: 'version', - alias: 'v', - usage: '--version', - type: Boolean, - description: 'version', - }, -); - -describe.skip('arg-parser', () => { - beforeEach(() => { - warnMock.mockClear(); - processExitSpy.mockClear(); - consoleErrorSpy.mockClear(); - }); - - it('parses no flags', () => { - const res = argParser(basicOptions, [], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses basic flags', () => { - const res = argParser(basicOptions, ['--bool-flag', '--string-flag', 'val', '--num-flag', '100'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - boolFlag: true, - numFlag: 100, - stringFlag: 'val', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses number flags', () => { - const res = argParser(basicOptions, ['--num-flag', '100'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - numFlag: 100, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses number flags with = sign', () => { - const res = argParser(basicOptions, ['--num-flag=10'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - numFlag: 10, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('should not parse negated boolean flags which are not specified', () => { - const res = argParser(basicOptions, ['--no-bool-flag'], true); - expect(res.unknownArgs.includes('--no-bool-flag')).toBeTruthy(); - }); - - it('parses boolean flag alias', () => { - const res = argParser(basicOptions, ['-b'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - boolFlag: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses string flag alias', () => { - const res = argParser(basicOptions, ['-s', 'string-value'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - stringFlag: 'string-value', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag as Boolean', () => { - const res = argParser(basicOptions, ['--multi-type'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiType: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag as String', () => { - const res = argParser(basicOptions, ['--multi-type', 'value'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiType: 'value', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag alias as Boolean', () => { - const res = argParser(basicOptions, ['-m'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiType: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag alias as String', () => { - const res = argParser(basicOptions, ['-m', 'value'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiType: 'value', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses negated multi type flag as Boolean', () => { - const res = argParser(basicOptions, ['--no-multi-type'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiType: false, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag with different ordering as Boolean', () => { - const res = argParser(basicOptions, ['--multi-type-different-order'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiTypeDifferentOrder: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag with different ordering as String', () => { - const res = argParser(basicOptions, ['--multi-type-different-order', 'value'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiTypeDifferentOrder: 'value', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses empty multi type flag array as Boolean', () => { - const res = argParser(basicOptions, ['--multi-type-empty'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiTypeEmpty: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multi type flag (Number, Boolean) as Number', () => { - const res = argParser(basicOptions, ['--multi-type-number', '1.1'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiTypeNumber: 1.1, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parsing multi type flag (Number, Boolean) as Boolean fails', () => { - // this should fail because a multi type of Number and Boolean is - // not supported - argParser(basicOptions, ['--multi-type-number'], true); - expect(warnMock.mock.calls.length).toEqual(0); - - // by default, commander handles the error with a process.exit(1) - // along with an error message - expect(processExitSpy.mock.calls.length).toEqual(1); - expect(processExitSpy.mock.calls[0]).toEqual([1]); - - expect(consoleErrorSpy.mock.calls.length).toEqual(1); - expect(consoleErrorSpy.mock.calls[0][0]).toContain("option '--multi-type-number ' argument missing"); - }); - - it('warns on usage of both flag alias and same negated flag, setting it to true', () => { - const res = argParser(basicOptions, ['--no-neg-flag', '-n'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - negFlag: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(1); - expect(warnMock.mock.calls[0][0]).toContain('You provided both -n and --no-neg-flag'); - }); - - it('parses string flag using equals sign', () => { - const res = argParser(basicOptions, ['--string-flag=val'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - stringFlag: 'val', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('handles multiple same args', () => { - const res = argParser(basicOptions, ['--multi-flag', 'a.js', '--multi-flag', 'b.js'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - multiFlag: ['a.js', 'b.js'], - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('handles additional node args from argv', () => { - const res = argParser(basicOptions, ['node', 'index.js', '--bool-flag', '--string-flag', 'val'], false); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - boolFlag: true, - stringFlag: 'val', - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('handles unknown args', () => { - const res = argParser(basicOptions, ['--unknown-arg', '-b', 'no-leading-dashes'], true); - expect(res.unknownArgs).toEqual(['--unknown-arg', 'no-leading-dashes']); - expect(res.opts).toEqual({ - boolFlag: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('handles custom type args', () => { - const res = argParser(basicOptions, ['--custom-type-flag', 'val1,val2,val3'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - customTypeFlag: ['val1', 'val2', 'val3'], - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses webpack args', () => { - const res = argParser(flags, ['--entry', 'test.js', '--hot', '-o', './dist/', '--stats'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts.entry).toEqual(['test.js']); - expect(res.opts.hot).toBeTruthy(); - expect(res.opts.outputPath).toEqual('./dist/'); - expect(res.opts.stats).toEqual(true); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses --neg-flag', () => { - const res = argParser(basicOptions, ['--neg-flag'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - negFlag: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses --no-neg-flag', () => { - const res = argParser(basicOptions, ['--no-neg-flag'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - negFlag: false, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('warns on usage of both --neg and --no-neg flag, setting it to false', () => { - const res = argParser(basicOptions, ['--neg-flag', '--no-neg-flag'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - negFlag: false, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(1); - expect(warnMock.mock.calls[0][0]).toContain('You provided both --neg-flag and --no-neg-flag'); - }); - - it('warns on usage of both flag and same negated flag, setting it to true', () => { - const res = argParser(basicOptions, ['--no-neg-flag', '--neg-flag'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts).toEqual({ - negFlag: true, - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(1); - expect(warnMock.mock.calls[0][0]).toContain('You provided both --neg-flag and --no-neg-flag'); - }); - - it('handles unknown flag', () => { - const res = argParser(basicOptions, ['--unknown-flag'], true); - expect(res.unknownArgs).toEqual(['--unknown-flag']); - expect(res.opts).toEqual({ - stringFlagWithDefault: 'default-value', - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses multiType flag', () => { - const res = argParser(basicOptions, ['--env', 'production', '--env', 'platform=staging'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts.env).toEqual({ - platform: 'staging', - production: true, - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses nested multiType flag', () => { - const res = argParser(basicOptions, ['--env', 'a.b=d', '--env', 'a.b.c=d'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts.env).toEqual({ - a: { - b: { - c: 'd', - }, - }, - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); - - it('parses nested multiType flag with diff keys', () => { - const res = argParser(basicOptions, ['--env', 'a.b=c', '--env', 'd.e.f=g'], true); - expect(res.unknownArgs.length).toEqual(0); - expect(res.opts.env).toEqual({ - a: { - b: 'c', - }, - d: { - e: { - f: 'g', - }, - }, - }); - expect(warnMock.mock.calls.length).toEqual(0); - }); -}); diff --git a/packages/webpack-cli/lib/utils/cli-flags.js b/packages/webpack-cli/lib/utils/cli-flags.js index 49272981835..b24410f2c84 100644 --- a/packages/webpack-cli/lib/utils/cli-flags.js +++ b/packages/webpack-cli/lib/utils/cli-flags.js @@ -47,8 +47,32 @@ const builtInFlags = [ { name: 'env', usage: '--env | --env --env ', - type: String, - multipleType: true, + type: (value, previous = {}) => { + // This ensures we're only splitting by the first `=` + const [allKeys, val] = value.split(/=(.+)/, 2); + const splitKeys = allKeys.split(/\.(?!$)/); + + let prevRef = previous; + + splitKeys.forEach((someKey, index) => { + if (!prevRef[someKey]) { + prevRef[someKey] = {}; + } + + if (typeof prevRef[someKey] === 'string') { + prevRef[someKey] = {}; + } + + if (index === splitKeys.length - 1) { + prevRef[someKey] = val || true; + } + + prevRef = prevRef[someKey]; + }); + + return previous; + }, + multiple: true, description: 'Environment passed to the configuration when it is a function.', }, diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index 67201852276..c3b91fc554e 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -109,96 +109,102 @@ class WebpackCLI { return command; } - // TODO refactor this terrible stuff makeOption(command, option) { - let optionType = option.type; - let isStringOrBool = false; - - if (Array.isArray(optionType)) { - // filter out duplicate types - optionType = optionType.filter((type, index) => { - return optionType.indexOf(type) === index; - }); - - // the only multi type currently supported is String and Boolean, - // if there is a case where a different multi type is needed it - // must be added here - if (optionType.length === 0) { - // if no type is provided in the array fall back to Boolean - optionType = Boolean; - } else if (optionType.length === 1 || optionType.length > 2) { - // treat arrays with 1 or > 2 args as a single type - optionType = optionType[0]; + let type = option.type; + let isMultipleTypes = Array.isArray(type); + let isOptional = false; + + if (isMultipleTypes) { + if (type.length === 1) { + type = type[0]; + isMultipleTypes = false; } else { - // only String and Boolean multi type is supported - if (optionType.includes(Boolean) && optionType.includes(String)) { - isStringOrBool = true; - } else { - optionType = optionType[0]; - } + isOptional = type.includes(Boolean); } } - const flags = option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`; + const isMultiple = option.multiple; + const isRequired = type !== Boolean && typeof type !== 'undefined'; - let flagsWithType = flags; + let flags = option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`; - if (isStringOrBool) { - // commander recognizes [value] as an optional placeholder, - // making this flag work either as a string or a boolean - flagsWithType = `${flags} [value]`; - } else if (optionType !== Boolean) { + if (isOptional) { + // `commander.js` recognizes [value] as an optional placeholder, making this flag work either as a string or a boolean + flags = `${flags} [value${isMultiple ? '...' : ''}]`; + } else if (isRequired) { // is a required placeholder for any non-Boolean types - flagsWithType = `${flags} `; + flags = `${flags} `; } - if (isStringOrBool || optionType === Boolean || optionType === String) { - if (option.multiple) { - // a multiple argument parsing function - const multiArg = (value, previous = []) => previous.concat([value]); - - command.option(flagsWithType, option.description, multiArg, option.defaultValue); - } else if (option.multipleType) { - // for options which accept multiple types like env - // so you can do `--env platform=staging --env production` - // { platform: "staging", production: true } - const multiArg = (value, previous = {}) => { - // this ensures we're only splitting by the first `=` - const [allKeys, val] = value.split(/=(.+)/, 2); - const splitKeys = allKeys.split(/\.(?!$)/); - - let prevRef = previous; - - splitKeys.forEach((someKey, index) => { - if (!prevRef[someKey]) { - prevRef[someKey] = {}; - } + // TODO need to fix on webpack-dev-server side + // `describe` used by `webpack-dev-server` + const description = option.description || option.describe || ''; + const defaultValue = option.defaultValue; + + if (type === Boolean) { + command.option(flags, description, defaultValue); + } else if (type === Number) { + let skipDefault = true; + + command.option( + flags, + description, + (value, prev = []) => { + if (defaultValue && isMultiple && skipDefault) { + prev = []; + skipDefault = false; + } - if (typeof prevRef[someKey] === 'string') { - prevRef[someKey] = {}; - } + return isMultiple ? [].concat(prev).concat(Number(value)) : Number(value); + }, + defaultValue, + ); + } else if (type === String) { + let skipDefault = true; + + command.option( + flags, + description, + (value, prev = []) => { + if (defaultValue && isMultiple && skipDefault) { + prev = []; + skipDefault = false; + } - if (index === splitKeys.length - 1) { - prevRef[someKey] = val || true; - } + return isMultiple ? [].concat(prev).concat(value) : value; + }, + defaultValue, + ); + } else if (isMultipleTypes) { + let skipDefault = true; + + command.option( + flags, + description, + (value, prev = []) => { + if (defaultValue && isMultiple && skipDefault) { + prev = []; + skipDefault = false; + } - prevRef = prevRef[someKey]; - }); + if (type.includes(Number)) { + const numberValue = Number(value); - return previous; - }; + if (!isNaN(numberValue)) { + return isMultiple ? [].concat(prev).concat(numberValue) : numberValue; + } + } - command.option(flagsWithType, option.description, multiArg, option.defaultValue); - } else { - // Prevent default behavior for standalone options - command.option(flagsWithType, option.description, option.defaultValue); - } - } else if (optionType === Number) { - // this will parse the flag as a number - command.option(flagsWithType, option.description, Number, option.defaultValue); + if (type.includes(String)) { + return isMultiple ? [].concat(prev).concat(value) : value; + } + + return value; + }, + defaultValue, + ); } else { - // in this case the type is a parsing function - command.option(flagsWithType, option.description, optionType, option.defaultValue); + command.option(flags, description, type, defaultValue); } if (option.negative) { diff --git a/test/config/type/function-with-env/function-with-env.test.js b/test/config/type/function-with-env/function-with-env.test.js index d498a5d582a..c3a0275e460 100644 --- a/test/config/type/function-with-env/function-with-env.test.js +++ b/test/config/type/function-with-env/function-with-env.test.js @@ -8,7 +8,7 @@ describe('function configuration', () => { const { exitCode, stderr, stdout } = run(__dirname, ['--env'], false); expect(exitCode).toBe(2); - expect(stderr).toContain(`option '--env ' argument missing`); + expect(stderr).toContain(`option '--env ' argument missing`); expect(stdout).toBeFalsy(); });