diff --git a/MIGRATE.md b/MIGRATE.md new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/MIGRATE.md @@ -0,0 +1 @@ + diff --git a/README.md b/README.md index cfe291bcb49..7b93947cf2c 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,22 @@ # Webpack CLI -This project intends to be the main cli package of webpack. Here we will encapsulate all the features and code related to the command line. From sending options to the compiler, to initialize and migrate from version to version. +Webpack CLI encapsulates all code related to CLI handling. It captures options and sends them to webpack compiler. You can also find functionality for initializing a project and migrating between versions. For the time being, it is backwards-compatible with the CLI included in webpack itself. **Note** The package is still in work in progress. In case you want to contribute, reach to us, so we can point you out how and when you can help us. +## Migration from webpack v1 to v2 -# Roadmap to the first release +The `migrate` feature eases the transition from [version 1](http://webpack.github.io/docs/) to [version 2](https://gist.github.com/sokra/27b24881210b56bbaff7). `migrate` also allows users to switch to the new version of webpack without having to extensively [refactor](https://webpack.js.org/guides/migrating/). -- Migrate to webpack-cli all the current cli options available in webpack -- Create a `webpack-cli init` command that serves to set a configuration of webpack to the user -- create a `webpack-cli migrate` command that serves to migrate an existing configuration from webpack 1 to webpack 2. +`webpack-cli --migrate ` + +[Read more about migrating](MIGRATE.md) + +## Creating new webpack projects + +The `init` feature allows users to get started with webpack, fast. Through scaffolding, people can create their own configuration in order to faster initialize new projects for various of use cases. + +`webpack-cli --init [webpack-addons-]` + +[Read more about scaffolding](SCAFFOLDING.md) diff --git a/SCAFFOLDING.md b/SCAFFOLDING.md new file mode 100644 index 00000000000..f5d06e1734e --- /dev/null +++ b/SCAFFOLDING.md @@ -0,0 +1,83 @@ +# Introduction + +Setting up webpack for the first time is hard. Writing advanced configurations to optimize performance is even harder. The `init` feature is designed to support people that want to create their own configuration or initializing other projects people create. + +Through [yeoman](http://yeoman.io/), the `webpack-cli --init` feature allows people to create scaffolds and generate new projects quickly. An npm dependency that scaffolds a `webpack.config.js` through `webpack-cli` is what we refer to as an **addon**. + +## Writing a good scaffold + +Before writing a `webpack-cli` scaffold, think about what you're trying to achieve. Do you want a "general" scaffold that could be used by any project or type of app? Do you want something very focused - like a scaffold that writes both your `webpack.config.js` and your framework code? It's also useful to think about the user experience for your scaffold. + +`webpack-cli` offers an experience that is interactive and you can prompt users for questions (like, "What is your entry point?") to help customize the output accordingly. + +## webpack-addons + +[`webpack-addons`](https://github.com/webpack-contrib/webpack-addons) is a utility suite for creating addons. It contains functions that could be of use for creating an addon yourself. + +## webpack-addons-yourpackage + +In order for `webpack-cli` to compile your package, it relies on a prefix of `webpack-addons`. The package must also be published on npm. If you are curious about how you can create your very own `addon`, please read [How do I compose a webpack-addon?](https://github.com/ev1stensberg/webpack-addons-demo). + +## API + +To create an `addon`, you must create a [`yeoman-generator`](http://yeoman.io/authoring/). Because of that, you can optionally extend your generator to include methods from the [Yeoman API](http://yeoman.io/learning/). Its worth noting that we support all the properties of a regular webpack configuration. In order for us to do this, there's a thing you need to remember. + +Objects are made using strings, while strings are made using double strings. This means that in order for you to create an string, you have to wrap it inside another string for us to validate it correctly. + + +### `opts.env.configuration`(Required) + +Initialized inside the constructor of your generator in order for the CLI to work. + +```js +constructor(args, opts) { + super(args, opts); + opts.env.configuration = {}; + } +``` +### `opts.env.configuration.myObj` (required) + +`myObj` is your scaffold. This is where you will add options for the CLI to transform into a configuration. You can name it anything, and you can also add more objects, that could represent a `dev.config` or `prod.config`. + +```js +constructor(args, opts) { + super(args, opts); + opts.env.configuration = { + dev: {}, + prod: {} + }; + } +``` + +### `myObj.webpackOptions` (required) + +As with a regular webpack configuration, this property behaves the same. Inside `webpackOptions` you can declare the properties you want to scaffold. You can for instance, scaffold `entry`, `output` and `context`. + +(Inside a yeoman method) +```js +this.options.env.configuration.dev.webpackOptions = { +entry: '\'app.js\'', +output: {....}, +merge: 'myConfig' +}; +``` +If you want to use `webpack-merge`, you can supply `webpackOptions` with the merge property, and the configuration you want to merge it with. + +### `myObj.topScope`(optional) + +The `topScope` property is a way for the authors to add special behaviours, like functions that could be called inside a configuration, or variable initializations and module imports. + +```js +this.options.env.configuration.dev.topScope = [ +'var webpack = require(\'webpack\');' +'var path = require(\'path\');' +]; +``` + +### `myObj.configName`(optional) + +If you want to name your `webpack.config.js` something special, you can do that. + +```js +this.options.env.configuration.dev.configName = 'base'; +``` diff --git a/bin/config-yargs.js b/bin/config-yargs.js index a9363f40659..e75d9b7b5f3 100644 --- a/bin/config-yargs.js +++ b/bin/config-yargs.js @@ -33,7 +33,7 @@ module.exports = function(yargs) { requiresArg: true }, 'env': { - describe: 'Enviroment passed to the config, when it is a function', + describe: 'Environment passed to the config, when it is a function', group: CONFIG_GROUP }, 'context': { @@ -149,7 +149,7 @@ module.exports = function(yargs) { }, 'target': { type: 'string', - describe: 'The targeted execution enviroment', + describe: 'The targeted execution environment', group: ADVANCED_GROUP, requiresArg: true }, @@ -166,12 +166,6 @@ module.exports = function(yargs) { describe: 'Watch the filesystem for changes', group: BASIC_GROUP }, - 'save': { - type: 'boolean', - alias: 's', - describe: 'Rebuilds on save regardless of changes in watch mode', - group: BASIC_GROUP - }, 'watch-stdin': { type: 'boolean', alias: 'stdin', @@ -185,7 +179,7 @@ module.exports = function(yargs) { }, 'watch-poll': { type: 'boolean', - describe: 'The polling intervall for watching (also enable polling)', + describe: 'The polling interval for watching (also enable polling)', group: ADVANCED_GROUP }, 'hot': { diff --git a/bin/convert-argv.js b/bin/convert-argv.js index e3e004f652b..cd16639191f 100644 --- a/bin/convert-argv.js +++ b/bin/convert-argv.js @@ -122,7 +122,7 @@ module.exports = function(yargs, argv, convertOptions) { function processConfiguredOptions(options) { if(options === null || typeof options !== 'object') { console.error('Config did not export an object or a function returning an object.'); - process.exit(-1); + process.exitCode = -1; } // process Promise @@ -224,16 +224,10 @@ module.exports = function(yargs, argv, convertOptions) { options[optionName || name] = false; }); } - //eslint-disable-next-line - function mapArgToPath(name, optionName) { - ifArg(name, function(str) { - options[optionName || name] = path.resolve(str); - }); - } function loadPlugin(name) { var loadUtils = require('loader-utils'); - var args = null; + var args; try { var p = name && name.indexOf('?'); if(p > -1) { @@ -242,7 +236,7 @@ module.exports = function(yargs, argv, convertOptions) { } } catch(e) { console.log('Invalid plugin arguments ' + name + ' (' + e + ').'); - process.exit(-1); + process.exitCode = -1; } var path; @@ -251,7 +245,7 @@ module.exports = function(yargs, argv, convertOptions) { path = resolve.sync(process.cwd(), name); } catch(e) { console.log('Cannot resolve plugin ' + name + '.'); - process.exit(-1); + process.exitCode = -1; } var Plugin; try { @@ -281,7 +275,11 @@ module.exports = function(yargs, argv, convertOptions) { } ifArgPair('entry', function(name, entry) { - options.entry[name] = entry; + if(typeof options.entry[name] !== 'undefined' && options.entry[name] !== null) { + options.entry[name] = [].concat(options.entry[name]).concat(entry); + } else { + options.entry[name] = entry; + } }, function() { ensureObject(options, 'entry'); }); @@ -322,7 +320,7 @@ module.exports = function(yargs, argv, convertOptions) { ifArg('output-path', function(value) { ensureObject(options, 'output'); - options.output.path = value; + options.output.path = path.resolve(value); }); ifArg('output-filename', function(value) { @@ -454,7 +452,7 @@ module.exports = function(yargs, argv, convertOptions) { ifArg('prefetch', function(request) { ensureArray(options, 'plugins'); - var PrefetchPlugin = require('webpack/PrefetchPlugin'); + var PrefetchPlugin = require('webpack/lib/PrefetchPlugin'); options.plugins.push(new PrefetchPlugin(request)); }); @@ -468,16 +466,10 @@ module.exports = function(yargs, argv, convertOptions) { } else { name = value; } - var ProvidePlugin = require('webpack/ProvidePlugin'); + var ProvidePlugin = require('webpack/lib/ProvidePlugin'); options.plugins.push(new ProvidePlugin(name, value)); }); - ifBooleanArg('labeled-modules', function() { - ensureArray(options, 'plugins'); - var LabeledModulesPlugin = require('webpack/lib/dependencies/LabeledModulesPlugin'); - options.plugins.push(new LabeledModulesPlugin()); - }); - ifArg('plugin', function(value) { ensureArray(options, 'plugins'); options.plugins.push(loadPlugin(value)); @@ -490,19 +482,20 @@ module.exports = function(yargs, argv, convertOptions) { if(noOutputFilenameDefined) { ensureObject(options, 'output'); if(convertOptions && convertOptions.outputFilename) { - options.output.path = path.dirname(convertOptions.outputFilename); + options.output.path = path.resolve(path.dirname(convertOptions.outputFilename)); options.output.filename = path.basename(convertOptions.outputFilename); } else if(argv._.length > 0) { options.output.filename = argv._.pop(); - options.output.path = path.dirname(options.output.filename); + options.output.path = path.resolve(path.dirname(options.output.filename)); options.output.filename = path.basename(options.output.filename); } else if(configFileLoaded) { throw new Error('\'output.filename\' is required, either in config file or as --output-filename'); - } else { + } + else { console.error('No configuration file found and no output filename configured via CLI option.'); console.error('A configuration file could be named \'webpack.config.js\' in the current directory.'); console.error('Use --help to display the CLI options.'); - process.exit(-1); + process.exitCode = -1; } } @@ -549,7 +542,7 @@ module.exports = function(yargs, argv, convertOptions) { console.error('A configuration file could be named \'webpack.config.js\' in the current directory.'); } console.error('Use --help to display the CLI options.'); - process.exit(-1); + process.exitCode = -1; } } }; diff --git a/bin/process-options.js b/bin/process-options.js index 764c560546d..e97bb5af4c6 100644 --- a/bin/process-options.js +++ b/bin/process-options.js @@ -139,7 +139,7 @@ module.exports = function processOptions(yargs, argv) { console.error('\u001b[1m\u001b[31m' + e.message + '\u001b[39m\u001b[22m'); else console.error(e.message); - process.exit(1); + process.exitCode = 1; } throw e; } @@ -160,7 +160,7 @@ module.exports = function processOptions(yargs, argv) { lastHash = null; console.error(err.stack || err); if(err.details) console.error(err.details); - process.exit(1); + process.exitCode = 1; } if(outputOptions.json) { process.stdout.write(JSON.stringify(stats.toJson(outputOptions), null, 2) + '\n'); @@ -172,7 +172,7 @@ module.exports = function processOptions(yargs, argv) { } if(!options.watch && stats.hasErrors()) { process.on('exit', function() { - process.exit(2); + process.exitCode = 2; }); } } @@ -181,7 +181,7 @@ module.exports = function processOptions(yargs, argv) { var watchOptions = primaryOptions.watchOptions || primaryOptions.watch || {}; if(watchOptions.stdin) { process.stdin.on('end', function() { - process.exit(0); + process.exitCode = 0; }); process.stdin.resume(); } diff --git a/bin/webpack.js b/bin/webpack.js index 1a5a67aac00..6cc71f70226 100755 --- a/bin/webpack.js +++ b/bin/webpack.js @@ -1,45 +1,32 @@ #!/usr/bin/env node - /* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ var path = require('path'); -// var fs = require('fs'); -// Local version replace global one +var resolveCwd = require('resolve-cwd'); -process.title = 'webpack'; +var localCLI = resolveCwd.silent('webpack-cli/bin/webpack'); -try { - var localWebpack = require.resolve(path.join(process.cwd(), 'node_modules', 'webpack-cli', 'bin', 'webpack.js')); - if(localWebpack && path.relative(localWebpack, __filename) !== '') { - return require(localWebpack); +if (localCLI && path.relative(localCLI, __filename) !== '') { + require(localCLI); +} else { + try { + require('./webpack'); + } catch (err) { + console.error(`\n ${err.message}`); + process.exitCode = 1; } -} catch(e) { - /* - // Delete Cache, Retry and leave it if the path isn`t valid. - // swap with webpack-cli later - var dirPath = path.join(process.cwd(), 'node_modules', 'webpack', 'bin'); - var retryLocal; - fs.readdir(dirPath, function(err, files) { - files.filter( (file) => { - delete require.cache[file]; - }); - - // TODO: If path is wrong, leave it to higher powers until we publish on npm - // Could also install and load and install through github - return require(path.join(dirPath, 'webpack.js')) - }); - */ } + var yargs = require('yargs') - .usage('webpack-cli ' + require('../package.json').version + '\n' + - 'Usage: https://webpack.github.io/docs/cli.html\n' + + .usage('webpack ' + require('../package.json').version + '\n' + + 'Usage: https://webpack.js.org/api/cli/\n' + 'Usage without config file: webpack [] \n' + 'Usage with config file: webpack'); - require('./config-yargs')(yargs); + var DISPLAY_GROUP = 'Stats options:'; var BASIC_GROUP = 'Basic options:'; @@ -93,6 +80,11 @@ yargs.options({ group: DISPLAY_GROUP, describe: 'Display even excluded modules in the output' }, + 'display-max-modules': { + type: 'number', + group: DISPLAY_GROUP, + describe: 'Sets the maximum number of visible modules in output' + }, 'display-chunks': { type: 'boolean', group: DISPLAY_GROUP, @@ -123,6 +115,11 @@ yargs.options({ group: DISPLAY_GROUP, describe: 'Display reasons about module inclusion in the output' }, + 'display-depth': { + type: 'boolean', + group: DISPLAY_GROUP, + describe: 'Display distance from entry point for each module' + }, 'display-used-exports': { type: 'boolean', group: DISPLAY_GROUP, @@ -149,6 +146,7 @@ var argv = yargs.argv; if(argv.verbose) { argv['display-reasons'] = true; + argv['display-depth'] = true; argv['display-entrypoints'] = true; argv['display-used-exports'] = true; argv['display-provided-exports'] = true; @@ -157,16 +155,224 @@ if(argv.verbose) { argv['display-cached'] = true; argv['display-cached-assets'] = true; } - if(argv.init) { - require('../lib/initialize')(argv._); + return require('../lib/initialize')(argv._); } else if(argv.migrate) { const filePaths = argv._; if (!filePaths.length) { throw new Error('Please specify a path to your webpack config'); } const inputConfigPath = path.resolve(process.cwd(), filePaths[0]); - require('../lib/migrate.js')(inputConfigPath, inputConfigPath); + return require('../lib/migrate.js')(inputConfigPath, inputConfigPath); } else { - require('./process-options')(yargs,argv); + var options = require('./convert-argv')(yargs, argv); + return processOptions(options); +} + +function ifArg(name, fn, init) { + if(Array.isArray(argv[name])) { + if(init) init(); + argv[name].forEach(fn); + } else if(typeof argv[name] !== 'undefined') { + if(init) init(); + fn(argv[name], -1); + } +} +//eslint-disable-next-line +function processOptions(options) { + // process Promise + if(typeof options.then === 'function') { + options.then(processOptions).catch(function(err) { + console.error(err.stack || err); + process.exitCode = -1; + }); + return; + } + + var firstOptions = [].concat(options)[0]; + var statsPresetToOptions = require('webpack/lib/Stats.js').presetToOptions; + + var outputOptions = options.stats; + if(typeof outputOptions === 'boolean' || typeof outputOptions === 'string') { + outputOptions = statsPresetToOptions(outputOptions); + } else if(!outputOptions) { + outputOptions = {}; + } + outputOptions = Object.create(outputOptions); + if(Array.isArray(options) && !outputOptions.children) { + outputOptions.children = options.map(o => o.stats); + } + if(typeof outputOptions.context === 'undefined') + outputOptions.context = firstOptions.context; + + ifArg('json', function(bool) { + if(bool) + outputOptions.json = bool; + }); + + if(typeof outputOptions.colors === 'undefined') + outputOptions.colors = require('supports-color'); + + ifArg('sort-modules-by', function(value) { + outputOptions.modulesSort = value; + }); + + ifArg('sort-chunks-by', function(value) { + outputOptions.chunksSort = value; + }); + + ifArg('sort-assets-by', function(value) { + outputOptions.assetsSort = value; + }); + + ifArg('display-exclude', function(value) { + outputOptions.exclude = value; + }); + + if(!outputOptions.json) { + if(typeof outputOptions.cached === 'undefined') + outputOptions.cached = false; + if(typeof outputOptions.cachedAssets === 'undefined') + outputOptions.cachedAssets = false; + + ifArg('display-chunks', function(bool) { + outputOptions.modules = !bool; + outputOptions.chunks = bool; + }); + + ifArg('display-entrypoints', function(bool) { + outputOptions.entrypoints = bool; + }); + + ifArg('display-reasons', function(bool) { + outputOptions.reasons = bool; + }); + + ifArg('display-depth', function(bool) { + outputOptions.depth = bool; + }); + + ifArg('display-used-exports', function(bool) { + outputOptions.usedExports = bool; + }); + + ifArg('display-provided-exports', function(bool) { + outputOptions.providedExports = bool; + }); + + ifArg('display-error-details', function(bool) { + outputOptions.errorDetails = bool; + }); + + ifArg('display-origins', function(bool) { + outputOptions.chunkOrigins = bool; + }); + + ifArg('display-max-modules', function(value) { + outputOptions.maxModules = value; + }); + + ifArg('display-cached', function(bool) { + if(bool) + outputOptions.cached = true; + }); + + ifArg('display-cached-assets', function(bool) { + if(bool) + outputOptions.cachedAssets = true; + }); + + if(!outputOptions.exclude) + outputOptions.exclude = ['node_modules', 'bower_components', 'components']; + + if(argv['display-modules']) { + outputOptions.maxModules = Infinity; + outputOptions.exclude = undefined; + } + } else { + if(typeof outputOptions.chunks === 'undefined') + outputOptions.chunks = true; + if(typeof outputOptions.entrypoints === 'undefined') + outputOptions.entrypoints = true; + if(typeof outputOptions.modules === 'undefined') + outputOptions.modules = true; + if(typeof outputOptions.chunkModules === 'undefined') + outputOptions.chunkModules = true; + if(typeof outputOptions.reasons === 'undefined') + outputOptions.reasons = true; + if(typeof outputOptions.cached === 'undefined') + outputOptions.cached = true; + if(typeof outputOptions.cachedAssets === 'undefined') + outputOptions.cachedAssets = true; + } + + ifArg('hide-modules', function(bool) { + if(bool) { + outputOptions.modules = false; + outputOptions.chunkModules = false; + } + }); + + var webpack = require('webpack/lib/webpack.js'); + + Error.stackTraceLimit = 30; + var lastHash = null; + var compiler; + try { + compiler = webpack(options); + } catch(e) { + var WebpackOptionsValidationError = require('webpack/lib/WebpackOptionsValidationError'); + if(e instanceof WebpackOptionsValidationError) { + if(argv.color) + console.error('\u001b[1m\u001b[31m' + e.message + '\u001b[39m\u001b[22m'); + else + console.error(e.message); + process.exitCode = 1; + } + throw e; + } + + if(argv.progress) { + var ProgressPlugin = require('webpack/lib/ProgressPlugin'); + compiler.apply(new ProgressPlugin({ + profile: argv.profile + })); + } + + function compilerCallback(err, stats) { + if(!options.watch || err) { + // Do not keep cache anymore + compiler.purgeInputFileSystem(); + } + if(err) { + lastHash = null; + console.error(err.stack || err); + if(err.details) console.error(err.details); + process.exitCode = 1; + } + if(outputOptions.json) { + process.stdout.write(JSON.stringify(stats.toJson(outputOptions), null, 2) + '\n'); + } else if(stats.hash !== lastHash) { + lastHash = stats.hash; + process.stdout.write(stats.toString(outputOptions) + '\n'); + } + if(!options.watch && stats.hasErrors()) { + process.on('exit', function() { + process.exitCode = 2; + }); + } + } + if(firstOptions.watch || options.watch) { + var watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {}; + if(watchOptions.stdin) { + process.stdin.on('end', function() { + process.exitCode = 0; + }); + process.stdin.resume(); + } + compiler.watch(watchOptions, compilerCallback); + console.log('\nWebpack is watching the files…\n'); + } else + compiler.run(compilerCallback); + } diff --git a/example/neo-webpack.config.js b/example/neo-webpack.config.js deleted file mode 100644 index 458a1fd7f48..00000000000 --- a/example/neo-webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -var path = require('path'); - - -module.exports = { - devtool: 'eval', - entry: [ - './src/index' - ], - output: { - path: path.join(__dirname, 'dist'), - filename: 'index.js' - }, - module: { - rules: [{ - test: /\.js$/, - use: ['babel'], - include: path.join(__dirname, 'src') - }] - } -}; diff --git a/example/webpack.config.js b/example/webpack.config.js deleted file mode 100644 index d0dd7933a45..00000000000 --- a/example/webpack.config.js +++ /dev/null @@ -1,20 +0,0 @@ -var path = require('path'); - - -module.exports = { - devtool: 'eval', - entry: [ - './src/index' - ], - output: { - path: path.join(__dirname, 'dist'), - filename: 'index.js' - }, - module: { - loaders: [{ - test: /\.js$/, - loaders: ['babel'], - include: path.join(__dirname, 'src') - }] - } -}; diff --git a/lib/creator/index.js b/lib/creator/index.js index 9778b08fbe5..12ec46c8fc8 100644 --- a/lib/creator/index.js +++ b/lib/creator/index.js @@ -1,4 +1,5 @@ const yeoman = require('yeoman-environment'); +const Generator = require('yeoman-generator'); const path = require('path'); const defaultGenerator = require('./yeoman/webpack-generator'); const WebpackAdapter = require('./yeoman/webpack-adapter'); @@ -15,18 +16,29 @@ const runTransform = require('./transformations/index'); */ function creator(options) { - const env = yeoman.createEnv(null, null, new WebpackAdapter()); - const generatorName = options ? replaceGeneratorName(path.basename(options)) : 'webpack-default-generator'; + let env = yeoman.createEnv('webpack', null, new WebpackAdapter()); + const generatorName = options ? replaceGeneratorName(path.basename(options[0])) : 'webpack-default-generator'; if(options) { - const generatorName = replaceGeneratorName(path.basename(options)); - env.register(require.resolve(options), generatorName); + const WebpackGenerator = class extends Generator { + initializing() { + options.forEach( (path) => { + return this.composeWith(require.resolve(path)); + }); + } + }; + env.registerStub(WebpackGenerator, generatorName); } else { env.registerStub(defaultGenerator, 'webpack-default-generator'); } - env.run(generatorName) - .on('end', () => { - return runTransform(env.getArgument('configuration')); + env.run(generatorName).on('end', () => { + if(generatorName !== 'webpack-default-generator') { + //HACK / FIXME + env = env.options.env; + return runTransform(env.configuration); + } else { + return runTransform(env.getArgument('configuration')); + } }); } @@ -42,7 +54,7 @@ function creator(options) { function replaceGeneratorName(name) { return name.replace( - /(webpack-addons)?([^:]+)(:.*)?/g, 'generator$2'); + /(webpack-addons)?([^:]+)(:.*)?/g, 'generator$2'); } module.exports = { diff --git a/lib/creator/transformations/context/__snapshots__/context.test.js.snap b/lib/creator/transformations/context/__snapshots__/context.test.js.snap new file mode 100644 index 00000000000..d707e07cda2 --- /dev/null +++ b/lib/creator/transformations/context/__snapshots__/context.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`context transforms correctly using "context-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + context: path.resolve(__dirname, \\"app\\") +} +" +`; + +exports[`context transforms correctly using "context-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + context: './some/fake/path' +} +" +`; + +exports[`context transforms correctly using "context-2" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + context: contextVariable +} +" +`; diff --git a/lib/creator/transformations/context/__testfixtures__/context-0.input.js b/lib/creator/transformations/context/__testfixtures__/context-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/context/__testfixtures__/context-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/context/__testfixtures__/context-1.input.js b/lib/creator/transformations/context/__testfixtures__/context-1.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/context/__testfixtures__/context-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/context/__testfixtures__/context-2.input.js b/lib/creator/transformations/context/__testfixtures__/context-2.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/context/__testfixtures__/context-2.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/context/context.js b/lib/creator/transformations/context/context.js new file mode 100644 index 00000000000..98fb0710b99 --- /dev/null +++ b/lib/creator/transformations/context/context.js @@ -0,0 +1,22 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for context. Finds the context property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'context', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/context/context.test.js b/lib/creator/transformations/context/context.test.js new file mode 100644 index 00000000000..0258c5528de --- /dev/null +++ b/lib/creator/transformations/context/context.test.js @@ -0,0 +1,5 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'context', 'context-0', 'path.resolve(__dirname, "app")'); +defineTest(__dirname, 'context', 'context-1', '\'./some/fake/path\''); +defineTest(__dirname, 'context', 'context-2', 'contextVariable'); diff --git a/lib/creator/transformations/devtool/__snapshots__/devtool.test.js.snap b/lib/creator/transformations/devtool/__snapshots__/devtool.test.js.snap new file mode 100644 index 00000000000..d343b1fc107 --- /dev/null +++ b/lib/creator/transformations/devtool/__snapshots__/devtool.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`devtool transforms correctly using "devtool-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + devtool: 'source-map' +} +" +`; + +exports[`devtool transforms correctly using "devtool-0" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + devtool: myVariable +} +" +`; + +exports[`devtool transforms correctly using "devtool-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + devtool: 'cheap-module-source-map' +} +" +`; + +exports[`devtool transforms correctly using "devtool-1" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + devtool: false +} +" +`; diff --git a/lib/creator/transformations/devtool/__testfixtures__/devtool-0.input.js b/lib/creator/transformations/devtool/__testfixtures__/devtool-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/devtool/__testfixtures__/devtool-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/devtool/__testfixtures__/devtool-1.input.js b/lib/creator/transformations/devtool/__testfixtures__/devtool-1.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/devtool/__testfixtures__/devtool-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/devtool/devtool.js b/lib/creator/transformations/devtool/devtool.js new file mode 100644 index 00000000000..ac3930ad366 --- /dev/null +++ b/lib/creator/transformations/devtool/devtool.js @@ -0,0 +1,23 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for devtool. Finds the devtool property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'devtool', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/devtool/devtool.test.js b/lib/creator/transformations/devtool/devtool.test.js new file mode 100644 index 00000000000..01f574301a5 --- /dev/null +++ b/lib/creator/transformations/devtool/devtool.test.js @@ -0,0 +1,6 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'devtool', 'devtool-0', '\'source-map\''); +defineTest(__dirname, 'devtool', 'devtool-0', 'myVariable'); +defineTest(__dirname, 'devtool', 'devtool-1', '\'cheap-module-source-map\''); +defineTest(__dirname, 'devtool', 'devtool-1', 'false'); diff --git a/lib/creator/transformations/entry/__snapshots__/entry.test.js.snap b/lib/creator/transformations/entry/__snapshots__/entry.test.js.snap new file mode 100644 index 00000000000..1fb3d3acd85 --- /dev/null +++ b/lib/creator/transformations/entry/__snapshots__/entry.test.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`entry transforms correctly using "entry-0" data 1`] = ` +"module.exports = { + entry: 'index.js' +} +" +`; + +exports[`entry transforms correctly using "entry-0" data 2`] = ` +"module.exports = { + entry: ['index.js', 'app.js'] +} +" +`; + +exports[`entry transforms correctly using "entry-0" data 3`] = ` +"module.exports = { + entry: { + index: 'index.js', + app: 'app.js' + } +} +" +`; + +exports[`entry transforms correctly using "entry-0" data 4`] = ` +"module.exports = { + entry: { + something, + app: 'app.js', + else + } +} +" +`; + +exports[`entry transforms correctly using "entry-0" data 5`] = ` +"module.exports = { + entry: () => 'index.js' +} +" +`; + +exports[`entry transforms correctly using "entry-0" data 6`] = ` +"module.exports = { + entry: () => new Promise((resolve) => resolve(['./app', './router'])) +} +" +`; + +exports[`entry transforms correctly using "entry-0" data 7`] = ` +"module.exports = { + entry: entryStringVariable +} +" +`; diff --git a/lib/creator/transformations/entry/__testfixtures__/entry-0.input.js b/lib/creator/transformations/entry/__testfixtures__/entry-0.input.js new file mode 100644 index 00000000000..4ba52ba2c8d --- /dev/null +++ b/lib/creator/transformations/entry/__testfixtures__/entry-0.input.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/lib/creator/transformations/entry/entry.js b/lib/creator/transformations/entry/entry.js new file mode 100644 index 00000000000..5f142907561 --- /dev/null +++ b/lib/creator/transformations/entry/entry.js @@ -0,0 +1,40 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for entry. Finds the entry property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + + +module.exports = function(j, ast, webpackProperties) { + + function createEntryProperty(p) { + + if(typeof(webpackProperties) === 'string') { + return utils.pushCreateProperty(j, p, 'entry', webpackProperties); + } + if(Array.isArray(webpackProperties)) { + const externalArray = utils.createArrayWithChildren( + j, 'entry', webpackProperties, true + ); + return p.value.properties.push(externalArray); + } + else { + utils.pushCreateProperty(j, p, 'entry', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'entry'); + } + } + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createEntryProperty)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/entry/entry.test.js b/lib/creator/transformations/entry/entry.test.js new file mode 100644 index 00000000000..27047eb84ee --- /dev/null +++ b/lib/creator/transformations/entry/entry.test.js @@ -0,0 +1,17 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'entry', 'entry-0', '\'index.js\''); +defineTest(__dirname, 'entry', 'entry-0', ['\'index.js\'', '\'app.js\'']); +defineTest(__dirname, 'entry', 'entry-0', { + index: '\'index.js\'', + app: '\'app.js\'' +}); + +defineTest(__dirname, 'entry', 'entry-0', { + inject: 'something', + app: '\'app.js\'', + inject_1: 'else' +}); +defineTest(__dirname, 'entry', 'entry-0', '() => \'index.js\''); +defineTest(__dirname, 'entry', 'entry-0', '() => new Promise((resolve) => resolve([\'./app\', \'./router\']))'); +defineTest(__dirname, 'entry', 'entry-0', 'entryStringVariable'); diff --git a/lib/creator/transformations/externals/__snapshots__/externals.test.js.snap b/lib/creator/transformations/externals/__snapshots__/externals.test.js.snap new file mode 100644 index 00000000000..1a77ae06b34 --- /dev/null +++ b/lib/creator/transformations/externals/__snapshots__/externals.test.js.snap @@ -0,0 +1,137 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`externals transforms correctly using "externals-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: /react/ +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: { + jquery: 'jQuery', + react: 'react' + } +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: myObj +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 3`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: { + jquery: 'jQuery', + react: reactObj + } +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 4`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: { + jquery: 'jQuery', + react: [reactObj, path.join(__dirname, 'app'), 'jquery'] + } +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 5`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: { + lodash: { + commonjs: 'lodash', + amd: 'lodash', + root: '_' + } + } +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 6`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: { + lodash: { + commonjs: lodash, + amd: hidash, + root: _ + } + } +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 7`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: [{ + a: false, + b: true, + './ext': ./hey + }, function(context, request, callback) {if (/^yourregex$/.test(request)){return callback(null, 'commonjs ' + request);}callback();}] +} +" +`; + +exports[`externals transforms correctly using "externals-1" data 8`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + externals: [ + myObj, + function(context, request, callback) {if (/^yourregex$/.test(request)){return callback(null, 'commonjs ' + request);}callback();} + ] +} +" +`; diff --git a/lib/creator/transformations/externals/__testfixtures__/externals-0.input.js b/lib/creator/transformations/externals/__testfixtures__/externals-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/externals/__testfixtures__/externals-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/externals/__testfixtures__/externals-1.input.js b/lib/creator/transformations/externals/__testfixtures__/externals-1.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/externals/__testfixtures__/externals-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/externals/externals.js b/lib/creator/transformations/externals/externals.js new file mode 100644 index 00000000000..9f20dc2d801 --- /dev/null +++ b/lib/creator/transformations/externals/externals.js @@ -0,0 +1,38 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for externals. Finds the externals property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createExternalProperty(p) { + if(webpackProperties instanceof RegExp || typeof(webpackProperties) === 'string') { + return utils.pushCreateProperty(j, p, 'externals', webpackProperties); + } + if(Array.isArray(webpackProperties)) { + const externalArray = utils.createArrayWithChildren( + j, 'externals', webpackProperties, true + ); + return p.value.properties.push(externalArray); + } + else { + utils.pushCreateProperty(j, p, 'externals', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'externals'); + } + } + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.safeTraverse(p , ['parent', 'value', 'left', 'property', 'name']) === 'exports') + .forEach(createExternalProperty); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/externals/externals.test.js b/lib/creator/transformations/externals/externals.test.js new file mode 100644 index 00000000000..07331dda65a --- /dev/null +++ b/lib/creator/transformations/externals/externals.test.js @@ -0,0 +1,61 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'externals', 'externals-0', /react/); +defineTest(__dirname, 'externals', 'externals-1', { + jquery: '\'jQuery\'', + react: '\'react\'' +}); + +defineTest(__dirname, 'externals', 'externals-1', 'myObj'); + +defineTest(__dirname, 'externals', 'externals-1', { + jquery: '\'jQuery\'', + react: 'reactObj' +}); + +defineTest(__dirname, 'externals', 'externals-1', { + jquery: '\'jQuery\'', + react: ['reactObj', 'path.join(__dirname, \'app\')', '\'jquery\''] +}); + +defineTest(__dirname, 'externals', 'externals-1', { + lodash: { + commonjs: '\'lodash\'', + amd: '\'lodash\'', + root: '\'_\'' + } +}); + +defineTest(__dirname, 'externals', 'externals-1', { + lodash: { + commonjs: 'lodash', + amd: 'hidash', + root: '_' + } +}); + +defineTest(__dirname, 'externals', 'externals-1', [ + { + a: 'false', + b: 'true', + '\'./ext\'': './hey' + }, + 'function(context, request, callback) {' + + 'if (/^yourregex$/.test(request)){' + + 'return callback(null, \'commonjs \' + request);' + + '}' + + 'callback();' + + '}' +] +); + +defineTest(__dirname, 'externals', 'externals-1', [ + 'myObj', + 'function(context, request, callback) {' + + 'if (/^yourregex$/.test(request)){' + + 'return callback(null, \'commonjs \' + request);' + + '}' + + 'callback();' + + '}' +] +); diff --git a/lib/creator/transformations/index.js b/lib/creator/transformations/index.js index 308d2644156..779bede11c6 100644 --- a/lib/creator/transformations/index.js +++ b/lib/creator/transformations/index.js @@ -1,25 +1,111 @@ -//const chalk = require('chalk'); -//const validateSchema = require('../../utils/validateSchema.js'); -//const webpackOptionsSchema = require('../../utils/webpackOptionsSchema.json'); -//const WebpackOptionsValidationError = require('../../utils/WebpackOptionsValidationError'); +const path = require('path'); +const j = require('jscodeshift'); +const chalk = require('chalk'); +const pEachSeries = require('p-each-series'); + +const runPrettier = require('../utils/run-prettier'); + +const entryTransform = require('./entry/entry'); +const outputTransform = require('./output/output'); +const contextTransform = require('./context/context'); +const resolveTransform = require('./resolve/resolve'); +const devtoolTransform = require('./devtool/devtool'); +const targetTransform = require('./target/target'); +const watchTransform = require('./watch/watch'); +const watchOptionsTransform = require('./watch/watchOptions'); +const externalsTransform = require('./externals/externals'); +const nodeTransform = require('./node/node'); +const performanceTransform = require('./performance/performance'); +const statsTransform = require('./stats/stats'); +const amdTransform = require('./other/amd'); +const bailTransform = require('./other/bail'); +const cacheTransform = require('./other/cache'); +const profileTransform = require('./other/profile'); +const mergeTransform = require('./other/merge'); +const moduleTransform = require('./module/module'); +const pluginsTransform = require('./plugins/plugins'); +const topScopeTransform = require('./top-scope/top-scope'); + /* * @function runTransform * * Runs the transformations from an object we get from yeoman * * @param { Object } transformObject - Options to transform -* @returns { } TODO +* @returns { } - A promise that writes each transform, runs prettier +* and writes the file */ -module.exports = function runTransform(transformObject) { - - // scaffold is done - /* - const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, initialWebpackConfig); - if (webpackOptionsValidationErrors.length) { - throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); - } else { - process.stdout.write('\n' + chalk.green('Congratulations! Your new webpack config file is created!') + '\n'); - } - */ +const transformsObject = { + entryTransform, + outputTransform, + contextTransform, + resolveTransform, + devtoolTransform, + targetTransform, + watchTransform, + watchOptionsTransform, + externalsTransform, + nodeTransform, + performanceTransform, + statsTransform, + amdTransform, + bailTransform, + cacheTransform, + profileTransform, + moduleTransform, + pluginsTransform, + topScopeTransform, + mergeTransform +}; + +module.exports = function runTransform(webpackProperties) { + + // webpackOptions.name sent to nameTransform if match + Object.keys(webpackProperties).forEach( (scaffoldPiece) => { + const config = webpackProperties[scaffoldPiece]; + + const transformations = Object.keys(transformsObject).map(k => { + const stringVal = k.substr(0, k.indexOf('Transform')); + if(config.webpackOptions) { + if(config.webpackOptions[stringVal]) { + return [transformsObject[k], config.webpackOptions[stringVal]]; + } else { + return [transformsObject[k], config[stringVal]]; + } + } else { + return [transformsObject[k]]; + } + }); + + const ast = j('module.exports = {}'); + + return pEachSeries(transformations, f => { + if(!f[1]) { + return f[0](j, ast); + } else { + return f[0](j, ast, f[1]); + } + }) + .then(() => { + let configurationName; + if(!config.configName) { + configurationName = 'webpack.config.js'; + } else { + configurationName = 'webpack.' + config.configName + '.js'; + } + + const outputPath = path.join(process.cwd(), configurationName); + const source = ast.toSource({ + quote: 'single' + }); + + runPrettier(outputPath, source); + }).catch(err => { + console.error(err.message ? err.message : err); + }); + }); + process.stdout.write('\n' + chalk.green( + 'Congratulations! Your new webpack configuration file has been created!\n' + )); }; diff --git a/lib/creator/transformations/module/__snapshots__/module.test.js.snap b/lib/creator/transformations/module/__snapshots__/module.test.js.snap new file mode 100644 index 00000000000..6824146c497 --- /dev/null +++ b/lib/creator/transformations/module/__snapshots__/module.test.js.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`module transforms correctly using "module-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + module: { + rules: [{ + test: /\\\\.(js|vue)$/, + loader: 'eslint-loader', + enforce: 'pre', + include: [customObj, 'Stringy'], + options: { + formatter: 'someOption' + } + }, { + test: /\\\\.vue$/, + loader: 'vue-loader', + options: vueObject + }, { + test: /\\\\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test')] + }, { + test: /\\\\.(png|jpe?g|gif|svg)(\\\\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, { + test: /\\\\.(woff2?|eot|ttf|otf)(\\\\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]'), + someArr: [Hey] + } + }] + } +} +" +`; + +exports[`module transforms correctly using "module-0" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + module: { + rules: [{{#if_eq build 'standalone'}}, { + test: /\\\\.(js|vue)$/, + loader: 'eslint-loader', + enforce: 'pre', + include: [customObj, 'Stringy'], + options: { + formatter: 'someOption' + } + }, { + test: /\\\\.vue$/, + loader: 'vue-loader', + options: vueObject + }, { + test: /\\\\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test')] + }, { + test: /\\\\.(png|jpe?g|gif|svg)(\\\\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]'), + {{#if_eq build 'standalone'}} + } + }, { + test: /\\\\.(woff2?|eot|ttf|otf)(\\\\?.*)?$/, + loader: 'url-loader', + {{#if_eq build 'standalone'}}, + options: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]') + } + }] + } +} +" +`; + +exports[`module transforms correctly using "module-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + module: { + noParse: /jquery|lodash/, + rules: [{ + test: /\\\\.js$/, + parser: { + amd: false + }, + + use: ['htmllint-loader', { + loader: 'html-loader', + options: { + hello: 'world' + } + }] + }] + } +} +" +`; diff --git a/lib/creator/transformations/module/__testfixtures__/module-0.input.js b/lib/creator/transformations/module/__testfixtures__/module-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/module/__testfixtures__/module-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/module/__testfixtures__/module-1.input.js b/lib/creator/transformations/module/__testfixtures__/module-1.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/module/__testfixtures__/module-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/module/module.js b/lib/creator/transformations/module/module.js new file mode 100644 index 00000000000..66c1371dccb --- /dev/null +++ b/lib/creator/transformations/module/module.js @@ -0,0 +1,33 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for module. Finds the module property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + + function createModuleProperties(p) { + utils.pushCreateProperty(j, p, 'module', j.objectExpression([])); + return utils.safeTraverse(p, ['key', 'name'] === 'module'); + } + function createRules(p) { + return utils.pushObjectKeys( + j, p, webpackProperties, 'module' + ); + } + if(!webpackProperties) { + return ast; + } else { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createModuleProperties)) + .forEach(p => createRules(p)); + } +}; diff --git a/lib/creator/transformations/module/module.test.js b/lib/creator/transformations/module/module.test.js new file mode 100644 index 00000000000..710060d9c5e --- /dev/null +++ b/lib/creator/transformations/module/module.test.js @@ -0,0 +1,93 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'module', 'module-0', { + rules: [{ + test: new RegExp(/\.(js|vue)$/), + loader: '\'eslint-loader\'', + enforce: '\'pre\'', + include: ['customObj', '\'Stringy\''], + options: { + formatter: '\'someOption\'' + } + }, { + test: new RegExp(/\.vue$/), + loader: '\'vue-loader\'', + options: 'vueObject' + }, { + test: new RegExp(/\.js$/), + loader: '\'babel-loader\'', + include: ['resolve(\'src\')', 'resolve(\'test\')'] + }, { + test: new RegExp(/\.(png|jpe?g|gif|svg)(\?.*)?$/), + loader: '\'url-loader\'', + options: { + limit: 10000, + name: 'utils.assetsPath(\'img/[name].[hash:7].[ext]\')' + } + }, { + test: new RegExp(/\.(woff2?|eot|ttf|otf)(\?.*)?$/), + loader: '\'url-loader\'', + options: { + limit: '10000', + name: 'utils.assetsPath(\'fonts/[name].[hash:7].[ext]\')', + someArr: ['Hey'] + } + }] +}); + +defineTest(__dirname, 'module', 'module-1', { + noParse: /jquery|lodash/, + rules: [{ + test: new RegExp(/\.js$/), + parser: { + amd: false + }, + use: [ + '\'htmllint-loader\'', + { + loader: '\'html-loader\'', + options: { + hello: '\'world\'' + } + } + ] + }] +}); + +defineTest(__dirname, 'module', 'module-0', { + rules: [ + '{{#if_eq build \'standalone\'}}', + { + test: new RegExp(/\.(js|vue)$/), + loader: '\'eslint-loader\'', + enforce: '\'pre\'', + include: ['customObj', '\'Stringy\''], + options: { + formatter: '\'someOption\'' + } + }, { + test: new RegExp(/\.vue$/), + loader: '\'vue-loader\'', + options: 'vueObject' + }, { + test: new RegExp(/\.js$/), + loader: '\'babel-loader\'', + include: ['resolve(\'src\')', 'resolve(\'test\')'] + }, { + test: new RegExp(/\.(png|jpe?g|gif|svg)(\?.*)?$/), + loader: '\'url-loader\'', + options: { + limit: 10000, + name: 'utils.assetsPath(\'img/[name].[hash:7].[ext]\')', + inject: '{{#if_eq build \'standalone\'}}' + } + }, { + test: new RegExp(/\.(woff2?|eot|ttf|otf)(\?.*)?$/), + loader: '\'url-loader\'', + inject: '{{#if_eq build \'standalone\'}}', + options: { + limit: '10000', + name: 'utils.assetsPath(\'fonts/[name].[hash:7].[ext]\')' + } + }] +}); diff --git a/lib/creator/transformations/node/__snapshots__/node.test.js.snap b/lib/creator/transformations/node/__snapshots__/node.test.js.snap new file mode 100644 index 00000000000..0046bc33d63 --- /dev/null +++ b/lib/creator/transformations/node/__snapshots__/node.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`node transforms correctly using "node-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + node: { + console: false, + global: true, + process: true, + Buffer: true, + __filename: mock, + __dirname: mock, + setImmediate: true + } +} +" +`; diff --git a/lib/creator/transformations/node/__testfixtures__/node-0.input.js b/lib/creator/transformations/node/__testfixtures__/node-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/node/__testfixtures__/node-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/node/node.js b/lib/creator/transformations/node/node.js new file mode 100644 index 00000000000..26a9a13ffe4 --- /dev/null +++ b/lib/creator/transformations/node/node.js @@ -0,0 +1,26 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for node. Finds the node property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createNodeProperty(p) { + utils.pushCreateProperty(j, p, 'node', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'node'); + } + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createNodeProperty)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/node/node.test.js b/lib/creator/transformations/node/node.test.js new file mode 100644 index 00000000000..53cf24e541a --- /dev/null +++ b/lib/creator/transformations/node/node.test.js @@ -0,0 +1,11 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'node', 'node-0', { + console: false, + global: true, + process: true, + Buffer: true, + __filename: 'mock', + __dirname: 'mock', + setImmediate: true +}); diff --git a/lib/creator/transformations/other/__snapshots__/other.test.js.snap b/lib/creator/transformations/other/__snapshots__/other.test.js.snap new file mode 100644 index 00000000000..5a3d06f1f94 --- /dev/null +++ b/lib/creator/transformations/other/__snapshots__/other.test.js.snap @@ -0,0 +1,74 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`amd transforms correctly using "other-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + amd: { + jQuery: true, + kQuery: false + } +} +" +`; + +exports[`bail transforms correctly using "other-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + bail: true +} +" +`; + +exports[`cache transforms correctly using "other-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + cache: true +} +" +`; + +exports[`cache transforms correctly using "other-0" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + cache: cacheVal +} +" +`; + +exports[`merge transforms correctly using "other-0" data 1`] = ` +"module.exports = merge(myConfig, { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +}); +" +`; + +exports[`profile transforms correctly using "other-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + profile: true +} +" +`; diff --git a/lib/creator/transformations/other/__testfixtures__/other-0.input.js b/lib/creator/transformations/other/__testfixtures__/other-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/other/__testfixtures__/other-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/other/amd.js b/lib/creator/transformations/other/amd.js new file mode 100644 index 00000000000..8453e6105c1 --- /dev/null +++ b/lib/creator/transformations/other/amd.js @@ -0,0 +1,26 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for amd. Finds the amd property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createAMDProperty(p) { + utils.pushCreateProperty(j, p, 'amd', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'amd'); + } + if(webpackProperties && typeof(webpackProperties) === 'object') { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createAMDProperty)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/other/bail.js b/lib/creator/transformations/other/bail.js new file mode 100644 index 00000000000..b61d793f30a --- /dev/null +++ b/lib/creator/transformations/other/bail.js @@ -0,0 +1,23 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for bail. Finds the bail property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'bail', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/other/cache.js b/lib/creator/transformations/other/cache.js new file mode 100644 index 00000000000..ad0ee5d5e0c --- /dev/null +++ b/lib/creator/transformations/other/cache.js @@ -0,0 +1,23 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for cache. Finds the cache property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'cache', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/other/merge.js b/lib/creator/transformations/other/merge.js new file mode 100644 index 00000000000..b93d2f4bde8 --- /dev/null +++ b/lib/creator/transformations/other/merge.js @@ -0,0 +1,44 @@ +/* +* +* Transform for merge. Finds the merge property from yeoman and creates a way +* for users to allow webpack-merge in their scaffold +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createMergeProperty(p) { + // FIXME Use j.callExp() + let exportsDecl = p.value.body.map( (n) => { + if(n.expression) { + return n.expression.right; + } + }); + const bodyLength = exportsDecl.length; + let newVal = {}; + newVal.type = 'ExpressionStatement'; + newVal.expression = { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: false, + object: j.identifier('module'), + property: j.identifier('exports') + }, + right: j.callExpression( + j.identifier('merge'), + [j.identifier(webpackProperties), exportsDecl.pop()]) + }; + p.value.body[bodyLength - 1] = newVal; + } + if(webpackProperties) { + return ast.find(j.Program) + .filter(p => createMergeProperty(p)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/other/other.test.js b/lib/creator/transformations/other/other.test.js new file mode 100644 index 00000000000..ba9d0c66ac9 --- /dev/null +++ b/lib/creator/transformations/other/other.test.js @@ -0,0 +1,11 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'amd', 'other-0', { + jQuery: true, + kQuery: false} +); +defineTest(__dirname, 'bail', 'other-0', true); +defineTest(__dirname, 'cache', 'other-0', true); +defineTest(__dirname, 'cache', 'other-0', 'cacheVal'); +defineTest(__dirname, 'profile', 'other-0', true); +defineTest(__dirname, 'merge', 'other-0', 'myConfig'); diff --git a/lib/creator/transformations/other/profile.js b/lib/creator/transformations/other/profile.js new file mode 100644 index 00000000000..bdb146dc9d4 --- /dev/null +++ b/lib/creator/transformations/other/profile.js @@ -0,0 +1,22 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for profile. Finds the profile property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'profile', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/output/__snapshots__/output.test.js.snap b/lib/creator/transformations/output/__snapshots__/output.test.js.snap new file mode 100644 index 00000000000..9c51ca710ee --- /dev/null +++ b/lib/creator/transformations/output/__snapshots__/output.test.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`output transforms correctly using "output-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle', + path: 'dist/assets', + pathinfo: true, + publicPath: 'https://google.com', + sourceMapFilename: '[name].map', + sourcePrefix: '\\\\t', + umdNamedDefine: true, + strictModuleExceptionHandling: true + } +} +" +`; diff --git a/lib/creator/transformations/output/__testfixtures__/output-0.input.js b/lib/creator/transformations/output/__testfixtures__/output-0.input.js new file mode 100644 index 00000000000..a9899df14fa --- /dev/null +++ b/lib/creator/transformations/output/__testfixtures__/output-0.input.js @@ -0,0 +1,3 @@ +module.exports = { + entry: 'index.js' +} diff --git a/lib/creator/transformations/output/output.js b/lib/creator/transformations/output/output.js new file mode 100644 index 00000000000..ea69aaab02a --- /dev/null +++ b/lib/creator/transformations/output/output.js @@ -0,0 +1,26 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for output. Finds the output property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createOutputProperties(p) { + utils.pushCreateProperty(j, p, 'output', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'output'); + } + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createOutputProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/output/output.test.js b/lib/creator/transformations/output/output.test.js new file mode 100644 index 00000000000..413ead9c291 --- /dev/null +++ b/lib/creator/transformations/output/output.test.js @@ -0,0 +1,13 @@ +const defineTest = require('../../../transformations/defineTest'); +const jscodeshift = require('jscodeshift'); + +defineTest(__dirname, 'output', 'output-0', { + filename: '\'bundle\'', + path: '\'dist/assets\'', + pathinfo: true, + publicPath: '\'https://google.com\'', + sourceMapFilename: '\'[name].map\'', + sourcePrefix: jscodeshift('\'\t\''), + umdNamedDefine: true, + strictModuleExceptionHandling: true +}); diff --git a/lib/creator/transformations/performance/__snapshots__/performance.test.js.snap b/lib/creator/transformations/performance/__snapshots__/performance.test.js.snap new file mode 100644 index 00000000000..93bd117add3 --- /dev/null +++ b/lib/creator/transformations/performance/__snapshots__/performance.test.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`performance transforms correctly using "performance-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + performance: { + hints: 'warning', + maxEntrypointSize: 400000, + maxAssetSize: 100000, + assetFilter: function(assetFilename) {return assetFilename.endsWith('.js');} + } +} +" +`; diff --git a/lib/creator/transformations/performance/__testfixtures__/performance-0.input.js b/lib/creator/transformations/performance/__testfixtures__/performance-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/performance/__testfixtures__/performance-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/performance/performance.js b/lib/creator/transformations/performance/performance.js new file mode 100644 index 00000000000..7450f12df62 --- /dev/null +++ b/lib/creator/transformations/performance/performance.js @@ -0,0 +1,27 @@ +const utils = require('../../../transformations/utils'); + + +/* +* +* Transform for performance. Finds the performance property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + + function createPerformanceProperty(p) { + utils.pushCreateProperty(j, p, 'performance', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'performance'); + } + if(webpackProperties && typeof(webpackProperties) === 'object') { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createPerformanceProperty)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/performance/performance.test.js b/lib/creator/transformations/performance/performance.test.js new file mode 100644 index 00000000000..0e078cb5d24 --- /dev/null +++ b/lib/creator/transformations/performance/performance.test.js @@ -0,0 +1,10 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'performance', 'performance-0', { + hints: '\'warning\'', + maxEntrypointSize: 400000, + maxAssetSize: 100000, + assetFilter: 'function(assetFilename) {' + + 'return assetFilename.endsWith(\'.js\');' + + '}' +}); diff --git a/lib/creator/transformations/plugins/__snapshots__/plugins.test.js.snap b/lib/creator/transformations/plugins/__snapshots__/plugins.test.js.snap new file mode 100644 index 00000000000..6fbe0c9fe35 --- /dev/null +++ b/lib/creator/transformations/plugins/__snapshots__/plugins.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`plugins transforms correctly using "plugins-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + plugins: [ + new webpack.optimize.CommonsChunkPlugin({name:'vendor',filename:'vendor-[hash].min.js'}) + ] +} +" +`; diff --git a/lib/creator/transformations/plugins/__testfixtures__/plugins-0.input.js b/lib/creator/transformations/plugins/__testfixtures__/plugins-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/plugins/__testfixtures__/plugins-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/plugins/plugins.js b/lib/creator/transformations/plugins/plugins.js new file mode 100644 index 00000000000..33167582f29 --- /dev/null +++ b/lib/creator/transformations/plugins/plugins.js @@ -0,0 +1,25 @@ +const utils = require('../../../transformations/utils'); + +/* +* +* Transform for plugins. Finds the plugins property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createPluginsProperty(p) { + const pluginArray = utils.createArrayWithChildren(j, 'plugins', webpackProperties, true); + return p.value.properties.push(pluginArray); + } + if(webpackProperties && Array.isArray(webpackProperties)) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createPluginsProperty)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/plugins/plugins.test.js b/lib/creator/transformations/plugins/plugins.test.js new file mode 100644 index 00000000000..dde8d0ae2d5 --- /dev/null +++ b/lib/creator/transformations/plugins/plugins.test.js @@ -0,0 +1,5 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'plugins', 'plugins-0', [ + 'new webpack.optimize.CommonsChunkPlugin({name:' + '\'' + 'vendor' + '\'' + ',filename:' + '\'' + 'vendor' + '-[hash].min.js\'})' +]); diff --git a/lib/creator/transformations/resolve/__snapshots__/resolve.test.js.snap b/lib/creator/transformations/resolve/__snapshots__/resolve.test.js.snap new file mode 100644 index 00000000000..8e78a7cc775 --- /dev/null +++ b/lib/creator/transformations/resolve/__snapshots__/resolve.test.js.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resolve transforms correctly using "resolve-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + resolve: { + alias: { + {{#if_eq build 'standalone'}}, + hello: 'world', + {{/if_eq}}, + world: hello + }, + + aliasFields: ['browser', wars], + descriptionFiles: ['a', b], + enforceExtension: false, + enforceModuleExtension: false, + extensions: [hey, 'ho'], + mainFields: [main, 'story'], + mainFiles: ['noMainFileHere', iGuess], + modules: [one, 'two'], + unsafeCache: false, + resolveLoader: { + modules: ['node_modules', mode_nodules], + extensions: [jsVal, '.json'], + mainFields: [loader, 'main'], + moduleExtensions: ['-loader', value] + }, + + plugins: [somePlugin, 'stringVal'], + symlinks: true + } +} +" +`; diff --git a/lib/creator/transformations/resolve/__testfixtures__/resolve-0.input.js b/lib/creator/transformations/resolve/__testfixtures__/resolve-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/resolve/__testfixtures__/resolve-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/resolve/resolve.js b/lib/creator/transformations/resolve/resolve.js new file mode 100644 index 00000000000..66d572edb84 --- /dev/null +++ b/lib/creator/transformations/resolve/resolve.js @@ -0,0 +1,26 @@ +const utils = require('../../../transformations/utils'); + +/* +* +* Transform for resolve. Finds the resolve property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createResolveProperties(p) { + utils.pushCreateProperty(j, p, 'resolve', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'resolve'); + } + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createResolveProperties)); + } + else { + return ast; + } +}; diff --git a/lib/creator/transformations/resolve/resolve.test.js b/lib/creator/transformations/resolve/resolve.test.js new file mode 100644 index 00000000000..53218c2a4f3 --- /dev/null +++ b/lib/creator/transformations/resolve/resolve.test.js @@ -0,0 +1,27 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'resolve', 'resolve-0', { + alias: { + inject: '{{#if_eq build \'standalone\'}}', + hello: '\'world\'', + inject_1: '{{/if_eq}}', + world: 'hello', + }, + aliasFields: ['\'browser\'', 'wars'], + descriptionFiles: ['\'a\'', 'b'], + enforceExtension: false, + enforceModuleExtension: false, + extensions: ['hey', '\'ho\''], + mainFields: ['main', '\'story\''], + mainFiles: ['\'noMainFileHere\'', 'iGuess'], + modules: ['one', '\'two\''], + unsafeCache: false, + resolveLoader: { + modules: ['\'node_modules\'', 'mode_nodules'], + extensions: ['jsVal', '\'.json\''], + mainFields: ['loader', '\'main\''], + moduleExtensions: ['\'-loader\'', 'value'] + }, + plugins: ['somePlugin', '\'stringVal\''], + symlinks: true +}); diff --git a/lib/creator/transformations/stats/__snapshots__/stats.test.js.snap b/lib/creator/transformations/stats/__snapshots__/stats.test.js.snap new file mode 100644 index 00000000000..b94252a2e01 --- /dev/null +++ b/lib/creator/transformations/stats/__snapshots__/stats.test.js.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stats transforms correctly using "stats-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + stats: { + assets: true, + assetsSort: 'field', + cached: true, + cachedAssets: true, + children: true, + chunks: true, + chunkModules: true, + chunkOrigins: true, + chunksSort: 'field', + context: '../src/', + colors: true, + depth: false, + entrypoints: customVal, + errors: true, + errorDetails: true, + exclude: [], + hash: true, + maxModules: 15, + modules: true, + modulesSort: 'field', + performance: true, + providedExports: false, + publicPath: true, + reasons: true, + source: true, + timings: true, + usedExports: false, + version: true, + warnings: true + } +} +" +`; + +exports[`stats transforms correctly using "stats-0" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + stats: 'errors-only' +} +" +`; diff --git a/lib/creator/transformations/stats/__testfixtures__/stats-0.input.js b/lib/creator/transformations/stats/__testfixtures__/stats-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/stats/__testfixtures__/stats-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/stats/stats.js b/lib/creator/transformations/stats/stats.js new file mode 100644 index 00000000000..46a0e0ad5e3 --- /dev/null +++ b/lib/creator/transformations/stats/stats.js @@ -0,0 +1,29 @@ +const utils = require('../../../transformations/utils'); + +/* +* +* Transform for stats. Finds the stats property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createStatsProperty(p) { + utils.pushCreateProperty(j, p, 'stats', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'stats'); + } + if(webpackProperties && typeof(webpackProperties) === 'object') { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createStatsProperty)); + } + else if(webpackProperties && webpackProperties.length) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'stats', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/stats/stats.test.js b/lib/creator/transformations/stats/stats.test.js new file mode 100644 index 00000000000..432eac37522 --- /dev/null +++ b/lib/creator/transformations/stats/stats.test.js @@ -0,0 +1,34 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'stats', 'stats-0', { + assets: true, + assetsSort: '\'field\'', + cached: true, + cachedAssets: true, + children: true, + chunks: true, + chunkModules: true, + chunkOrigins: true, + chunksSort: '\'field\'', + context: '\'../src/\'', + colors: true, + depth: false, + entrypoints: 'customVal', + errors: true, + errorDetails: true, + exclude: [], + hash: true, + maxModules: 15, + modules: true, + modulesSort: '\'field\'', + performance: true, + providedExports: false, + publicPath: true, + reasons: true, + source: true, + timings: true, + usedExports: false, + version: true, + warnings: true +}); +defineTest(__dirname, 'stats', 'stats-0', '\'errors-only\''); diff --git a/lib/creator/transformations/target/__snapshots__/target.test.js.snap b/lib/creator/transformations/target/__snapshots__/target.test.js.snap new file mode 100644 index 00000000000..3af819e1ce2 --- /dev/null +++ b/lib/creator/transformations/target/__snapshots__/target.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`target transforms correctly using "target-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + target: 'async-node' +} +" +`; + +exports[`target transforms correctly using "target-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + target: node +} +" +`; diff --git a/lib/creator/transformations/target/__testfixtures__/target-0.input.js b/lib/creator/transformations/target/__testfixtures__/target-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/target/__testfixtures__/target-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/target/__testfixtures__/target-1.input.js b/lib/creator/transformations/target/__testfixtures__/target-1.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/target/__testfixtures__/target-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/target/target.js b/lib/creator/transformations/target/target.js new file mode 100644 index 00000000000..01501315d1d --- /dev/null +++ b/lib/creator/transformations/target/target.js @@ -0,0 +1,22 @@ +const utils = require('../../../transformations/utils'); + +/* +* +* Transform for target. Finds the target property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + + if(webpackProperties && webpackProperties.length) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'target', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/target/target.test.js b/lib/creator/transformations/target/target.test.js new file mode 100644 index 00000000000..e42a7347a91 --- /dev/null +++ b/lib/creator/transformations/target/target.test.js @@ -0,0 +1,4 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'target', 'target-0', '\'async-node\''); +defineTest(__dirname, 'target', 'target-1', 'node'); diff --git a/lib/creator/transformations/top-scope/__snapshots__/top-scope.test.js.snap b/lib/creator/transformations/top-scope/__snapshots__/top-scope.test.js.snap new file mode 100644 index 00000000000..9ceb4dcb14e --- /dev/null +++ b/lib/creator/transformations/top-scope/__snapshots__/top-scope.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`top-scope transforms correctly using "top-scope-0" data 1`] = ` +"var test = 'me'; +module.exports = {} +" +`; diff --git a/lib/creator/transformations/top-scope/__testfixtures__/top-scope-0.input.js b/lib/creator/transformations/top-scope/__testfixtures__/top-scope-0.input.js new file mode 100644 index 00000000000..4ba52ba2c8d --- /dev/null +++ b/lib/creator/transformations/top-scope/__testfixtures__/top-scope-0.input.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/lib/creator/transformations/top-scope/top-scope.js b/lib/creator/transformations/top-scope/top-scope.js new file mode 100644 index 00000000000..06e681d4577 --- /dev/null +++ b/lib/creator/transformations/top-scope/top-scope.js @@ -0,0 +1,22 @@ + +/* +* +* Get an property named topScope from yeoman and inject it to the top scope of +* the config, outside module.exports +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing topscope properties +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createTopScopeProperty(p) { + webpackProperties.forEach( (n) => { + p.value.body.splice(-1, 0, n); + }); + } + if(webpackProperties) { + return ast.find(j.Program).filter(p => createTopScopeProperty(p)); + } +}; diff --git a/lib/creator/transformations/top-scope/top-scope.test.js b/lib/creator/transformations/top-scope/top-scope.test.js new file mode 100644 index 00000000000..da1398e55f9 --- /dev/null +++ b/lib/creator/transformations/top-scope/top-scope.test.js @@ -0,0 +1,5 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'top-scope', 'top-scope-0', [ + 'var test = \'me\';' +]); diff --git a/lib/creator/transformations/watch/__snapshots__/watch.test.js.snap b/lib/creator/transformations/watch/__snapshots__/watch.test.js.snap new file mode 100644 index 00000000000..964ece52812 --- /dev/null +++ b/lib/creator/transformations/watch/__snapshots__/watch.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`watch transforms correctly using "watch-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watch: true +} +" +`; + +exports[`watch transforms correctly using "watch-0" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watch: false +} +" +`; + +exports[`watch transforms correctly using "watch-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watch: true +} +" +`; + +exports[`watch transforms correctly using "watch-1" data 2`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watch: false +} +" +`; diff --git a/lib/creator/transformations/watch/__snapshots__/watchOptions.test.js.snap b/lib/creator/transformations/watch/__snapshots__/watchOptions.test.js.snap new file mode 100644 index 00000000000..6f93736f0fd --- /dev/null +++ b/lib/creator/transformations/watch/__snapshots__/watchOptions.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`watchOptions transforms correctly using "watch-0" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watchOptions: { + aggregateTimeout: 300, + poll: 1000, + ignored: /node_modules/ + } +} +" +`; + +exports[`watchOptions transforms correctly using "watch-1" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watchOptions: { + aggregateTimeout: 300, + poll: 1000, + ignored: /node_modules/ + } +} +" +`; + +exports[`watchOptions transforms correctly using "watch-2" data 1`] = ` +"module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + }, + + watchOptions: { + aggregateTimeout: 300, + poll: 1000, + ignored: /node_modules/ + } +} +" +`; diff --git a/lib/creator/transformations/watch/__testfixtures__/watch-0.input.js b/lib/creator/transformations/watch/__testfixtures__/watch-0.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/watch/__testfixtures__/watch-0.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/watch/__testfixtures__/watch-1.input.js b/lib/creator/transformations/watch/__testfixtures__/watch-1.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/watch/__testfixtures__/watch-1.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/watch/__testfixtures__/watch-2.input.js b/lib/creator/transformations/watch/__testfixtures__/watch-2.input.js new file mode 100644 index 00000000000..ea0822c2484 --- /dev/null +++ b/lib/creator/transformations/watch/__testfixtures__/watch-2.input.js @@ -0,0 +1,6 @@ +module.exports = { + entry: 'index.js', + output: { + filename: 'bundle.js' + } +} diff --git a/lib/creator/transformations/watch/watch.js b/lib/creator/transformations/watch/watch.js new file mode 100644 index 00000000000..650ddd22805 --- /dev/null +++ b/lib/creator/transformations/watch/watch.js @@ -0,0 +1,21 @@ +const utils = require('../../../transformations/utils'); + +/* +* +* Transform for watch. Finds the watch property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + if(typeof(webpackProperties) === 'boolean') { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(j, p, utils.pushCreateProperty, 'watch', webpackProperties)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/watch/watch.test.js b/lib/creator/transformations/watch/watch.test.js new file mode 100644 index 00000000000..c564fcba4ec --- /dev/null +++ b/lib/creator/transformations/watch/watch.test.js @@ -0,0 +1,6 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'watch', 'watch-0', true); +defineTest(__dirname, 'watch', 'watch-0', false); +defineTest(__dirname, 'watch', 'watch-1', true); +defineTest(__dirname, 'watch', 'watch-1', false); diff --git a/lib/creator/transformations/watch/watchOptions.js b/lib/creator/transformations/watch/watchOptions.js new file mode 100644 index 00000000000..b08c8218a5c --- /dev/null +++ b/lib/creator/transformations/watch/watchOptions.js @@ -0,0 +1,25 @@ +const utils = require('../../../transformations/utils'); + +/* +* +* Transform for watchOptions. Finds the watchOptions property from yeoman and creates a +* property based on what the user has given us. +* +* @param j — jscodeshift API +* @param ast - jscodeshift API +* @param { Object } webpackProperties - Object containing transformation rules +* @returns ast - jscodeshift API +*/ + +module.exports = function(j, ast, webpackProperties) { + function createWatchOptionsProperty(p) { + utils.pushCreateProperty(j, p, 'watchOptions', j.objectExpression([])); + return utils.pushObjectKeys(j, p, webpackProperties, 'watchOptions'); + } + if(webpackProperties) { + return ast.find(j.ObjectExpression) + .filter(p => utils.isAssignment(null, p, createWatchOptionsProperty)); + } else { + return ast; + } +}; diff --git a/lib/creator/transformations/watch/watchOptions.test.js b/lib/creator/transformations/watch/watchOptions.test.js new file mode 100644 index 00000000000..33eb8811e08 --- /dev/null +++ b/lib/creator/transformations/watch/watchOptions.test.js @@ -0,0 +1,19 @@ +const defineTest = require('../../../transformations/defineTest'); + +defineTest(__dirname, 'watchOptions', 'watch-0', { + aggregateTimeout: 300, + poll: 1000, + ignored: '/node_modules/' +}); + +defineTest(__dirname, 'watchOptions', 'watch-1', { + aggregateTimeout: 300, + poll: 1000, + ignored: '/node_modules/' +}); + +defineTest(__dirname, 'watchOptions', 'watch-2', { + aggregateTimeout: 300, + poll: 1000, + ignored: '/node_modules/' +}); diff --git a/lib/creator/utils/run-prettier.js b/lib/creator/utils/run-prettier.js new file mode 100644 index 00000000000..59f60b0e161 --- /dev/null +++ b/lib/creator/utils/run-prettier.js @@ -0,0 +1,33 @@ +const prettier = require('prettier'); +const fs = require('fs'); +const chalk = require('chalk'); + +/* +* +* Runs prettier and later prints the output configuration +* +* @param { String } outputPath - Path to write the config to +* @param { Node } source - AST to write at the given path +* @returns fs - Writes a file at given location and prints messages accordingly +*/ + +module.exports = function runPrettier(outputPath, source) { + function validateConfig() { + let prettySource; + try { + prettySource = prettier.format(source, { + singleQuote: true, + useTabs: true, + tabWidth: 1, + }); + } catch(err) { + process.stdout.write('\n' + + chalk.yellow(`WARNING: Could not apply prettier to ${outputPath}` + + ' due validation error, but the file has been created\n') + ); + prettySource = source; + } + return fs.writeFileSync(outputPath, prettySource, 'utf8'); + } + return fs.writeFile(outputPath, source, 'utf8', validateConfig); +}; diff --git a/lib/creator/utils/validate-options.js b/lib/creator/utils/validate-options.js index be76787e0f8..111bebd428b 100644 --- a/lib/creator/utils/validate-options.js +++ b/lib/creator/utils/validate-options.js @@ -31,29 +31,7 @@ module.exports = function validateOptions(opts) { fs.readFileSync(part); } catch (err) { console.error('Found no file at:', part); - process.exit(1); + process.exitCode = 1; } }); }; -/* - -const fs = require('fs'); -const path = require('path'); -const Rx = require('rxjs'); - -function getPath(part) { - return path.join(process.cwd(), part); -} - -const readFileObservable = Rx.Observable.bindNodeCallback(fs.readFileSync); - -module.exports = function validateOptions(opts) { - return Rx.Observable.pairs(opts) - .map(([, opt]) => getPath(opt)) - .flatMap(part => readFileObservable(part)) - .then(part, err => { - console.error('Found no file at:', part); - }); -}; - -*/ diff --git a/lib/creator/utils/validate-options.spec.js b/lib/creator/utils/validate-options.spec.js index fc2c0ef0f4f..aae91533562 100644 --- a/lib/creator/utils/validate-options.spec.js +++ b/lib/creator/utils/validate-options.spec.js @@ -13,7 +13,7 @@ describe('validate-options', () => { it('should find the real files', () => { expect(() => { validateOptions({entry: 'package.json'}); - }).not.toThrowError('Did not find the file'); + }).not.toThrowError(/'Did not find the file'/); }); }); diff --git a/lib/creator/yeoman/utils/entry.js b/lib/creator/yeoman/utils/entry.js new file mode 100644 index 00000000000..f1f2839e8f7 --- /dev/null +++ b/lib/creator/yeoman/utils/entry.js @@ -0,0 +1,74 @@ +const InputValidate = require('webpack-addons').InputValidate; +const validate = require('./validate'); + +module.exports = (self, answer) => { + let entryIdentifiers; + let result; + if(answer['entryType'] === true) { + result = self.prompt([ + InputValidate( + 'multipleEntries', + 'Type the name you want for your modules (entry files), seperated by comma', + validate + ) + ]).then( (multipleEntriesAnswer) => { + let webpackEntryPoint = {}; + entryIdentifiers = multipleEntriesAnswer['multipleEntries'].split(','); + function forEachPromise(obj, fn) { + return obj.reduce(function (promise, prop) { + const trimmedProp = prop.trim(); + return promise.then(function (n) { + if(n) { + Object.keys(n).forEach( (val) => { + if( + n[val].charAt(0) !== '(' + && n[val].charAt(0) !== '[' + && n[val].indexOf('function') < 0 + && n[val].indexOf('path') < 0 + && n[val].indexOf('process') < 0 + ) { + n[val] = `'${n[val]}.js'`; + } + webpackEntryPoint[val] = n[val]; + }); + } else { + n = {}; + } + return fn(trimmedProp); + }); + }, Promise.resolve()); + } + return forEachPromise(entryIdentifiers, (entryProp) => self.prompt([ + InputValidate( + `${entryProp}`, + `What is the location of '${entryProp}'?`, + validate + ) + ])).then(propAns => { + Object.keys(propAns).forEach( (val) => { + if( + propAns[val].charAt(0) !== '(' + && propAns[val].charAt(0) !== '[' + && propAns[val].indexOf('function') < 0 + && propAns[val].indexOf('path') < 0 + && propAns[val].indexOf('process') < 0 + ) { + propAns[val] = `'${propAns[val]}.js'`; + } + webpackEntryPoint[val] = propAns[val]; + }); + return webpackEntryPoint; + }); + }); + } + else { + result = self.prompt([ + InputValidate( + 'singularEntry', + 'Which module will be the first to enter the application?', + validate + ) + ]).then( (singularAnswer) => `'${singularAnswer['singularEntry']}'`); + } + return result; +}; diff --git a/lib/creator/yeoman/utils/module.js b/lib/creator/yeoman/utils/module.js new file mode 100644 index 00000000000..161ec111962 --- /dev/null +++ b/lib/creator/yeoman/utils/module.js @@ -0,0 +1,12 @@ +module.exports = () => { + return { + test: new RegExp(/\.js$/), + exclude: '/node_modules/', + loader: '\'babel-loader\'', + options: { + presets: [ + '\'es2015\'' + ] + } + }; +}; diff --git a/lib/creator/yeoman/utils/plugins.js b/lib/creator/yeoman/utils/plugins.js new file mode 100644 index 00000000000..90b39179d41 --- /dev/null +++ b/lib/creator/yeoman/utils/plugins.js @@ -0,0 +1,5 @@ +module.exports = () => { + return [ + 'new UglifyJSPlugin()' + ]; +}; diff --git a/lib/creator/yeoman/utils/tooltip.js b/lib/creator/yeoman/utils/tooltip.js new file mode 100644 index 00000000000..041966d9a2f --- /dev/null +++ b/lib/creator/yeoman/utils/tooltip.js @@ -0,0 +1,49 @@ +module.exports = { + uglify: () => { + return (`/* + * We've enabled UglifyJSPlugin for you! This minifies your app + * in order to load faster and run less javascript. + * + * https://github.com/webpack-contrib/uglifyjs-webpack-plugin + * + */`); + }, + commonsChunk: () => { + return (`/* + * We've enabled commonsChunkPlugin for you. This allows your app to + * load faster and it splits the modules you provided as entries across + * different bundles! + * + * https://webpack.js.org/plugins/commons-chunk-plugin/ + * + */`); + }, + cssPlugin: () => { + return( + `/* + * We've enabled ExtractTextPlugin for you. This allows your app to + * use css modules that will be moved into a seperate CSS file instead of inside + * one of your module entries! + * + * https://github.com/webpack-contrib/extract-text-webpack-plugin + * + */`); + }, + postcss: () => { + return( + `/* + * We've enabled Postcss, autoprefixer and precss for you. This allows your app + * to lint CSS, support variables and mixins, transpile future CSS syntax, + * inline images, and more! + * + * To enable SASS or LESS, add the respective loaders to module.rules + * + * https://github.com/postcss/postcss + * + * https://github.com/postcss/autoprefixer + * + * https://github.com/jonathantneal/precss + * + */`); + } +}; diff --git a/lib/creator/yeoman/utils/validate.js b/lib/creator/yeoman/utils/validate.js new file mode 100644 index 00000000000..bf981fd454f --- /dev/null +++ b/lib/creator/yeoman/utils/validate.js @@ -0,0 +1,7 @@ +module.exports = (value) => { + const pass = value.length; + if(pass) { + return true; + } + return 'Please specify an answer!'; +}; diff --git a/lib/creator/yeoman/webpack-adapter.js b/lib/creator/yeoman/webpack-adapter.js index ae95fc287eb..00cf84195c1 100644 --- a/lib/creator/yeoman/webpack-adapter.js +++ b/lib/creator/yeoman/webpack-adapter.js @@ -1,6 +1,12 @@ const inquirer = require('inquirer'); -// we can use rxJS here to validate the answers against a generator +/* +* @class - WebpackAdapter +* +* Custom adapter that is integrated in the scaffold. Here we can validate +* paths and answers from the user +* +*/ module.exports = class WebpackAdapter { prompt(questions, callback) { const promise = inquirer.prompt(questions); diff --git a/lib/creator/yeoman/webpack-generator.js b/lib/creator/yeoman/webpack-generator.js index 726f4c34205..de4587356fc 100644 --- a/lib/creator/yeoman/webpack-generator.js +++ b/lib/creator/yeoman/webpack-generator.js @@ -1,22 +1,328 @@ const Generator = require('yeoman-generator'); + +const createCommonsChunkPlugin = require('webpack-addons').createCommonsChunkPlugin; + const Input = require('webpack-addons').Input; +const Confirm = require('webpack-addons').Confirm; +const RawList = require('webpack-addons').RawList; +const entryQuestions = require('./utils/entry'); +const getBabelPlugin = require('./utils/module'); +const getDefaultPlugins = require('./utils/plugins'); +const tooltip = require('./utils/tooltip'); module.exports = class WebpackGenerator extends Generator { constructor(args, opts) { super(args, opts); - this.configuration = {}; + this.isProd = false; + this.npmInstalls = ['webpack', 'uglifyjs-webpack-plugin']; + this.configuration = { + config: { + webpackOptions: {}, + topScope: [] + } + }; } prompting() { - return this.prompt([Input('entry', 'What is the name of the entry point in your application?'), - Input('output', 'What is the name of the output directory in your application?')]).then( (answer) => { - this.configuration.webpackOptions = answer; + + let done = this.async(); + let self = this; + let oneOrMoreEntries; + let regExpForStyles; + let ExtractUseProps; + + this.configuration.config.webpackOptions.module = { + rules: [] + }; + this.configuration.config.webpackOptions.plugins = getDefaultPlugins(); + this.configuration.config.topScope.push( + 'const webpack = require(\'webpack\')', + tooltip.uglify(), + 'const UglifyJSPlugin = require(\'uglifyjs-webpack-plugin\');', + '\n' + ); + this.prompt([ + Confirm('entryType', 'Will you be creating multiple bundles?') + ]).then( (entryTypeAnswer) => { + // Ask different questions for entry points + entryQuestions(self, entryTypeAnswer).then(entryOptions => { + this.configuration.config.webpackOptions.entry = entryOptions; + oneOrMoreEntries = Object.keys(entryOptions); + }).then( () => { + + this.prompt([ + Input( + 'outputType', + 'Which folder will your generated bundles be in? [default: dist]:' + ) + ]).then( (outputTypeAnswer) => { + if(!this.configuration.config.webpackOptions.entry.length) { + this.configuration.config.topScope.push(tooltip.commonsChunk()); + this.configuration.config.webpackOptions.output = { + filename: '\'[name].[chunkhash].js\'', + chunkFilename: '\'[name].[chunkhash].js\'' + }; + } else { + this.configuration.config.webpackOptions.output = { + filename: '\'[name].bundle.js\'', + }; + } + if(outputTypeAnswer['outputType'].length) { + this.configuration.config.webpackOptions.output.path = `'${outputTypeAnswer['outputType']}'`; + } else { + this.configuration.config.webpackOptions.output.path = '\'./dist\''; + } + }).then( () => { + this.prompt([ + Confirm('prodConfirm', 'Are you going to use this in production?') + ]).then( (prodAnswer) => { + if(prodAnswer['prodConfirm'] === true) { + this.isProd = true; + } else { + this.isProd = false; + } + }).then( () => { + this.prompt([ + Confirm('babelConfirm', 'Will you be using ES2015?') + ]).then( (ans) => { + if(ans['babelConfirm'] === true) { + this.configuration.config.webpackOptions.module.rules.push(getBabelPlugin()); + this.npmInstalls = ['babel-loader', 'babel-core', 'babel-preset-env']; + } + }).then( () => { + this.prompt([ + RawList( + 'stylingType', + 'Will you use one of the below CSS solutions?', + ['SASS', 'LESS', 'CSS', 'PostCSS', 'No'] + ) + ]).then( (stylingAnswer) => { + if(!this.isProd) { + ExtractUseProps = []; + } + if(stylingAnswer['stylingType'] === 'SASS') { + this.npmInstalls.push( + 'sass-loader', 'node-sass', + 'style-loader', 'css-loader' + ); + regExpForStyles = new RegExp(/\.(scss|css)$/); + if(this.isProd) { + ExtractUseProps = `use: [{ + loader: 'css-loader', + options: { + sourceMap: true + } + }, { + loader: 'sass-loader', + options: { + sourceMap: true + } + }], + fallback: 'style-loader'`; + } else { + ExtractUseProps.push({ + loader: '\'style-loader\'' + }, { + loader: '\'css-loader\'' + }, { + loader: '\'sass-loader\'' + }); + } + } + else if(stylingAnswer['stylingType'] === 'LESS') { + regExpForStyles = new RegExp(/\.(less|css)$/); + this.npmInstalls.push( + 'less-loader', 'less', + 'style-loader', 'css-loader' + ); + if(this.isProd) { + ExtractUseProps = ` + use: [{ + loader: 'css-loader', + options: { + sourceMap: true + } + }, { + loader: 'less-loader', + options: { + sourceMap: true + } + }], + fallback: 'style-loader'`; + } else { + ExtractUseProps.push({ + loader: '\'css-loader\'', + options: { + sourceMap: true + } + }, { + loader: '\'less-loader\'', + options: { + sourceMap: true + } + }); + } + } + else if(stylingAnswer['stylingType'] === 'PostCSS') { + this.configuration.config.topScope.push( + tooltip.postcss(), + 'const autoprefixer = require(\'autoprefixer\');', + 'const precss = require(\'precss\');', + '\n' + ); + this.npmInstalls.push( + 'style-loader', 'css-loader', + 'postcss-loader', 'precss', + 'autoprefixer' + ); + regExpForStyles = new RegExp(/\.css$/); + if(this.isProd) { + ExtractUseProps = ` + use: [{ + loader: 'style-loader' + },{ + loader: 'css-loader', + options: { + sourceMap: true, + importLoaders: 1 + } + }, { + loader: 'postcss-loader', + options: { + plugins: function () { + return [ + precss, + autoprefixer + ]; + } + } + }], + fallback: 'style-loader'`; + } else { + ExtractUseProps.push({ + loader: '\'style-loader\'' + },{ + loader: '\'css-loader\'', + options: { + sourceMap: true, + importLoaders: 1 + } + }, { + loader: '\'postcss-loader\'', + options: { + plugins: `function () { + return [ + precss, + autoprefixer + ]; + }` + } + }); + } + } + else if(stylingAnswer['stylingType'] === 'CSS') { + this.npmInstalls.push('style-loader', 'css-loader'); + regExpForStyles = new RegExp(/\.css$/); + if(this.isProd) { + ExtractUseProps = `use: [{ + loader: 'css-loader', + options: { + sourceMap: true + } + }], + fallback: 'style-loader'`; + } else { + ExtractUseProps.push({ + loader: '\'css-loader\'', + options: { + sourceMap: true + } + }); + } + } + else { + regExpForStyles = null; + } + }).then( () => { + // Ask if the user wants to use extractPlugin + this.prompt([ + Input( + 'extractPlugin', + 'If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)' + ) + ]).then( (extractAnswer) => { + if(regExpForStyles) { + if(this.isProd) { + + this.configuration.config.topScope.push(tooltip.cssPlugin()); + this.npmInstalls.push('extract-text-webpack-plugin'); + if(extractAnswer['extractPlugin'].length !== 0) { + this.configuration.config.webpackOptions.plugins.push( + 'new ExtractTextPlugin(\'' + + extractAnswer['extractPlugin'] + + '.[contentHash].css\')' + ); + } else { + this.configuration.config.webpackOptions.plugins.push( + 'new ExtractTextPlugin(\'' + + 'style.css\')' + ); + } + const moduleRulesObj = { + test: regExpForStyles, + use: `ExtractTextPlugin.extract({ + ${ExtractUseProps} + })` + }; + this.configuration.config.webpackOptions.module.rules.push( + moduleRulesObj + ); + this.configuration.config.topScope.push( + 'const ExtractTextPlugin = require(\'extract-text-webpack-plugin\');', + '\n' + ); + } else { + const moduleRulesObj = { + test: regExpForStyles, + use: ExtractUseProps + }; + this.configuration.config.webpackOptions.module.rules.push( + moduleRulesObj + ); + } + } + }).then( () => { + if(!this.configuration.config.webpackOptions.entry.length) { + oneOrMoreEntries.forEach( (prop) => { + this.configuration.config.webpackOptions.plugins.push( + createCommonsChunkPlugin(prop) + ); + }); + } + done(); + }); + }); + }); + }); + }); }); + }); } - config() {} - childDependencies() { - this.configuration.childDependencies = ['webpack-addons-preact']; + installPlugins() { + let asyncNamePrompt = this.async(); + let defaultName = this.isProd ? 'prod' : 'config'; + this.prompt([ + Input('nameType', `Name your \'webpack.[name].js?\' [default: \'${defaultName}\']:`) + ]).then( (nameAnswer) => { + if(nameAnswer['nameType'].length) { + this.configuration.config.configName = nameAnswer['nameType']; + } else { + this.configuration.config.configName = defaultName; + } + }).then( () => { + asyncNamePrompt(); + this.npmInstall(this.npmInstalls, { 'save-dev': true }); + }); } - inject() {} }; diff --git a/lib/transformations/__snapshots__/utils.test.js.snap b/lib/transformations/__snapshots__/utils.test.js.snap index 7a30f2983a4..9ebb6a83c1f 100644 --- a/lib/transformations/__snapshots__/utils.test.js.snap +++ b/lib/transformations/__snapshots__/utils.test.js.snap @@ -1,9 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`utils checkIfExistsAndAddValue should create new prop if none exist 1`] = ` +" + module.exports = { + entry: 'index.js', + externals: \\"React\\" + } + " +`; + +exports[`utils checkIfExistsAndAddValue should override prop if it exists 1`] = ` +" + module.exports = { + entry: \\"app.js\\" + } + " +`; + +exports[`utils createArrayWithChildren should add all children of an array to a new one with a supplied key 1`] = `"myVeryOwnKey: ['hello', world]"`; + +exports[`utils createArrayWithChildren should find an prop that matches key and create an array with it 1`] = `"react: ['bo']"`; + +exports[`utils createEmptyArrayProperty should create an array with no properties 1`] = `"its-lit: []"`; + +exports[`utils createExternalRegExp should create an regExp property that has been parsed by jscodeshift 1`] = `"\\"\\\\t\\""`; + +exports[`utils createIdentifierOrLiteral should create basic literal 1`] = `"'stringLiteral'"`; + +exports[`utils createIdentifierOrLiteral should create boolean 1`] = `"true"`; + exports[`utils createLiteral should create basic literal 1`] = `"\\"stringLiteral\\""`; exports[`utils createLiteral should create boolean 1`] = `"true"`; +exports[`utils createObjectWithSuppliedProperty should create an object with a property supplied by us 1`] = `"its-lit: {}"`; + exports[`utils createOrUpdatePluginByName should add an object as an argument 1`] = ` "[new Plugin({ \\"foo\\": true @@ -57,3 +88,43 @@ exports[`utils createProperty should create properties for non-literal keys 1`] `; exports[`utils getRequire should create a require statement 1`] = `"const filesys = require(\\"fs\\");"`; + +exports[`utils isAssignment should allow custom transform functions instead of singularProperty 1`] = ` +"module.exports = { + plugins: [one, two, three] +}" +`; + +exports[`utils isAssignment should invoke a callback if parent type is AssignmentExpression 1`] = ` +"module.exports = { + context: Heyho +}" +`; + +exports[`utils loopThroughObjects Use recursion and add elements to an node 1`] = ` +"module.exports = { + hello: { + webpack: cli + } +}" +`; + +exports[`utils pushCreateProperty should create an object or property and push the value to a node 1`] = ` +"module.exports = { + pushMe: { + just: pushed + } + }" +`; + +exports[`utils pushObjectKeys should push object to an node using Object.keys 1`] = ` +"module.exports = { + pushMe: { + hello: { + world: { + its: 'great' + } + } + } + }" +`; diff --git a/lib/transformations/defineTest.js b/lib/transformations/defineTest.js index 36a1017765e..2824337e41e 100644 --- a/lib/transformations/defineTest.js +++ b/lib/transformations/defineTest.js @@ -22,11 +22,10 @@ const path = require('path'); * - Test data should be located in a directory called __testfixtures__ * alongside the transform and __tests__ directory. */ -function runSingleTansform(dirName, transformName, testFilePrefix) { +function runSingleTansform(dirName, transformName, testFilePrefix, initOptions) { if (!testFilePrefix) { testFilePrefix = transformName; } - const fixtureDir = path.join(dirName, '__testfixtures__'); const inputPath = path.join(fixtureDir, testFilePrefix + '.input.js'); const source = fs.readFileSync(inputPath, 'utf8'); @@ -42,6 +41,9 @@ function runSingleTansform(dirName, transformName, testFilePrefix) { jscodeshift = jscodeshift.withParser(module.parser); } const ast = jscodeshift(source); + if (initOptions || typeof(initOptions) === 'boolean') { + return transform(jscodeshift, ast, initOptions).toSource({ quote: 'single' }); + } return transform(jscodeshift, ast, source).toSource({ quote: 'single' }); } @@ -49,13 +51,13 @@ function runSingleTansform(dirName, transformName, testFilePrefix) { * Handles some boilerplate around defining a simple jest/Jasmine test for a * jscodeshift transform. */ -function defineTest(dirName, transformName, testFilePrefix) { +function defineTest(dirName, transformName, testFilePrefix, type) { const testName = testFilePrefix ? `transforms correctly using "${testFilePrefix}" data` : 'transforms correctly'; describe(transformName, () => { it(testName, () => { - const output = runSingleTansform(dirName, transformName, testFilePrefix); + const output = runSingleTansform(dirName, transformName, testFilePrefix, type); expect(output).toMatchSnapshot(); }); }); diff --git a/lib/transformations/utils.js b/lib/transformations/utils.js index 428d0e315ee..73623e70276 100644 --- a/lib/transformations/utils.js +++ b/lib/transformations/utils.js @@ -111,6 +111,46 @@ function createLiteral(j, val) { return j.literal(literalVal); } +/* + * @function createIdentifierOrLiteral + * + * Creates an appropriate identifier or literal property + * + * @param j — jscodeshift API + * @param { string | boolean | number } val + * @returns { Node } + * */ + +function createIdentifierOrLiteral(j, val) { + let literalVal = val; + // We'll need String to native type conversions + if (typeof val === 'string' || val.__paths) { + // 'true' => true + if (val === 'true') { + literalVal = true; + return j.literal(literalVal); + } + // 'false' => false + if (val === 'false') { + literalVal = false; + return j.literal(literalVal); + } + // '1' => 1 + if (!isNaN(Number(val))) { + literalVal = Number(val); + return j.literal(literalVal); + } + + if(val.__paths) { + return createExternalRegExp(j, val); + } + // Use identifier instead + else { + return j.identifier(literalVal); + } + } + return j.literal(literalVal); +} /* * @function createOrUpdatePluginByName * @@ -246,6 +286,222 @@ function getRequire(j, constName, packagePath) { ]); } +/* +* @function checkIfExistsAndAddValue +* +* If a prop exists, it overrides it, else it creates a new one +* @param j — jscodeshift API +* @param { Node } node - objectexpression to check +* @param { string } key - Key of the property +* @param { string } value - computed value of the property +* @returns - nothing +*/ + +function checkIfExistsAndAddValue(j, node, key, value) { + const existingProp = node.value.properties.filter(prop => prop.key.name === key); + let prop; + if (existingProp.length > 0){ + prop = existingProp[0]; + prop.value = value; + } else { + prop = j.property('init', j.identifier(key), value); + node.value.properties.push(prop); + } +} + +/* +* @function createEmptyArrayProperty +* +* Creates an empty array +* @param j — jscodeshift API +* @param { String } key - st name +* @returns - { Array } arr - An empty array +*/ +function createEmptyArrayProperty(j, key) { + return j.property('init', j.identifier(key), j.arrayExpression([])); +} + +/* +* @function createArrayWithChildren +* +* Creates an array and iterates on an object with properties +* @param j — jscodeshift API +* @param { String } key - object name +* @param { string } subProps - computed value of the property +* @returns - { Array } arr - An array with the object properties +*/ + +function createArrayWithChildren(j, key, subProps, shouldDropKeys) { + let arr = createEmptyArrayProperty(j, key); + if(shouldDropKeys) { + subProps.forEach( (subProperty) => { + let objectOfArray = j.objectExpression([]); + if(typeof(subProperty) !== 'string') { + loopThroughObjects(j, objectOfArray, subProperty); + arr.value.elements.push(objectOfArray); + } else { + return arr.value.elements.push(createIdentifierOrLiteral(j, subProperty)); + } + }); + } else { + Object.keys(subProps[key]).forEach( (subProperty) => { + arr.value.elements.push(createIdentifierOrLiteral(j, subProps[key][subProperty])); + }); + } + return arr; +} + +/* +* @function loopThroughObjects +* +* Loops through an object and adds property to an object with no identifier +* @param j — jscodeshift API +* @param { Node } p - node to add value to +* @param { Object } obj - Object to loop through +* @returns - { Function|Node } - Either pushes the node, or reruns until +* nothing is left +*/ + +function loopThroughObjects(j, p, obj) { + Object.keys(obj).forEach( (prop) => { + if(prop.indexOf('inject') >= 0) { + return p.properties.push(createIdentifierOrLiteral(j, obj[prop])); + } + // eslint-disable-next-line no-irregular-whitespace + if(typeof(obj[prop]) !== 'object' || obj[prop] instanceof RegExp) { + p.properties.push(createObjectWithSuppliedProperty( + j, prop, createIdentifierOrLiteral(j, obj[prop]) + )); + } else if(Array.isArray(obj[prop])) { + let arrayProp = createArrayWithChildren(j, prop, obj[prop], true); + p.properties.push(arrayProp); + } else { + let objectBlock = j.objectExpression([]); + let propertyOfBlock = createObjectWithSuppliedProperty(j, prop, objectBlock); + loopThroughObjects(j, objectBlock, obj[prop]); + p.properties.push(propertyOfBlock); + } + }); +} + +/* +* @function createObjectWithSuppliedProperty +* +* Creates an object with an supplied property as parameter +* @param j — jscodeshift API +* @param { String } key - object name +* @param { Node } prop - property to be added +* @returns - { Node } - An property with the supplied property +*/ + +function createObjectWithSuppliedProperty(j, key, prop) { + return j.property('init', j.identifier(key), prop); +} + +/* +* @function createExternalRegExp +* +* Finds a regexp property with an already parsed AST from the user +* @param j — jscodeshift API +* @param { String } prop - property to find the value at +* @returns - { Node } - A literal node with the found regexp +*/ + +function createExternalRegExp(j, prop) { + let regExpProp = prop.__paths[0].value.program.body[0].expression; + return j.literal(regExpProp.value); +} + +/* +* @function pushCreateProperty +* +* Creates a property and pushes the value to a node +* @param j — jscodeshift API +* @param { Node } p - Node to push against +* @param { String } key - key used as identifier +* @param { String } val - property value +* @returns - { Node } - Returns node the pushed property +*/ + +function pushCreateProperty(j, p, key, val) { + let property; + if(val.hasOwnProperty('type')) { + property = val; + } else { + property = createIdentifierOrLiteral(j, val); + } + return p.value.properties.push( + createObjectWithSuppliedProperty(j, key, property) + ); +} + +/* +* @function pushObjectKeys +* +* @param j — jscodeshift API +* @param { Node } p - path to push +* @param { Object } webpackProperties - The object to loop over +* @param { String } name - Key that will be the identifier we find and add values to +* @returns - { Node/Function } Returns a function that will push a node if +*subProperty is an array, else it will invoke a function that will push a single node +*/ + +function pushObjectKeys(j, p, webpackProperties, name) { + p.value.properties.filter(n => + (safeTraverse(n, ['key', 'name']) === name) + ).forEach( (prop) => { + Object.keys(webpackProperties).forEach( (webpackProp) => { + if(webpackProp.indexOf('inject') >= 0) { + return prop.value.properties.push(createIdentifierOrLiteral(j, webpackProperties[webpackProp])); + } + else if(Array.isArray(webpackProperties[webpackProp])) { + const propArray = createArrayWithChildren( + j, webpackProp, webpackProperties[webpackProp], true + ); + return prop.value.properties.push(propArray); + } + else if(typeof(webpackProperties[webpackProp]) !== 'object' || webpackProperties[webpackProp].__paths || webpackProperties[webpackProp] instanceof RegExp) { + return pushCreateProperty( + j, prop, webpackProp, webpackProperties[webpackProp] + ); + } else { + pushCreateProperty( + j, prop, webpackProp, j.objectExpression([]) + ); + return pushObjectKeys( + j, prop, webpackProperties[webpackProp], webpackProp + ); + } + }); + }); +} + +/* +* @function isAssignment +* +* Checks if we are at the correct node and later invokes a callback +* for the transforms to either use their own transform function or +* use pushCreateProperty if the transform doesn't expect any properties +* @param j — jscodeshift API +* @param { Node } p - Node to push against +* @param { Function } cb - callback to be invoked +* @param { String } identifier - key to use as property +* @param { Object } property - WebpackOptions that later will be converted via +* pushCreateProperty via WebpackOptions[identifier] +* @returns - { Function } cb - Returns the callback and pushes a new node +*/ + +function isAssignment(j, p, cb, identifier, property) { + if(p.parent.value.type === 'AssignmentExpression') { + if(j) { + return cb(j, p, identifier, property); + } + else { + return cb(p); + } + } +} + module.exports = { safeTraverse, createProperty, @@ -255,6 +511,16 @@ module.exports = { findVariableToPlugin, isType, createLiteral, + createIdentifierOrLiteral, findObjWithOneOfKeys, - getRequire + getRequire, + checkIfExistsAndAddValue, + createArrayWithChildren, + createEmptyArrayProperty, + createObjectWithSuppliedProperty, + createExternalRegExp, + pushCreateProperty, + pushObjectKeys, + isAssignment, + loopThroughObjects }; diff --git a/lib/transformations/utils.test.js b/lib/transformations/utils.test.js index 73a8c93a9fc..c0adae775d3 100644 --- a/lib/transformations/utils.test.js +++ b/lib/transformations/utils.test.js @@ -143,6 +143,18 @@ var a = { plugs: [] } const literal = utils.createLiteral(j, 'true'); expect(j(literal).toSource()).toMatchSnapshot(); }); + + }); + + describe('createIdentifierOrLiteral', () => { + it('should create basic literal', () => { + const literal = utils.createIdentifierOrLiteral(j, '\'stringLiteral\''); + expect(j(literal).toSource()).toMatchSnapshot(); + }); + it('should create boolean', () => { + const literal = utils.createLiteral(j, 'true'); + expect(j(literal).toSource()).toMatchSnapshot(); + }); }); describe('findObjWithOneOfKeys', () => { @@ -164,4 +176,140 @@ var a = { plugs: [] } expect(j(require).toSource()).toMatchSnapshot(); }); }); + + describe('checkIfExistsAndAddValue', () => { + it('should create new prop if none exist', () => { + const ast = j(` + module.exports = { + entry: 'index.js' + } + `); + ast.find(j.ObjectExpression).forEach(node => utils.checkIfExistsAndAddValue(j, node, 'externals', j.literal('React'))); + expect(ast.toSource()).toMatchSnapshot(); + }); + it('should override prop if it exists', () => { + const ast = j(` + module.exports = { + entry: 'index.js' + } + `); + ast.find(j.ObjectExpression).forEach(node => utils.checkIfExistsAndAddValue(j, node, 'entry', j.literal('app.js'))); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('createArrayWithChildren', () => { + it('should find an prop that matches key and create an array with it', () => { + const ast = j('{}'); + let key = 'react'; + let reactArr = { + react: [ '\'bo\''] + }; + const arr = utils.createArrayWithChildren(j, key, reactArr, false); + ast.find(j.Program).forEach(node => j(node).replaceWith(arr)); + expect(ast.toSource()).toMatchSnapshot(); + }); + it('should add all children of an array to a new one with a supplied key', () => { + const ast = j('{}'); + let key = 'myVeryOwnKey'; + let helloWorldArray = [ '\'hello\'', 'world']; + const arr = utils.createArrayWithChildren(j, key, helloWorldArray, true); + ast.find(j.Program).forEach(node => j(node).replaceWith(arr)); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('createEmptyArrayProperty', () => { + it('should create an array with no properties', () => { + const ast = j('{}'); + const arr = utils.createEmptyArrayProperty(j, 'its-lit'); + ast.find(j.Program).forEach(node => j(node).replaceWith(arr)); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + + describe('createObjectWithSuppliedProperty', () => { + it('should create an object with a property supplied by us', () => { + const ast = j('{}'); + const prop = utils.createObjectWithSuppliedProperty(j, 'its-lit', j.objectExpression([])); + ast.find(j.Program).forEach(node => j(node).replaceWith(prop)); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('createExternalRegExp', () => { + it('should create an regExp property that has been parsed by jscodeshift', () => { + const ast = j('{}'); + const reg = j('\'\t\''); + const prop = utils.createExternalRegExp(j, reg); + ast.find(j.Program).forEach(node => j(node).replaceWith(prop)); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('pushCreateProperty', () => { + it('should create an object or property and push the value to a node', () => { + const ast = j(`module.exports = { + pushMe: {} + }`); + ast.find(j.Identifier).filter(n => n.value.name === 'pushMe').forEach(node => { + const heavyNodeNoSafeTraverse = node.parentPath.value; + utils.pushCreateProperty(j, heavyNodeNoSafeTraverse, 'just', 'pushed'); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('pushObjectKeys', () => { + it('should push object to an node using Object.keys', () => { + const ast = j(`module.exports = { + pushMe: {} + }`); + const webpackProperties = { + hello: { + world: { + its: '\'great\'' + } + } + }; + ast.find(j.ObjectExpression).forEach(node => { + utils.pushObjectKeys(j, node, webpackProperties, 'pushMe'); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('loopThroughObjects', () => { + it('Use recursion and add elements to an node', () => { + const ast = j('module.exports = {}'); + const webpackProperties = { + hello: { + webpack: 'cli' + } + }; + ast.find(j.ObjectExpression).forEach(node => { + return utils.loopThroughObjects(j, node.value, webpackProperties); + }); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); + describe('isAssignment', () => { + it('should invoke a callback if parent type is AssignmentExpression', () => { + const ast = j('module.exports = {}'); + const myObj = 'Heyho'; + const myKey = 'context'; + + ast.find(j.ObjectExpression) + .filter(n => utils.isAssignment(j, n, utils.pushCreateProperty, myKey, myObj)); + expect(ast.toSource()).toMatchSnapshot(); + }); + it('should allow custom transform functions instead of singularProperty', () => { + const ast = j('module.exports = {}'); + + function createPluginsProperty(p) { + const webpackProperties = { + plugins: ['one', 'two', 'three'] + }; + const pluginArray = utils.createArrayWithChildren(j, 'plugins', webpackProperties); + return p.value.properties.push(pluginArray); + } + ast.find(j.ObjectExpression) + .filter(n => utils.isAssignment(null, n, createPluginsProperty)); + expect(ast.toSource()).toMatchSnapshot(); + }); + }); }); diff --git a/lib/utils/npm-exists.spec.js b/lib/utils/npm-exists.spec.js index b192127a4be..be5ba3d7c96 100644 --- a/lib/utils/npm-exists.spec.js +++ b/lib/utils/npm-exists.spec.js @@ -1,16 +1,17 @@ -/* eslint node/no-unsupported-features: 0 */ 'use strict'; const exists = require('./npm-exists'); -describe('exists', () => { +describe('npm-exists', () => { - it('should sucessfully existence of a published module', async () => { - let itExists = await exists('webpack-addons-ylvis'); - expect(itExists).toBe(true); + it('should sucessfully existence of a published module', () => { + exists('webpack-addons-ylvis').then( (status) => { + expect(status).toBe(true); + }); }); - it('should return false for the existence of a fake module', async () => { - let itExists = await exists('webpack-addons-noop'); - expect(itExists).toBe(false); + it('should return false for the existence of a fake module', () => { + exists('webpack-addons-noop').then( (status) => { + expect(status).toBe(false); + }); }); }); diff --git a/lib/utils/npm-packages-exists.js b/lib/utils/npm-packages-exists.js index 8748b1e47ac..020cf0bbd09 100644 --- a/lib/utils/npm-packages-exists.js +++ b/lib/utils/npm-packages-exists.js @@ -6,36 +6,29 @@ const resolvePackages = require('./resolve-packages'); * @function npmPackagesExists * * Loops through an array and checks if a package is registered -* on npm and throws an error if it is not from @checkEachPackage +* on npm and throws an error if it is not. * * @param { Array } pkg - Array of packages to check existence of -* @returns { Array } resolvePackages - Returns an process to install the pkg +* @returns { Array } resolvePackages - Returns an process to install the packages */ -module.exports = function npmPackagesExists(addons) { - return addons.map( pkg => checkEachPackage(pkg)); -}; -/* -* @function checkEachPackage -* -* Checks if a package is registered on npm and throws if it is not -* -* @param { Object } pkg - pkg to check existence of -* @returns { } resolvePackages - Returns an process to install the pkg -*/ - -function checkEachPackage(pkg) { - return npmExists(pkg).then( (moduleExists) => { - if(!moduleExists) { - Error.stackTraceLimit = 0; - throw new TypeError('Package isn\'t registered on npm.'); - } - if (moduleExists) { - return resolvePackages(pkg); - } - }).catch(err => { - console.error(err.stack || err); - process.exit(0); +module.exports = function npmPackagesExists(pkg) { + let acceptedPackages = []; + pkg.forEach( addon => { + npmExists(addon).then( (moduleExists) => { + if(!moduleExists) { + Error.stackTraceLimit = 0; + throw new TypeError('Package isn\'t registered on npm.'); + } + if (moduleExists) { + acceptedPackages.push(addon); + } + }).catch(err => { + console.error(err.stack || err); + process.exit(0); + }).then( () => { + if(acceptedPackages.length === pkg.length) return resolvePackages(acceptedPackages); + }); }); -} +}; diff --git a/lib/utils/resolve-packages.js b/lib/utils/resolve-packages.js index a892cc494d0..eb960e2a4cc 100644 --- a/lib/utils/resolve-packages.js +++ b/lib/utils/resolve-packages.js @@ -1,8 +1,9 @@ -const spawn = require('cross-spawn'); -const creator = require('../creator/index').creator; const path = require('path'); +const fs = require('fs'); const chalk = require('chalk'); - +const spawn = require('cross-spawn'); +const creator = require('../creator/index').creator; +const globalPath = require('global-modules'); /* * @function processPromise * @@ -14,8 +15,11 @@ const chalk = require('chalk'); function processPromise(child) { return new Promise(function(resolve, reject) { //eslint-disable-line - child.addListener('error', reject); - child.addListener('exit', resolve); + if(child.status !== 0) { + reject(); + } else { + resolve(); + } }); } @@ -29,38 +33,49 @@ function processPromise(child) { */ function spawnChild(pkg) { - return spawn('npm', ['install', '--save', pkg], { stdio: 'inherit', customFds: [0, 1, 2] }); - //return spawn('npm', ['install', '--save', pkg]); + const pkgPath = path.resolve(globalPath, pkg); + let installType; + if(fs.existsSync(pkgPath)) { + installType = 'update'; + } else { + installType = 'install'; + } + return spawn.sync('npm', [installType, '-g', pkg], { stdio: 'inherit'}); } /* * @function resolvePackages * -* Resolves the package after it is validated, later sending it to the creator -* to be validated +* Resolves and installs the packages, later sending them to @creator * -* @param { String } pkg - The dependency to be installed -* @returns { } creator - Validates the dependency and builds -* the webpack configuration +* @param { Array } pkg - The dependencies to be installed +* @returns { } creator - Builds +* a webpack configuration through yeoman or throws an error */ module.exports = function resolvePackages(pkg) { Error.stackTraceLimit = 30; - return processPromise(spawnChild(pkg)).then( () => { - try { - let loc = path.join('..', '..', 'node_modules', pkg); - return creator(loc); - } catch(err) { - console.log('Package wasn\'t validated correctly..'); - console.log('Submit an issue for', pkg, 'if this persists'); + + let packageLocations = []; + + pkg.forEach( (addon) => { + processPromise(spawnChild(addon)).then( () => { + try { + packageLocations.push(path.resolve(globalPath, addon)); + } catch(err) { + console.log('Package wasn\'t validated correctly..'); + console.log('Submit an issue for', pkg, 'if this persists'); + console.log('\nReason: \n'); + console.error(chalk.bold.red(err)); + process.exitCode = 1; + } + }).catch(err => { + console.log('Package Coudln\'t be installed, aborting..'); console.log('\nReason: \n'); console.error(chalk.bold.red(err)); - process.exit(1); - } - }).catch(err => { - console.log('Package Coudln\'t be installed, aborting..'); - console.log('\nReason: \n'); - console.error(chalk.bold.red(err)); - process.exit(1); + process.exitCode = 1; + }).then( () => { + if(packageLocations.length === pkg.length) return creator(packageLocations); + }); }); }; diff --git a/package.json b/package.json index 43f84a140f3..e7b98ddeff3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webpack-cli", - "version": "0.0.1", + "version": "1.0.0", "description": "CLI for webpack & friends", "license": "MIT", "preferGlobal": true, @@ -34,6 +34,7 @@ "diff": "^3.2.0", "enhanced-resolve": "^3.0.2", "fit-commit-js": "^0.3.1", + "global-modules": "^0.2.3", "got": "^6.6.3", "inquirer": "^2.0.0", "interpret": "^1.0.1", @@ -43,12 +44,12 @@ "lodash": "^4.17.4", "p-each-series": "^1.0.0", "p-lazy": "^1.0.0", + "prettier": "^1.2.2", "recast": "git://github.com/kalcifer/recast.git#bug/allowbreak", - "rx": "^4.1.0", + "resolve-cwd": "^2.0.0", "supports-color": "^3.1.2", "webpack": "^2.2.0-rc.0", - "webpack-addons": "0.0.20", - "webpack-addons-ylvis": "0.0.34", + "webpack-addons": "^1.1.2", "yargs": "^6.5.0", "yeoman-environment": "^1.6.6", "yeoman-generator": "git://github.com/ev1stensberg/generator.git#Feature-getArgument"