diff --git a/grunt/optimize-build.js b/grunt/optimize-build.js index 2f3492298..c09f13ed1 100644 --- a/grunt/optimize-build.js +++ b/grunt/optimize-build.js @@ -1,170 +1,101 @@ -module.exports = function(grunt) { - var PATHS = { - 'backbone-validation': 'empty:', - 'backbone.stickit': 'empty:', - 'backbone.wreqr': 'empty:', - backbone: 'empty:', - bootstrap: 'empty:', - bowser: 'empty:', - classnames: 'empty:', - clipboard: 'empty:', - 'create-react-class': 'empty:', - 'd3-cloud': 'empty:', - d3: 'empty:', - filesaver: 'empty:', - 'google-recaptcha': 'empty:', - 'google-analytics': 'empty:', - jquery: 'empty:', - 'jquery-ui': 'empty:', - 'jquery-querybuilder': 'empty:', - jsonpath: 'empty:', - marionette: 'empty:', - mathjax: 'empty:', - moment: 'empty:', - 'persist-js': 'empty:', - 'react-bootstrap': 'empty:', - 'react-dom': 'empty:', - 'prop-types': 'empty:', - 'react-redux': 'empty:', - react: 'empty:', - 'redux-thunk': 'empty:', - redux: 'empty:', - requirejs: 'empty:', - reselect: 'empty:', - select2: 'empty:', - sprintf: 'empty:', - underscore: 'empty:', - 'discovery.vars': 'empty:', - yup: 'empty:', - 'react-hook-form': 'empty:', - 'react-flexview': 'empty:', - 'styled-components': 'empty:', - 'react-is': 'empty:', - 'react-data-table-component': 'empty:', - 'react-window': 'empty:', - 'react-async': 'empty:', - diff: 'empty:', - hotkeys: 'empty:', - 'react-transition-group': 'empty:', - 'regenerator-runtime': 'empty:', - '@hookform/resolvers': 'empty:', - 'react-aria-menubutton': 'empty:', - }; - - var baseConfig = { - waitSeconds: 7, - logLevel: 1, - baseUrl: 'dist', - optimize: 'none', - mainConfigFile: 'dist/config/discovery.config.js', - deps: [], - findNestedDependencies: false, - create: true, - paths: PATHS, - stubModules: ['es6', 'babel'], - }; - - grunt.registerMultiTask( - 'optimize-build', - 'Generate config and optimize build', - function() { - var _ = require('lodash'); - var path = require('path'); - var fullConfig = {}; - - // callback called with URL and name pulled from folder - var getByGlob = function(globs, cb) { - var urls = []; - grunt.file.expand({ filter: 'isFile' }, globs).forEach(function(url) { - var parts = url.split(path.sep); - if (/jsx\.js$/.test(url)) { - url = url.replace(/^/, 'es6!'); - } - url = url.replace('src/', '').replace(/\.js$/, ''); - urls.push(url); - cb && cb(url, parts[3]); - }); - return urls; - }; - - var writeOutConfig = function(config, done) { - var output = ` - // GENERATED FILE (edits will be overwritten) - module.exports = ${JSON.stringify(config, null, 2)}; - `; - fullConfig = config; - grunt.file.write(path.resolve(__dirname, 'requirejs.js'), output); - (function check(i) { - if ( - i >= 30 || - grunt.file.exists(path.resolve(__dirname, 'requirejs.js')) - ) { - grunt.log.writeln('Configuration Generated...'); - return done(); - } - setTimeout(check, 500, ++i); - })(0); - }; - - grunt.registerTask('generateConfig', function() { - var config = {}; - var done = this.async(); - var addConfig = function(name, cfg) { - config[name] = {}; - config[name].options = _.extend( - {}, - baseConfig, - { - name: `${name}.bundle`, - out: `dist/config/${name}.bundle.js`, - }, - cfg - ); - }; - - const globFiles = (arrOfPaths) => { - const filter = (path) => { - // filter out all es6 modules, for now - return !/\.jsx\.js$/.test(path); - }; +/* eslint-disable global-require */ - return grunt.file - .expand({ cwd: 'dist', filter }, arrOfPaths) - .map((path) => { - return path.replace(/\.js$/, ''); - }); - }; - - addConfig('landing-page', { - include: [ - ...globFiles([ - 'js/react/Recommender/**/*.js', - 'js/widgets/paper_search_form/**/*.js', - 'js/widgets/search_bar/**/*.js', - 'js/widgets/facet/**/*.js', - 'js/widgets/alerts/**/*.js', - ]), +module.exports = function(grunt) { + const buildConfig = { + options: { + paths: { + 'backbone-validation': 'empty:', + 'backbone.stickit': 'empty:', + 'backbone.wreqr': 'empty:', + backbone: 'empty:', + bootstrap: 'empty:', + bowser: 'empty:', + classnames: 'empty:', + clipboard: 'empty:', + 'create-react-class': 'empty:', + 'd3-cloud': 'empty:', + d3: 'empty:', + filesaver: 'empty:', + 'google-recaptcha': 'empty:', + 'google-analytics': 'empty:', + jquery: 'empty:', + 'jquery-ui': 'empty:', + 'jquery-querybuilder': 'empty:', + jsonpath: 'empty:', + marionette: 'empty:', + mathjax: 'empty:', + moment: 'empty:', + 'persist-js': 'empty:', + 'react-bootstrap': 'empty:', + 'react-dom': 'empty:', + 'prop-types': 'empty:', + 'react-redux': 'empty:', + react: 'empty:', + 'redux-thunk': 'empty:', + redux: 'empty:', + requirejs: 'empty:', + reselect: 'empty:', + select2: 'empty:', + sprintf: 'empty:', + underscore: 'empty:', + 'discovery.vars': 'empty:', + yup: 'empty:', + 'react-hook-form': 'empty:', + 'react-flexview': 'empty:', + 'styled-components': 'empty:', + 'react-is': 'empty:', + 'react-data-table-component': 'empty:', + 'react-window': 'empty:', + 'react-async': 'empty:', + diff: 'empty:', + hotkeys: 'empty:', + 'react-transition-group': 'empty:', + 'regenerator-runtime': 'empty:', + '@hookform/resolvers': 'empty:', + 'react-aria-menubutton': 'empty:', + }, + stubModules: ['es6', 'babel'], + }, + release: { + options: { + bundles: { + 'landing-page': [ + 'js/wraps/*_facet.js', + 'js/widgets/metrics/**/*.js', + 'js/widgets/list_of_things/**/*.js', + 'js/widgets/search_bar/**/*.js', + 'js/widgets/facet/**/*.js', + 'js/widgets/alerts/**/*.js', + 'js/widgets/abstract/widget', + 'js/widgets/api_query/widget', 'js/widgets/base/base_widget', - 'js/widgets/classic_form/widget', + 'js/widgets/bubble_chart/widget', + 'js/widgets/citation_helper/widget', + 'js/widgets/dropdown-menu/widget', + 'js/widgets/filter_visualizer/widget', 'js/widgets/footer/widget', 'js/widgets/navbar/widget', + 'js/widgets/network_vis/network_widget', + 'js/widgets/query_info/query_info_widget', + 'js/widgets/results/widget', + 'js/widgets/tabs/tabs_widget', 'js/widgets/widget_states', + 'js/widgets/wordcloud/widget', 'js/wraps/alerts_mediator', 'js/wraps/discovery_mediator', - 'js/wraps/landing_page_manager/landing_page_manager', + 'js/wraps/export_dropdown', + 'js/wraps/graph_tabs', + 'js/wraps/results_page_manager', + 'js/wraps/visualization_dropdown', + 'libs/select2/matcher', ], - }); - - addConfig('search-page', { - include: [ - ...globFiles([ - 'js/wraps/*_facet.js', - 'js/widgets/metrics/**/*.js', - 'js/widgets/list_of_things/**/*.js', - 'js/widgets/search_bar/**/*.js', - 'js/widgets/facet/**/*.js', - 'js/widgets/alerts/**/*.js', - ]), + 'search-page': [ + 'js/wraps/*_facet.js', + 'js/widgets/metrics/**/*.js', + 'js/widgets/list_of_things/**/*.js', + 'js/widgets/search_bar/**/*.js', + 'js/widgets/facet/**/*.js', + 'js/widgets/alerts/**/*.js', 'js/widgets/abstract/widget', 'js/widgets/api_query/widget', 'js/widgets/base/base_widget', @@ -188,18 +119,13 @@ module.exports = function(grunt) { 'js/wraps/visualization_dropdown', 'libs/select2/matcher', ], - }); - - addConfig('abstract-page', { - include: [ - ...globFiles([ - 'js/wraps/*_facet.js', - 'js/widgets/metrics/**/*.js', - 'js/widgets/list_of_things/**/*.js', - 'js/widgets/search_bar/**/*.js', - 'js/widgets/facet/**/*.js', - 'js/widgets/alerts/**/*.js', - ]), + 'abstract-page': [ + 'js/wraps/*_facet.js', + 'js/widgets/metrics/**/*.js', + 'js/widgets/list_of_things/**/*.js', + 'js/widgets/search_bar/**/*.js', + 'js/widgets/facet/**/*.js', + 'js/widgets/alerts/**/*.js', 'js/widgets/abstract/widget', 'js/widgets/base/base_widget', 'js/widgets/footer/widget', @@ -221,24 +147,19 @@ module.exports = function(grunt) { 'js/wraps/table_of_contents', 'libs/select2/matcher', ], - }); - - addConfig('main', { - include: [ - ...globFiles([ - 'config/**/*.js', - '!config/discovery.vars.js', - '!config/shim.js', - 'js/apps/discovery/**/*.js', - '!js/apps/discovery/router.js', - 'js/components/**/*.js', - '!js/components/analytics.js', - '!js/components/query_builder/**/*.js', - 'js/mixins/**/*.js', - 'js/modules/**/*.js', - 'js/page_managers/**/*.js', - 'js/services/**/*.js', - ]), + main: [ + 'config/**/*.js', + '!config/discovery.vars.js', + '!config/shim.js', + 'js/apps/discovery/**/*.js', + '!js/apps/discovery/router.js', + 'js/components/**/*.js', + '!js/components/analytics.js', + '!js/components/query_builder/**/*.js', + 'js/mixins/**/*.js', + 'js/modules/**/*.js', + 'js/page_managers/**/*.js', + 'js/services/**/*.js', 'analytics', 'router', 'utils', @@ -260,43 +181,145 @@ module.exports = function(grunt) { 'react-transition-group', '@hookform/resolvers', ], + }, + }, + }, + }; + + grunt.registerMultiTask( + 'optimize-build', + 'Generate config and optimize build', + function() { + const _ = require('lodash'); + const rjs = require('requirejs'); + const fullConfig = {}; + const options = this.options({ + waitSeconds: 7, + logLevel: 4, + baseUrl: 'dist', + optimize: 'none', + mainConfigFile: 'dist/config/discovery.config.js', + deps: [], + findNestedDependencies: false, + create: true, + bundles: {}, + }); + + const { bundles, ...baseConfig } = options; + + // fail here if no bundles are passed + if (Object.keys(options.bundles).length === 0) { + return grunt.fail.fatal('No bundles configured'); + } + + /** + * Generate the bundles based on the passed in config + */ + grunt.registerTask('generateBundles', function() { + const done = this.async(); + + // log out a message on each successful completion + const onBundleComplete = ({ name }) => { + grunt.log.ok(`${name} generated!`); + }; + + // generate the config based on the base and bundle name + const getConfig = (name, cfg) => ({ + ...baseConfig, + name: `${name}.bundle`, + out: `dist/config/${name}.bundle.js`, + onModuleBundleComplete: onBundleComplete, + ...cfg, }); - writeOutConfig(config, done); + // expand file globs + const expand = (arrOfPaths) => { + const filter = (path) => { + // filter out all es6 modules, for now + return !/\.jsx\.js$/.test(path); + }; + + return grunt.file + .expand({ cwd: 'dist', filter }, arrOfPaths) + .map((path) => { + return path.replace(/\.js$/, ''); + }); + }; + + // wraps requirejs optimizer in promise + const optimize = (name, modules) => + new Promise((res, rej) => { + const config = getConfig(name, { include: modules }); + + // update fullConfig variable (used in other task) + fullConfig[name] = config; + + // call optimize on the configuration, passing in our promise handlers + rjs.optimize( + config, + () => res(), + (args) => rej(args) + ); + }); + + const promises = []; + Object.keys(bundles).forEach((name) => + promises.push(optimize(name, expand(bundles[name]))) + ); + + // wait on all the promises to finish, otherwise fail here + Promise.all(promises) + .catch((err) => grunt.fail.fatal(err)) + .finally(() => done()); }); - var getDiscoveryConfig = function() { - var content = grunt.file.read('dist/config/discovery.config.js'); - var cfg = {}; + const getDiscoveryConfig = function() { + const content = grunt.file.read('dist/config/discovery.config.js', { + encoding: 'utf-8', + }); + let cfg = {}; (function() { - var require = (requirejs = { - config: function(data) { + // mocks "require" in context for the eval call, this allows us to extract the config + // eslint-disable-next-line no-unused-vars + const require = { + config(data) { cfg = data; }, - }); + }; + + // eslint-disable-next-line no-eval eval(content.toString()); })(); + return cfg; }; - var generateConfigFileString = function(name, cnts) { - return ` + const generateConfigFileString = (name, contents) => + Buffer.from( + ` /** * GENERATED FILE (edits will be overwritten): * This is the configuration for ${name}. */ -requirejs.config(${JSON.stringify(cnts, null, 2)}); -`; - }; +requirejs.config(${JSON.stringify(contents)}); +`, + 'utf-8' + ); - grunt.registerTask('applyIncludesToConfig', function() { - var cfg = getDiscoveryConfig(); + /** + * Generates the requirejs configs based on a default one which we grab first + * + * Basically we generate bundles above, which now need to be the target of several paths. In + * these generated configs, we point to our bundle (if necessary) otherwise leave it be. + */ + grunt.registerTask('generateConfigs', function() { + const cfg = getDiscoveryConfig(); // generate the rest of the bundles - _.forEach(fullConfig, function(bundle, name) { - var _cfg = _.extend({}, cfg, { + _.forEach(fullConfig, function({ name, include }, moduleName) { + const _cfg = _.extend({}, cfg, { // set the main dependency to the bundle name - deps: [`config/${bundle.options.name}`], + deps: [`config/${name}`], // update the paths config with new revved names paths: _.extend( @@ -305,35 +328,30 @@ requirejs.config(${JSON.stringify(cnts, null, 2)}); // add all additional revved filenames to the paths _.reduce( - bundle.options.include, - function(acc, p) { - acc[p] = `config/${bundle.options.name}`; - return acc; - }, + include, + (acc, k) => ({ ...acc, [k]: `config/${name}` }), {} ), // some explicit path changes { - 'discovery.config': `config/${bundle.options.name}`, + 'discovery.config': `config/${name}`, } ), }); - var out = generateConfigFileString(`dist/${name}.config.js`, _cfg); - grunt.file.write(`dist/config/${name}.config.js`, out); - grunt.log.writeln(`${name}.config.js has been created`); + const out = generateConfigFileString( + `dist/${moduleName}.config.js`, + _cfg + ); + grunt.file.write(`dist/config/${moduleName}.config.js`, out); + grunt.log.writeln(`${moduleName}.config.js has been created`); }); }); - grunt.task.run(['generateConfig', 'babel:release', 'requirejs']); - grunt.task.run(['applyIncludesToConfig']); - // grunt.task.run(['uglify']); + grunt.task.run(['babel:release', 'generateBundles', 'generateConfigs']); } ); - return { - options: {}, - release: {}, - }; + return buildConfig; };