diff --git a/packages/init/config.ts b/packages/init/config.ts index 7c7cf7bb56b..d17235f93fb 100644 --- a/packages/init/config.ts +++ b/packages/init/config.ts @@ -9,7 +9,7 @@ export class Config { // Data members private configName: string = ""; private topScope: string[] = [""]; - private webpackOptions: IWebpackOptions = {}; + public webpackOptions: IWebpackOptions = {}; private merge: object = {}; private configPath: string = process.cwd(); diff --git a/packages/ui-gui/README.md b/packages/ui-gui/README.md index b9dfd953296..fad7e591235 100644 --- a/packages/ui-gui/README.md +++ b/packages/ui-gui/README.md @@ -4,9 +4,8 @@ GUI for `webpack-cli ui` #### Package Structure (base: `./`) -1. `./`: Backend will serve from root folder +1. `./build`: contains bundled resources and frontend serves from this folder. 2. `./src` : contains source -3. `./build`: contains bundled resources ## CLI Commands ``` bash @@ -21,3 +20,4 @@ npm run build # test the production build locally npm run serve +``` diff --git a/packages/ui-server/.gitignore b/packages/ui-server/.gitignore new file mode 100644 index 00000000000..93a410214a1 --- /dev/null +++ b/packages/ui-server/.gitignore @@ -0,0 +1 @@ +/utils/**.js diff --git a/packages/ui-server/routes/api/_entry.js b/packages/ui-server/routes/api/_entry.js new file mode 100644 index 00000000000..e9f157c40bc --- /dev/null +++ b/packages/ui-server/routes/api/_entry.js @@ -0,0 +1,85 @@ +/* eslint-disable node/no-unpublished-require */ +const InputValidate = require("../../../webpack-scaffold").InputValidate; +const webpackEntryPoint = {}; +function forEachPromise(entries, fn) { + return entries.reduce((promise, prop) => { + const trimmedProp = prop.trim(); + + return promise.then((n) => { + if (n) { + Object.keys(n).forEach((val) => { + if ( + n[val].charAt(0) !== "(" && + n[val].charAt(0) !== "[" && + !n[val].includes("function") && + !n[val].includes("path") && + !n[val].includes("process") + ) { + n[val] = `\'${n[val].replace(/"|'/g, "").concat(".js")}\'`; + } + webpackEntryPoint[val] = n[val]; + }); + } else { + n = {}; + } + return fn(trimmedProp); + }); + }, Promise.resolve()); +} +/** + * Asks for entry points, either if it has multiple or one entry + * @param {Object} questioner Instance of TCP questioner used by init endpoint + * @param {Object} answer Boolean answer to whether mutiple entries are used or not + * @returns {Object} An Object to entry for webpack config + */ +module.exports = function(questioner, answer) { + if (answer.entryType === true) { + return questioner.question({ + action: "question", + question: InputValidate( + "multipleEntries", + "Type the names you want for your modules (entry files), separated by comma [example: app,vendor]" + ), + }).then((entries) => { + return forEachPromise(entries, (entryProp) => { + return questioner.question({ + action: "question", + question: InputValidate( + `${entryProp}`, + `What is the location of "${entryProp}"? [example: ./src/${entryProp}]` + ), + }); + }).then((entryPropAnswer) => { + Object.keys(entryPropAnswer).forEach(val => { + if ( + entryPropAnswer[val].charAt(0) !== "(" && + entryPropAnswer[val].charAt(0) !== "[" && + !entryPropAnswer[val].includes("function") && + !entryPropAnswer[val].includes("path") && + !entryPropAnswer[val].includes("process") + ) { + entryPropAnswer[val] = `\'${entryPropAnswer[val] + .replace(/"|'/g, "") + .concat(".js")}\'`; + } + webpackEntryPoint[val] = entryPropAnswer[val]; + }); + return webpackEntryPoint; + }); + }); + } else { + return questioner.question({ + action: "question", + question: InputValidate( + "singularEntry", + "Which module will be the first to enter the application? [default: ./src/index]" + ) + }).then((singularEntryAnswer) => { + let singularEntry = singularEntryAnswer.singularEntry; + if (singularEntry.indexOf("\"") >= 0) { + singularEntry = singularEntry.replace(/"/g, "'"); + } + return singularEntry; + }); + } +}; diff --git a/packages/ui-server/routes/api/_module.js b/packages/ui-server/routes/api/_module.js new file mode 100644 index 00000000000..25a8b64f7a0 --- /dev/null +++ b/packages/ui-server/routes/api/_module.js @@ -0,0 +1,26 @@ +/** + * + * Returns an module.rule object that has the babel loader if invoked + * + * @returns {Function} A callable function that adds the babel-loader with env preset + */ +module.exports = function() { + return { + include: ["path.resolve(__dirname, 'src')"], + loader: "'babel-loader'", + options: { + plugins: [ + "'syntax-dynamic-import'", + ], + presets: [ + [ + "'env'", + { + "'modules'": false, + }, + ], + ], + }, + test: `${new RegExp(/\.js$/)}`, + }; +}; diff --git a/packages/ui-server/routes/api/_plugins.js b/packages/ui-server/routes/api/_plugins.js new file mode 100644 index 00000000000..243ce68e8f2 --- /dev/null +++ b/packages/ui-server/routes/api/_plugins.js @@ -0,0 +1,7 @@ +/** + * Callable function with the initial plugins + * + * @returns {Function} An function that returns an array + * that consists of the uglify plugin + */ +module.exports = () => ["new UglifyJSPlugin()"]; diff --git a/packages/ui-server/routes/api/_tooltip.js b/packages/ui-server/routes/api/_tooltip.js new file mode 100644 index 00000000000..c11131ab457 --- /dev/null +++ b/packages/ui-server/routes/api/_tooltip.js @@ -0,0 +1,50 @@ +/** + * + * Tooltip object that consists of tooltips for various of + * features + * + * @returns {Object} An Object that consists of tooltip methods to be invoked + */ +module.exports = { + cssPlugin: () => `/* +* We've enabled MiniCssExtractPlugin for you. This allows your app to +* use css modules that will be moved into a separate CSS file instead of inside +* one of your module entries! +* +* https://github.com/webpack-contrib/mini-css-extract-plugin +* +*/`, + splitChunks: () => `/* +* SplitChunksPlugin is enabled by default and replaced +* deprecated CommonsChunkPlugin. It automatically identifies modules which +* should be splitted of chunk by heuristics using module duplication count and +* module category (i. e. node_modules). And splits the chunks… +* +* It is safe to remove "splitChunks" from the generated configuration +* and was added as an educational example. +* +* https://webpack.js.org/plugins/split-chunks-plugin/ +* +*/`, + postcss: () => `/* +* 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 +* +*/`, + uglify: () => `/* +* 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 +* +*/` +}; diff --git a/packages/ui-server/routes/api/init.js b/packages/ui-server/routes/api/init.js index 7f89a4d0585..9f97a06f6ff 100644 --- a/packages/ui-server/routes/api/init.js +++ b/packages/ui-server/routes/api/init.js @@ -7,14 +7,32 @@ const router = express.Router(); router.get("/", function(req, res, next) { + // Configuration Manager const Config = require("../../../init/config").Config; + + // Scaffolder const scaffoldProject = require("../../../init").scaffoldProject; + + // Questioner const Questioner = require("../../utils/questioner").default; + // Utilities + const entryQuestions = require("./_entry"); + const getBabelPlugin = require("./_module"); + const getDefaultPlugins = require("./_plugins"); + const tooltip = require("./_tooltip"); + + // Question types used + const Confirm = require("../../../webpack-scaffold").Confirm; + const Input = require("../../../webpack-scaffold").Input; + const List = require("../../../webpack-scaffold").List; + let config = new Config(); let questioner = new Questioner(); - const dependencies = [ + config.setConfigPath(process.env.CWD); + + let dependencies = [ "webpack", "webpack-cli", "uglifyjs-webpack-plugin", @@ -31,10 +49,346 @@ router.get("/", function(req, res, next) { rules: [] }); + let usingDefaults = false; + let isProd = false; + let ExtractUseProps = []; + let regExpForStyles; + questioner.start({ action: "question", - question: "A dummy question for reference" - }).then((data) => { + question: Confirm("entryType", "Will your application have multiple bundles?"), + }).then((entryTypeAnswer) => { + return entryQuestions(questioner, entryTypeAnswer); + }).then((entryOptions) => { + if (entryOptions !== "\"\"") { + config.setWebpackOption("entry", entryOptions); + } else { + usingDefaults = true; + } + return questioner.question({ + action: "question", + question: Input( + "outputType", + "Which folder will your generated bundles be in? [default: dist]:" + ), + }); + }).then((outputTypeAnswer) => { + if ( + !config.webpackOptions.entry && + !usingDefaults + ) { + config.webpackOptions.output = { + chunkFilename: "'[name].[chunkhash].js'", + filename: "'[name].[chunkhash].js'", + }; + } else if (!usingDefaults) { + config.webpackOptions.output = { + filename: "'[name].[chunkhash].js'", + }; + } + + let outputPath = "dist"; + if (outputTypeAnswer.outputType.length) { + outputPath = outputTypeAnswer.outputType; + } + if (!usingDefaults) { + config.webpackOptions.output.path = `path.resolve(__dirname, '${outputPath}')`; + } + }).then((_) => { + isProd = usingDefaults ? true : false; + config.setConfigName(isProd ? "prod" : "dev"); + config.webpackOptions.mode = isProd + ? "'production'" + : "'development'"; + config.webpackOptions.plugins = isProd ? [] : getDefaultPlugins(); + + return questioner.question([ + { + action: "question", + question: Confirm("babelConfirm", "Will you be using ES2015?"), + } + ]); + }).then((babelConfirmAnswer) => { + if (babelConfirmAnswer.babelConfirm === true) { + config.webpackOptions.module.rules.push( + getBabelPlugin() + ); + dependencies.push( + "babel-core", + "babel-loader", + "babel-preset-env" + ); + } + }).then((_) => { + return questioner.question([ + { + action: "question", + question: List("stylingType", "Will you use one of the below CSS solutions?", [ + "SASS", + "LESS", + "CSS", + "PostCSS", + "No", + ]), + } + ]); + }).then((stylingTypeAnswer) => { + switch (stylingTypeAnswer.stylingType) { + case "SASS": + dependencies.push( + "sass-loader", + "node-sass", + "style-loader", + "css-loader" + ); + regExpForStyles = `${new RegExp(/\.(scss|css)$/)}`; + if (isProd) { + ExtractUseProps.push( + { + loader: "'css-loader'", + options: { + sourceMap: true, + }, + }, + { + loader: "'sass-loader'", + options: { + sourceMap: true, + }, + } + ); + } else { + ExtractUseProps.push( + { + loader: "'style-loader'", + }, + { + loader: "'css-loader'", + }, + { + loader: "'sass-loader'", + } + ); + } + break; + case "LESS": + regExpForStyles = `${new RegExp(/\.(less|css)$/)}`; + dependencies.push( + "less-loader", + "less", + "style-loader", + "css-loader" + ); + if (isProd) { + ExtractUseProps.push( + { + loader: "'css-loader'", + options: { + sourceMap: true, + }, + }, + { + loader: "'less-loader'", + options: { + sourceMap: true, + }, + } + ); + } else { + ExtractUseProps.push( + { + loader: "'css-loader'", + options: { + sourceMap: true, + }, + }, + { + loader: "'less-loader'", + options: { + sourceMap: true, + }, + } + ); + } + break; + case "PostCSS": + config.pushToTopScope( + tooltip.postcss(), + "const autoprefixer = require('autoprefixer');", + "const precss = require('precss');", + "\n" + ); + dependencies.push( + "style-loader", + "css-loader", + "postcss-loader", + "precss", + "autoprefixer" + ); + regExpForStyles = `${new RegExp(/\.css$/)}`; + if (isProd) { + ExtractUseProps.push( + { + loader: "'css-loader'", + options: { + importLoaders: 1, + sourceMap: true, + }, + }, + { + loader: "'postcss-loader'", + options: { + plugins: `function () { + return [ + precss, + autoprefixer + ]; + }`, + }, + } + ); + } else { + ExtractUseProps.push( + { + loader: "'style-loader'", + }, + { + loader: "'css-loader'", + options: { + importLoaders: 1, + sourceMap: true, + }, + }, + { + loader: "'postcss-loader'", + options: { + plugins: `function () { + return [ + precss, + autoprefixer + ]; + }`, + }, + } + ); + } + break; + case "CSS": + dependencies.push("style-loader", "css-loader"); + regExpForStyles = `${new RegExp(/\.css$/)}`; + if (isProd) { + ExtractUseProps.push({ + loader: "'css-loader'", + options: { + sourceMap: true, + }, + }); + } else { + ExtractUseProps.push( + { + loader: "'style-loader'", + options: { + sourceMap: true, + }, + }, + { + loader: "'css-loader'", + } + ); + } + break; + default: + regExpForStyles = null; + } + }).then((_) => { + if (isProd) { + // Ask if the user wants to use extractPlugin + return questioner.question([ + { + action: "question", + question: Input( + "extractPlugin", + "If you want to bundle your CSS files, what will you name the bundle?" + ), + } + ]); + } + }).then((extractPluginAnswer) => { + if (regExpForStyles) { + if (this.isProd) { + const cssBundleName = extractPluginAnswer.extractPlugin; + config.pushToTopScope(tooltip.cssPlugin()); + dependencies.push("mini-css-extract-plugin"); + + if (cssBundleName.length !== 0) { + config.webpackOptions.plugins.push( + // TODO: use [contenthash] after it is supported + `new MiniCssExtractPlugin({ filename:'${cssBundleName}.[chunkhash].css' })` + ); + } else { + config.webpackOptions.plugins.push( + "new MiniCssExtractPlugin({ filename:'style.css' })" + ); + } + + ExtractUseProps.unshift({ + loader: "MiniCssExtractPlugin.loader", + }); + + const moduleRulesObj = { + test: regExpForStyles, + use: ExtractUseProps, + }; + + config.webpackOptions.module.rules.push( + moduleRulesObj + ); + config.pushToTopScope( + "const MiniCssExtractPlugin = require('mini-css-extract-plugin');", + "\n" + ); + } else { + const moduleRulesObj = { + test: regExpForStyles, + use: ExtractUseProps, + }; + + config.webpackOptions.module.rules.push( + moduleRulesObj + ); + } + } + // add splitChunks options for transparency + // defaults coming from: https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks + config.pushToTopScope(tooltip.splitChunks()); + config.webpackOptions.optimization = { + splitChunks: { + cacheGroups: { + vendors: { + priority: -10, + test: "/[\\\\/]node_modules[\\\\/]/", + }, + }, + chunks: "'async'", + minChunks: 1, + minSize: 30000, + // for production name is recommended to be off + name: !isProd, + }, + }; + }).then((_) => { + // installPlugins() + if (isProd) { + dependencies = dependencies.filter( + (p) => p !== "uglifyjs-webpack-plugin" + ); + } else { + config.pushToTopScope( + tooltip.uglify(), + "const UglifyJSPlugin = require('uglifyjs-webpack-plugin');", + "\n" + ); + } scaffoldProject(dependencies,config.exportConfig()); return questioner.question({ action: "exit" }); }); diff --git a/packages/ui/bin/www.js b/packages/ui/bin/www.js index 14e03f9c1d0..e33b85e1a33 100644 --- a/packages/ui/bin/www.js +++ b/packages/ui/bin/www.js @@ -4,7 +4,11 @@ */ const path = require("path"); -const app = require("../../ui-server")(path.join(__dirname, "../../ui-gui/build")); // eslint-disable-line +const fs = require("fs"); +const STATIC = fs.existsSync(path.join(__dirname, "../../ui-gui/build")) + ? path.join(__dirname, "../../ui-gui/build") + : "../static_fallback"; +const app = require("../../ui-server")(STATIC); // eslint-disable-line const debug = require("debug")("ui:server"); const http = require("http"); diff --git a/packages/ui/static_fallback/index.html b/packages/ui/static_fallback/index.html new file mode 100644 index 00000000000..0e72f19028c --- /dev/null +++ b/packages/ui/static_fallback/index.html @@ -0,0 +1,12 @@ + + +
+ + + +