@epublishing/grunt-epublishing 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bumpedrc ADDED
@@ -0,0 +1,14 @@
1
+ files:
2
+ - package.json
3
+ - package-lock.json
4
+ plugins:
5
+ postrelease:
6
+ Committing new version:
7
+ plugin: bumped-terminal
8
+ command: 'git add package* && git commit -m "Version bump to v$newVersion"'
9
+ Publishing tag to Bitbucket:
10
+ plugin: bumped-terminal
11
+ command: 'git tag v$newVersion && git push && git push --tags'
12
+ Publishing to NPM:
13
+ plugin: bumped-terminal
14
+ command: npm publish .
package/.eslintrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": [ "@epublishing/epublishing" ],
3
+ "rules": {
4
+ "strict": 0
5
+ }
6
+ }
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright 2017 ePublishing, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in the
5
+ Software without restriction, including without limitation the rights to use,
6
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
7
+ Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
14
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
15
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
16
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # grunt-epublishing
2
+
3
+ > Automated front-end tasks for ePublishing Jade and client sites.
4
+
5
+ ## Getting Started
6
+ This plugin requires Grunt v1.0.0.
7
+
8
+ If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins.
9
+
10
+ NodeJS and Grunt-Cli must be installed before you proceed.
11
+
12
+ If you are on Mac OS X and use homebrew. The following commands will get you ready to run grunt.
13
+
14
+
15
+ ```sh
16
+ brew install node
17
+ npm install -g grunt-cli
18
+ ```
19
+
20
+ * To get started with using Grunt Jade you first need the right files within the project you wish to use this plugin. You will create a package.json and gruntfile.js in the root of your application. (unless someone has already done this)
21
+
22
+ * The gruntfile should contain the following code which enables the default jade task:
23
+
24
+
25
+ ```js
26
+ module.exports = function(grunt) {
27
+ // Load the grunt jade task
28
+ grunt.loadNpmTasks('@epublishing/grunt-epublishing');
29
+
30
+ // Run jade-default Grunt Task
31
+ grunt.registerTask('default', ['jade']);
32
+ };
33
+ ```
34
+
35
+ * The package.json file will contain the name of your site, version, and required dependencies that get installed before you can run "grunt". This file would look similar to this, and includes the actual required dependencies for Grunt Jade:
36
+
37
+
38
+ ```json
39
+ {
40
+ "name": "jade-labs",
41
+ "version": "0.1.0",
42
+ "description":"jade-labs",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git@bitbucket.org:epub_dev/jade"
46
+ },
47
+ "devDependencies": {
48
+ "grunt": "^1.0.0",
49
+ "grunt-jade": "git+ssh://git@bitbucket.org:epub_dev/grunt-jade.git"
50
+ }
51
+ }
52
+ ```
53
+
54
+ * Install the npm modules that this Grunt task depends on. To do this, run the following command in the application you wish to use this plugin.
55
+
56
+ ```sh
57
+ npm install
58
+ ```
59
+
60
+ Awesome! You are ready to run Grunt in the command line and watch Grunt Jade start working on your front-end assets. Grunt Jade will concatenate and minify the SCSS and Javascript. Here is what `grunt-jade` does when you run `grunt`:
61
+
62
+ 1. `set-jade-paths`: Resolve the locations of the active Jade gem, Jade child gem (if there is one), and site directories, making them available as config variables to subsequent tasks.
63
+ 1. `npm-install`: Install Node module dependencies in Jade and child gem locations if `package.json` files are present.
64
+ 1. `clean`: Clean (delete) previously compiled assets, if any.
65
+ 1. `babel`: Transpile standalone ES2015 scripts into public/javascripts/app with Babel.
66
+ 1. `concat`: Concatenate our default JS assets into a single script, `jade.default.js`.
67
+ 1. `uglify`: Minify the above script.
68
+ 1. `webpack`: Compile configured modules in `app/js` and bundle them with their dependencies using Webpack, saving them to `public/javascripts/app/bundle`.
69
+ 1. `sass`: Compile SCSS stylesheets to CSS using `libsass`.
70
+ 1. `bless`: _(disabled by default)_ Split compiled stylesheets into chunks in order to comply with MSIE 9's stylesheet selector limit.
71
+
72
+ ### Custom Tasks
73
+
74
+ `grunt-jade` provides a couple of custom Grunt tasks that run as part of the default task set:
75
+
76
+ + `set-jade-paths` makes Grunt's configuration aware of the ePublishing gems that a site depends on.
77
+ + Spawns a [Ruby script](./tasks/get-jade-gems.rb) as a subprocess that does the following things:
78
+ + Sets up Bundler and loads the site's RubyGems dependencies.
79
+ + Prints a list of gems whose names match the regular expression `/jade/` to stdout, as a JSON array.
80
+ + Parses the JSON array and populates `grunt.config.paths` with entries corresponding to:
81
+ + Jade's installation path as `paths.jade`
82
+ + The child gem's installation path (if any) as `paths.jadechild`
83
+ + Various relative asset directory paths (i.e. `paths.js_src`, `paths.css`, etc.)
84
+ + `npm-install` iterates through the paths populated by `set-jade-paths` looking for package.json files.
85
+ + If any results are found, it does the following for each:
86
+ + Switches the current working directory to the parent folder of the current package.json result.
87
+ + Runs `npm install` as a subprocess and waits for it to complete.
88
+ + When all results have been processed, the task changes the working directory back to the site's root.
89
+ + `watch-all` runs `grunt watch` and `grunt webpack --watch` at the same time in subprocesses
90
+
91
+ ## Release Management
92
+
93
+ `grunt-epublishing` installs the [bumped](https://bumped.github.io/) package as a development dependency, and its package.json file provides an NPM script that can be used to increment release versions, commit, tag, push, and publish to NPM in a single step:
94
+
95
+ ```sh
96
+ # Example (bump patch version):
97
+ $ npm run release patch
98
+ ```
99
+
100
+ ## Local Development
101
+
102
+ If you're working on new build system features, you can link your local clone of grunt-epublishing into a Jade site like
103
+ so:
104
+
105
+ ```sh
106
+ # Inside the grunt-epublishing directory:
107
+ npm link
108
+
109
+ # Change directories to the site:
110
+ cd ../jade-site
111
+
112
+ npm link @epublishing/grunt-epublishing
113
+ ```
@@ -0,0 +1,8 @@
1
+ module.exports = function(_pattern, _workingDir) {
2
+ return Promise.resolve(
3
+ [
4
+ {path: '/path/to/jade', name: 'jade'},
5
+ {path: '/path/to/jade_child', name: 'jade_engine'},
6
+ ]
7
+ )
8
+ }
package/etc/banner.txt ADDED
@@ -0,0 +1,6 @@
1
+ ┌─────────────────────────────┐
2
+ │┳━┓┳━┓┳ ┓┳━┓┳ o┓━┓┳ ┳o┏┓┓┏━┓│
3
+ │┣━ ┃━┛┃ ┃┃━┃┃ ┃┗━┓┃━┫┃┃┃┃┃ ┳│
4
+ │┻━┛┇ ┇━┛┇━┛┇━┛┇━━┛┇ ┻┇┇┗┛┇━┛│
5
+ └─────────────────────────────┘
6
+
@@ -0,0 +1,49 @@
1
+ {
2
+ "extends": [ "eslint:recommended", "plugin:react/recommended" ],
3
+ "parserOptions": {
4
+ "ecmaVersion": 6,
5
+ "sourceType": "module",
6
+ "ecmaFeatures": {
7
+ "impliedStrict": true,
8
+ "jsx": true,
9
+ "modules": true
10
+ }
11
+ },
12
+ "env": {
13
+ "browser": true,
14
+ "node": true,
15
+ "amd": true,
16
+ "es6": true,
17
+ "jquery": true
18
+ },
19
+ "settings": {
20
+ "react": {
21
+ "pragma": "React",
22
+ "version": "0.14.0"
23
+ }
24
+ },
25
+ "plugins": [
26
+ "react"
27
+ ],
28
+ "rules": {
29
+ "eqeqeq": [ 2, "smart" ],
30
+ "camelcase": 2,
31
+ "no-alert": 2,
32
+ "no-debugger": 2,
33
+ "no-console": 1,
34
+ "no-caller": 2,
35
+ "no-else-return": 1,
36
+ "no-eval": 2,
37
+ "no-extend-native": 1,
38
+ "no-useless-call": 1,
39
+ "no-case-declarations": 0,
40
+ "max-params": [ 2, 3 ],
41
+ "max-nested-callbacks": [ 2, 3 ],
42
+ "new-parens": 2,
43
+ "no-mixed-spaces-and-tabs": 2,
44
+ "operator-assignment": [ 2, "always" ],
45
+ "arrow-parens": [ 1, "always" ],
46
+ "react/no-danger": 1,
47
+ "react/prop-types": 1
48
+ }
49
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable camelcase */
2
+ /**
3
+ * This is the default, base configuration object into which grunt-jade merges
4
+ * additional configuration objects from Jade, engine gems, and the current site.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const modulesDir = path.resolve(require.resolve('lodash'), '..', '..');
11
+ const Resolver = require('@epublishing/jade-resolver');
12
+
13
+ module.exports = {
14
+ Resolver,
15
+ paths: {
16
+ app: 'app',
17
+ css: 'public/stylesheets',
18
+ js: 'public/javascripts',
19
+ js_src: 'app/js',
20
+ scss: 'app/sass',
21
+ spec: 'spec/javascripts',
22
+ },
23
+ babel: {
24
+ options: {
25
+ sourceMap: false,
26
+ presets: [
27
+ [require.resolve('@epublishing/babel-preset-epublishing'), {
28
+ transformRuntime: false,
29
+ lodash: { cwd: modulesDir },
30
+ }],
31
+ ],
32
+ },
33
+ },
34
+ };
@@ -0,0 +1,14 @@
1
+ module.exports = {
2
+ compilerOptions: {
3
+ allowJs: true,
4
+ experimentalDecorators: true,
5
+ allowSyntheticDefaultImports: true,
6
+ baseUrl: ".",
7
+ downlevelIteration: true,
8
+ forceConsistentCasingInFileNames: true,
9
+ lib: ["dom", "es6"],
10
+ module: "esnext",
11
+ moduleResolution: "node",
12
+ target: "es6",
13
+ },
14
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const cliui = require('cliui');
5
+
6
+ const flags = {
7
+ debug: 'Pretty-print a representation of the complete Grunt config object',
8
+ lint: 'Check the syntax of Webpack bundle JS files w/ eslint',
9
+ 'no-minify': 'Skip minification of Webpack bundles',
10
+ 'no-banner': 'Suppress ASCII banner',
11
+ 'no-time': 'Disable time-grunt performance stats',
12
+ analyze: 'Output Webpack bundle stats JSON and a graphical analysis of all bundles',
13
+ verbose: 'Show (much) more output',
14
+ watch: 'Keep Webpack alive and re-bundle when files change',
15
+ env: 'Sets process.env.NODE_ENV to the specified value',
16
+ };
17
+
18
+ const names = Object.keys(flags);
19
+
20
+ module.exports = function cliFlags() {
21
+ const ui = cliui({ width: 100 });
22
+ ui.div({ text: chalk.cyan.bold('Option Flags:'), padding: [ 1, 0, 1, 0 ] });
23
+
24
+ for (const name of names) {
25
+ ui.div(
26
+ {
27
+ text: chalk.bold(`--${name}`),
28
+ width: 20,
29
+ padding: [ 0, 4, 0, 4 ],
30
+ },
31
+ {
32
+ text: flags[name],
33
+ width: 80,
34
+ padding: [ 0, 4, 0, 4 ],
35
+ }
36
+ );
37
+ }
38
+
39
+ return ui.toString();
40
+ };
41
+
42
+ module.exports.flags = flags;
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const cssVariables = require('postcss-css-variables');
4
+
5
+ module.exports = function configurePostCSS(config, grunt) {
6
+ config.postcss.options = {
7
+ map: {
8
+ inline: false,
9
+ },
10
+ processors: [
11
+ cssVariables({
12
+ preserve: true,
13
+ }),
14
+ ],
15
+ };
16
+
17
+ return config;
18
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * This function merges custom SassScript functions into the base Sass config
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const _ = require('lodash');
8
+ const sass = require('node-sass');
9
+ const NODE_ENV = process.env.NODE_ENV || 'development';
10
+
11
+ module.exports = function configureSass(config) {
12
+
13
+ config.sass.options.functions = {
14
+ 'epub-show-deprecation-warnings()': () => {
15
+ if (_.includes([ 'local', 'development' ], NODE_ENV)) {
16
+ return sass.types.Boolean.TRUE;
17
+ }
18
+ return sass.types.Boolean.FALSE;
19
+ },
20
+ 'epub-node-env()': () => new sass.types.String(NODE_ENV),
21
+ };
22
+
23
+ return config;
24
+ };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * This function establishes base configuration for all detected Webpack build
3
+ * targets, including loaders and output plugins.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const os = require('os');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const webpack = require('webpack');
12
+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
13
+ const CompressionPlugin = require('compression-webpack-plugin');
14
+ const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
15
+ const lodashDir = path.dirname(require.resolve('lodash'));
16
+ const moduleDir = path.dirname(lodashDir);
17
+
18
+ module.exports = function configureWebpack(grunt, config) {
19
+ const { NODE_ENV = 'development' } = process.env;
20
+ const watch = !!grunt.option('watch');
21
+ const verbose = !!grunt.option('verbose');
22
+ const noMinify = !!grunt.option('no-minify');
23
+ const analyze = !!grunt.option('analyze');
24
+ const lint = NODE_ENV === 'test' || !!grunt.option('lint');
25
+
26
+ for (const target in config.webpack) {
27
+ const targetConfig = config.webpack[target];
28
+
29
+ // This makes select environment variables from the shell running Grunt available
30
+ // in client-side JS files (with values hard-coded at compile-time). The property values
31
+ // in the EnvironmentPlugin's configuration object function as default values, which
32
+ // will be used if no variable corresponding to the property name can be found
33
+ // in the user's terminal environment.
34
+ //
35
+ // They are accessible from bundled scripts the same way that environment variables in Node.js
36
+ // scripts are - as properties of the `process.env` object (i.e. process.env.NODE_ENV).
37
+ const environmentVars = new webpack.EnvironmentPlugin({
38
+ NODE_ENV,
39
+ DEBUG: false,
40
+ });
41
+
42
+ // This prevents Webpack from loading every single locale definition file that Moment.js provides.
43
+ // 99% of the time we only care about formatting dates in US English, so we have no need for i.e.
44
+ // the preferred date formats of Esperanto-speaking residents of Papua New Guinea. American cultural hedgemony FTW!
45
+ const momentLocales = new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /en/);
46
+
47
+ let plugins = [ environmentVars, momentLocales ];
48
+
49
+ if (!config.babelLoader) {
50
+ config.babelLoader = { exceptions: [], exclude: /(node_modules|bower_components)/ };
51
+ }
52
+
53
+ const babelExclude = config.babelLoader.exclude;
54
+ const babelExceptions = config.babelLoader.exceptions.map((mod) => new RegExp(`node_modules/${mod}/(.+)\\.js$`));
55
+
56
+ targetConfig.watch = watch;
57
+ targetConfig.keepalive = watch || analyze;
58
+ targetConfig.stats.modules = verbose;
59
+ targetConfig.stats.reasons = verbose;
60
+ targetConfig.profile = analyze;
61
+
62
+ // Tell babel-plugin-lodash where to find modularized Lo-Dash functions:
63
+ targetConfig.resolve.alias.lodash = lodashDir;
64
+
65
+ const babelOptions = {
66
+ presets: [
67
+ [require.resolve('@epublishing/babel-preset-epublishing'), {
68
+ lodash: { cwd: moduleDir },
69
+ env: {
70
+ modules: false,
71
+ },
72
+ minify: false,
73
+ }],
74
+ ],
75
+ }
76
+
77
+ const rules = [
78
+ {
79
+ test: /\.jsx?$/,
80
+ loader: 'babel-loader',
81
+ exclude: (input) => {
82
+ // Check whether the asset has a matching exclusion exception pattern and allow it to transpile if it does:
83
+ const isException = babelExceptions.some((pattern) => pattern.test(input));
84
+ if (isException) return !isException;
85
+
86
+ // Test asset against the default exclusion pattern and return result:
87
+ return babelExclude.test(input);
88
+ },
89
+ options: babelOptions,
90
+ },
91
+ {
92
+ test: /\.tsx?$/,
93
+ exclude: /node_modules/,
94
+ use: [
95
+ {
96
+ loader: 'babel-loader',
97
+ options: babelOptions,
98
+ },
99
+ {
100
+ loader: 'ts-loader',
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ test: /\.css$/,
106
+ use: [
107
+ 'style-loader',
108
+ 'css-loader',
109
+ ],
110
+ },
111
+ {
112
+ test: [ /\.svg$/, /\.jpe?g$/, /\.gif$/, /\.png$/ ],
113
+ loader: 'file-loader',
114
+ },
115
+ ];
116
+
117
+ if (lint) {
118
+ const localEslintConfig = path.join(process.cwd(), '.eslintrc');
119
+ const globalEslintConfig = path.resolve(__dirname, '../.eslintrc');
120
+ const eslintConfig = fs.existsSync(localEslintConfig) ? localEslintConfig : globalEslintConfig;
121
+
122
+ rules.push({
123
+ enforce: 'pre',
124
+ test: /\.js$/,
125
+ exclude: /(node_modules|bower_components|public|vendor)/,
126
+ loader: 'eslint-loader',
127
+ options: {
128
+ configFile: eslintConfig,
129
+ formatter: require('eslint-friendly-formatter'),
130
+ failOnError: true,
131
+ outputReport: {
132
+ filePath: 'eslint.xml',
133
+ formatter: require('eslint/lib/formatters/junit'),
134
+ },
135
+ },
136
+ });
137
+ }
138
+
139
+ targetConfig.module = { rules };
140
+
141
+ if (analyze) {
142
+ const bundleAnalyzer = new BundleAnalyzerPlugin({
143
+ analyzerMode: 'server',
144
+ analyzerHost: '127.0.0.1',
145
+ analyzerPort: '8888',
146
+ reportFilename: 'webpack-analysis.html',
147
+ defaultSizes: 'parsed',
148
+ openAnalyzer: true,
149
+ generateStatsFile: true,
150
+ statsFilename: 'webpack.stats.json',
151
+ statsOptions: { chunkModules: true },
152
+ });
153
+ plugins.push(bundleAnalyzer);
154
+ }
155
+
156
+ if (!noMinify) {
157
+ const uglifyDefaults = {
158
+ cache: true,
159
+ sourceMap: true,
160
+ parallel: Math.max(os.cpus().length / 2, 1),
161
+ };
162
+ const { uglifyConfig = {} } = targetConfig;
163
+ delete targetConfig.uglifyConfig;
164
+
165
+ plugins.push(new UglifyJsPlugin(Object.assign({}, uglifyDefaults, uglifyConfig)));
166
+ }
167
+
168
+ if (NODE_ENV === 'production') {
169
+ plugins.push(new CompressionPlugin({
170
+ asset: '[path].gz',
171
+ algorithm: 'gzip',
172
+ test: /\.js$/,
173
+ threshold: 10240,
174
+ minRatio: 0.8,
175
+ }));
176
+ }
177
+
178
+ if (Array.isArray(targetConfig.appendPlugins)) {
179
+ plugins = plugins.concat(targetConfig.appendPlugins);
180
+ delete targetConfig.appendPlugins;
181
+ }
182
+
183
+ targetConfig.plugins = plugins;
184
+
185
+ if (targetConfig.customize && typeof targetConfig.customize === 'function') {
186
+ config.webpack[target] = targetConfig.customize(targetConfig, webpack);
187
+ delete config.webpack[target].customize;
188
+ }
189
+ }
190
+
191
+ return config;
192
+ };
@@ -0,0 +1,64 @@
1
+ const getGemPaths = require('@epublishing/get-gem-paths');
2
+ const flatten = require('lodash/flatten');
3
+ const merge = require('lodash/merge');
4
+ const path = require('path');
5
+ const baseConfig = require('./base-tsconfig');
6
+
7
+ /**
8
+ * Generate an object representing all of the tsconfig.json files needed to build a site
9
+ * @param {string} sitePath - absolute path to site
10
+ *
11
+ * @typedef {Object} TsconfigData
12
+ * @property {string} location - the absolute path of the tsconfig
13
+ * @property {Object} tsconfig - the tsconfig object to write to file
14
+ *
15
+ * @returns TsconfigData[]
16
+ */
17
+ module.exports = function genTsConfig(sitePath = process.cwd()) {
18
+ return getGemPaths('jade_?', sitePath)
19
+ .then(function(gems) {
20
+ const sourceRoots = gems
21
+ .concat() // don't mutate input param
22
+ .sort(function(a, _b) {
23
+ // jade should always come last
24
+ if (a.name === 'jade') return 1
25
+ return -1;
26
+ })
27
+ .map(function(gem) { return gem.path })
28
+
29
+ sourceRoots.unshift(sitePath) // site should always come first
30
+
31
+ const assetLocation = 'app/js/*' // only assets in app/js should be considered.
32
+
33
+ /**
34
+ * for each source root get the relative path from the root to all roots
35
+ * (including self) + each asset location
36
+ */
37
+ return sourceRoots.map((sourceRoot) => {
38
+
39
+ /**
40
+ * generate all of the relative paths between a given source
41
+ * (site, jade engine or jade child) and all other sources
42
+ */
43
+ const relativePaths = sourceRoots.map((root) => {
44
+ const destination = path.resolve(root, assetLocation);
45
+ return path.relative(sourceRoot, destination);
46
+ })
47
+
48
+ const pathsField = {
49
+ compilerOptions: {
50
+ paths: {
51
+ '*': flatten(relativePaths),
52
+ },
53
+ },
54
+ }
55
+
56
+ const tsconfig = merge({}, baseConfig, pathsField)
57
+
58
+ return {
59
+ location: sourceRoot,
60
+ tsconfig,
61
+ }
62
+ })
63
+ })
64
+ }
@@ -0,0 +1,91 @@
1
+ /* eslint-disable max-params */
2
+ /**
3
+ * Initializer function for creating a complete Grunt configuration object for
4
+ * an ePublishing Jade site. Merges config values from Jade, any detected engine
5
+ * gems, and the site itself.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const _ = require('lodash');
11
+ const fs = require('fs');
12
+ const prettyjson = require('prettyjson');
13
+ const mergeConfigs = require('./merge-configs');
14
+ const configureWebpack = require('./configure-webpack');
15
+ const configureSass = require('./configure-sass');
16
+ const configurePostCSS = require('./configure-postcss');
17
+
18
+ module.exports = function initJadeConfig(grunt, jadePath, jadeChildPath, jadeChildPaths) {
19
+ // Initialize baseConfig
20
+ let baseConfig = require('./base-config');
21
+
22
+ baseConfig.paths.jade = jadePath;
23
+ baseConfig.paths.jadechild = jadeChildPath;
24
+
25
+ // Add in the jadeChildPaths
26
+ for (const i in jadeChildPaths) {
27
+ baseConfig.paths[i] = jadeChildPaths[i];
28
+ }
29
+
30
+ // Merge Jade's Grunt configuration into baseConfig
31
+ baseConfig = mergeConfigs(baseConfig, jadePath);
32
+
33
+ // Loop through all detected engine gem paths and merge their Grunt configurations into baseConfig
34
+ for (const childPath of _.values(jadeChildPaths)) {
35
+ baseConfig = mergeConfigs(baseConfig, childPath);
36
+ }
37
+
38
+ // Finally, merge the site's configuration values into baseConfig
39
+ baseConfig = mergeConfigs(baseConfig, process.cwd());
40
+
41
+ // Update baseConfig's Webpack settings with ePublishing defaults:
42
+ baseConfig = configureWebpack(grunt, baseConfig);
43
+
44
+ // Add custom functions and settings to the merged Sass config
45
+ baseConfig = configureSass(baseConfig);
46
+
47
+ if (baseConfig.postcss) {
48
+ baseConfig = configurePostCSS(baseConfig, grunt);
49
+ }
50
+
51
+ const jadeTasks = [
52
+ 'gen-tsconfig',
53
+ 'npm-install',
54
+ 'clean',
55
+ 'webpack',
56
+ 'babel',
57
+ 'concat',
58
+ 'uglify',
59
+ 'sass',
60
+ 'clean-tsconfig',
61
+ ];
62
+
63
+ if (baseConfig.bless) {
64
+ jadeTasks.push('bless');
65
+ }
66
+
67
+ if (baseConfig.postcss) {
68
+ jadeTasks.push('postcss');
69
+ }
70
+
71
+ // Initialize the grunt configuration
72
+ grunt.config.init(baseConfig);
73
+
74
+ // Add package.json to the grunt config
75
+ grunt.config.set('pkg', grunt.file.readJSON('package.json'));
76
+
77
+ // Check to see if we need to add the bower tasks
78
+ if (fs.existsSync('bower.json')) {
79
+ jadeTasks.unshift('bower-install-simple', 'bower');
80
+ }
81
+
82
+ // Register the main jade task!
83
+ grunt.registerTask('jade', jadeTasks);
84
+
85
+ // This is left for legacy sake. The task should be called 'jade' because that's
86
+ // what this module is called, but some people are already using this.
87
+ grunt.registerTask('jade-default', jadeTasks);
88
+
89
+ // Optionally output the entire, merged config object via the --debug flag
90
+ grunt.log.debug(prettyjson.render(grunt.config.data));
91
+ };
@@ -0,0 +1,89 @@
1
+ /* eslint-disable no-console */
2
+
3
+ 'use strict';
4
+
5
+ const _ = require('lodash');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * splits a path and returns the filename (everything after the last /)
11
+ * @param {string} fullPath - A path to a file
12
+ * @returns {string} the extracted filename
13
+ */
14
+ function getFilename(fullPath) {
15
+ if (!fullPath) {
16
+ console.trace();
17
+ throw new Error('fullPath was undefined');
18
+ }
19
+ return path.basename(fullPath);
20
+ }
21
+
22
+ /**
23
+ * This function merges grunt configuration objects.
24
+ * This is a function fed into the lodash libraries
25
+ * _.merge function. It only handles merging arrays.
26
+ * The default is to just concat the dest array onto
27
+ * the end of the src array.
28
+ *
29
+ * However if the elements in the array contain references to .js files
30
+ * it instead checks the file name of the .js file (everything after the last /)
31
+ * and OVERRIDES the .js file from the source if it exists in the dest
32
+ *
33
+ * Any src js files that are not in the dest are simply added to the end of the dest
34
+ *
35
+ * @param {Object} dest The destination object to be merged
36
+ * @param {Object} src The source object to be merged into the dest
37
+ * @returns {Object} Either the dest object or undefined to let lodash handle it.
38
+ */
39
+ function configMerger(dest, src) {
40
+ // We only trigger the custom merge for arrays
41
+ if (!_.isArray(dest)) return;
42
+
43
+ const jsRegExp = /[^*]\.js$/;
44
+
45
+ let i, j;
46
+
47
+ // JavaScript files with the same base name override one another:
48
+ for (i = 0; i < dest.length; i++) {
49
+ const destFile = getFilename(dest[i]);
50
+
51
+ if (!jsRegExp.test(destFile)) continue;
52
+
53
+ for (j = 0; j < src.length; j++) {
54
+ const srcFile = getFilename(src[j]);
55
+ if (destFile !== srcFile) continue;
56
+ dest[i] = src[j];
57
+ }
58
+ }
59
+
60
+ // All other array members are pushed and de-duped
61
+ for (const member of src) {
62
+ if (_.includes(dest, member)) continue;
63
+ dest.push(member);
64
+ }
65
+
66
+ return dest;
67
+ }
68
+
69
+ /**
70
+ * Merge a base Grunt configuration object with the configuration found
71
+ * in the specified base path, if any.
72
+ *
73
+ * @param {Object} baseConfig - The base Grunt configuration object
74
+ * @param {string} configDir - The root path of a site or Rails engine that contains additional configuration to be merged in
75
+ * @returns {Object} A new configuration object
76
+ */
77
+ module.exports = function mergeConfigs(baseConfig, configDir) {
78
+ const configFile = path.join(configDir, 'config', 'grunt-config.js');
79
+
80
+ if (!fs.existsSync(configFile)) return baseConfig;
81
+
82
+ let config = require(configFile);
83
+
84
+ if (_.isFunction(config)) {
85
+ config = config(baseConfig);
86
+ }
87
+
88
+ return _.mergeWith({}, baseConfig, config, configMerger);
89
+ };
@@ -0,0 +1,23 @@
1
+ /* eslint-disable class-methods-use-this, no-console */
2
+
3
+ const chalk = require('chalk');
4
+
5
+ class WebpackConsoleTimer {
6
+ apply(compiler) {
7
+ compiler.plugin('compilation', (compilation) => {
8
+ let startOptimizePhase;
9
+
10
+ compilation.plugin('optimize-chunk-assets', (chunks, callback) => {
11
+ startOptimizePhase = Date.now();
12
+ callback();
13
+ });
14
+
15
+ compilation.plugin('after-optimize-chunk-assets', () => {
16
+ const optimizePhaseDuration = Date.now() - startOptimizePhase;
17
+ console.log(`\nOptimizer phase duration: ${chalk.bold(optimizePhaseDuration)}ms`);
18
+ });
19
+ });
20
+ }
21
+ }
22
+
23
+ module.exports = WebpackConsoleTimer;
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "@epublishing/grunt-epublishing",
3
+ "description": "Automated front-end tasks for ePublishing Jade and client sites.",
4
+ "version": "0.3.0",
5
+ "homepage": "https://www.epublishing.com",
6
+ "contributors": [
7
+ {
8
+ "name": "Nick Brewer",
9
+ "email": "nbrewer@epublishing.com"
10
+ },
11
+ {
12
+ "name": "Mike Green",
13
+ "email": "mgreen@epublishing.com"
14
+ }
15
+ ],
16
+ "repository": "bitbucket:epub_dev/grunt-epublishing",
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">= 8.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@epublishing/babel-preset-epublishing": "^0.1.6",
23
+ "@epublishing/get-gem-paths": "^0.1.1",
24
+ "@epublishing/grunt-install-eslint": "^0.1.1",
25
+ "@epublishing/jade-resolver": "^0.1.2",
26
+ "async": "^2.6.1",
27
+ "babel-loader": "^7.1.5",
28
+ "babel-minify-webpack-plugin": "^0.3.0",
29
+ "bourbon": "^4.2.7",
30
+ "breakpoint-sass": "^2.7.0",
31
+ "chalk": "^2.4.1",
32
+ "cli-spinner": "^0.2.8",
33
+ "cliui": "^4.1.0",
34
+ "compass-mixins": "^0.12.10",
35
+ "compression-webpack-plugin": "^1.1.7",
36
+ "css-loader": "^0.28.9",
37
+ "cssnano": "^4.0.5",
38
+ "es5-shim": "^4.5.10",
39
+ "es6-promise": "^4.2.4",
40
+ "eslint": "^5.3.0",
41
+ "eslint-friendly-formatter": "^4.0.1",
42
+ "eslint-loader": "^2.1.0",
43
+ "eslint-plugin-import": "^2.13.0",
44
+ "eslint-plugin-react": "^7.10.0",
45
+ "execa": "^0.10.0",
46
+ "exports-loader": "^0.7.0",
47
+ "expose-loader": "^0.7.4",
48
+ "file-loader": "^1.1.11",
49
+ "grunt": "^1.0.3",
50
+ "grunt-babel": "^7.0.0",
51
+ "grunt-bless": "^1.0.2",
52
+ "grunt-bower": "^0.21.4",
53
+ "grunt-bower-install-simple": "^1.2.6",
54
+ "grunt-contrib-clean": "^1.1.0",
55
+ "grunt-contrib-concat": "^1.0.1",
56
+ "grunt-contrib-uglify": "^3.4.0",
57
+ "grunt-contrib-watch": "^1.1.0",
58
+ "grunt-postcss": "^0.9.0",
59
+ "grunt-sass": "^2.1.0",
60
+ "grunt-webpack": "^3.1.2",
61
+ "handlebars": "^4.0.11",
62
+ "handlebars-loader": "^1.6.0",
63
+ "imports-loader": "^0.8.0",
64
+ "jit-grunt": "^0.10.0",
65
+ "listr": "^0.14.1",
66
+ "lodash": "^4.17.10",
67
+ "node-sass": "^4.14.1",
68
+ "postcss-css-variables": "^0.9.0",
69
+ "prettyjson": "^1.2.1",
70
+ "read-pkg": "^4.0.1",
71
+ "style-loader": "^0.20.2",
72
+ "susy": "^2.2.14",
73
+ "time-grunt": "^1.4.0",
74
+ "ts-loader": "^3.5.0",
75
+ "uglifyjs-webpack-plugin": "^1.2.7",
76
+ "webpack": "^3.12.0",
77
+ "webpack-bundle-analyzer": "^2.13.1",
78
+ "webpack-dev-server": "^2.11.1",
79
+ "worker-loader": "^2.0.0"
80
+ },
81
+ "keywords": [
82
+ "gruntplugin"
83
+ ],
84
+ "devDependencies": {
85
+ "@epublishing/eslint-config-epublishing": "^0.2.0",
86
+ "bumped": "^0.10.10",
87
+ "bumped-terminal": "^0.7.5",
88
+ "jest": "^24.9.0"
89
+ },
90
+ "scripts": {
91
+ "release": "bumped release",
92
+ "test": "jest"
93
+ }
94
+ }
@@ -0,0 +1,74 @@
1
+ const genTsConfig = require('../../lib/gen-tsconfig');
2
+
3
+ function expectedConfig(sitePath) {
4
+
5
+ return [{
6
+ location: `${sitePath}`,
7
+ tsconfig: {
8
+ compilerOptions: {
9
+ allowSyntheticDefaultImports: true,
10
+ baseUrl: '.',
11
+ lib: ['dom', 'es6'],
12
+ module: 'esnext',
13
+ moduleResolution: 'node',
14
+ paths: {
15
+ '*': [
16
+ 'app/js/*',
17
+ '../../path/to/jade_child/app/js/*',
18
+ '../../path/to/jade/app/js/*'
19
+ ]
20
+ },
21
+ target: 'es6'
22
+ }
23
+ }
24
+ }, {
25
+ location: '/path/to/jade_child',
26
+ tsconfig: {
27
+ compilerOptions: {
28
+ allowSyntheticDefaultImports: true,
29
+ baseUrl: '.',
30
+ lib: ['dom', 'es6'],
31
+ module: 'esnext',
32
+ moduleResolution: 'node',
33
+ paths: {
34
+ '*': [
35
+ `../../..${sitePath}/app/js/*`,
36
+ 'app/js/*',
37
+ '../jade/app/js/*',
38
+ ]
39
+ },
40
+ 'target': 'es6',
41
+ }
42
+ }
43
+ }, {
44
+ location: '/path/to/jade',
45
+ tsconfig: {
46
+ compilerOptions: {
47
+ allowSyntheticDefaultImports: true,
48
+ baseUrl: '.',
49
+ lib: ['dom', 'es6'],
50
+ module: 'esnext',
51
+ moduleResolution: 'node',
52
+ paths: {
53
+ '*': [
54
+ `../../..${sitePath}/app/js/*`,
55
+ '../jade_child/app/js/*',
56
+ 'app/js/*'
57
+ ]
58
+ },
59
+ 'target': 'es6',
60
+ }
61
+ }
62
+ }]
63
+ }
64
+
65
+
66
+ test('returns tsconfigs for pwd', function(done) {
67
+
68
+ const processSpy = jest.spyOn(process, 'cwd').mockImplementation(() => '/my/cwd');
69
+ genTsConfig().then(function(result) {
70
+ expect(result).toEqual(expectedConfig('/my/cwd'));
71
+ processSpy.mockClear()
72
+ done()
73
+ })
74
+ })
@@ -0,0 +1,37 @@
1
+ /* eslint-disable no-console */
2
+ 'use strict';
3
+ /**
4
+ * Remove tsconfig from site and jade engines
5
+ */
6
+ module.exports = function (grunt) {
7
+ grunt.registerTask('clean-tsconfig', 'Remove tsconfig from site and jade engines', function() {
8
+ const genTsConfig = require('../lib/gen-tsconfig');
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+
12
+ const { NODE_ENV } = process.env;
13
+
14
+ /**
15
+ * if the build is not for a deployment (i.e. development), we'll want to keep tsconfigs
16
+ * a better developer experience
17
+ */
18
+ if ( NODE_ENV !== 'production' && NODE_ENV !== 'staging' ) {
19
+ console.log('Not deploying to production or stage. tsconfig.json will not be removed.');
20
+ return;
21
+ }
22
+
23
+ const done = this.async();
24
+
25
+ genTsConfig(grunt.option('siteRoot'))
26
+ .then((configPayloads) => {
27
+ configPayloads.forEach(function(payload) {
28
+ const configPath = path.resolve(payload.location, 'tsconfig.json');
29
+ fs.unlinkSync(configPath)
30
+ })
31
+ done();
32
+ }).catch((err) => {
33
+ console.log(err)
34
+ done()
35
+ })
36
+ })
37
+ }
@@ -0,0 +1,38 @@
1
+ /* eslint-disable no-console */
2
+ 'use strict';
3
+ /**
4
+ * This task generates a tsconfig.json for the site as well as its jade engines.
5
+ */
6
+ module.exports = function(grunt) {
7
+ grunt.registerTask('gen-tsconfig', 'Generate .tsconfig.json for site and all Jade engines', function() {
8
+ const genTsConfig = require('../lib/gen-tsconfig');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+
12
+ const done = this.async();
13
+ const tsConfigDotJson = 'tsconfig.json'
14
+
15
+ genTsConfig(grunt.option('siteRoot'))
16
+ .then((configPayloads) => {
17
+
18
+ configPayloads.forEach(function(payload) {
19
+ const configPath = path.resolve(payload.location, tsConfigDotJson);
20
+ createTSConfig(configPath, payload.tsconfig);
21
+ })
22
+
23
+ done();
24
+ })
25
+
26
+ /**
27
+ * creates a .tsconfig.json
28
+ * @param {string} path - path to write to
29
+ * @param {string} data - data to write
30
+ */
31
+ function createTSConfig(path, data) {
32
+ fs.writeFileSync(path, JSON.stringify(data))
33
+ console.log('created tsconfig.json for', path)
34
+ }
35
+ }
36
+ )
37
+ }
38
+
package/tasks/jade.js ADDED
@@ -0,0 +1,72 @@
1
+ /* eslint-disable no-console */
2
+
3
+ 'use strict';
4
+
5
+ const path = require('path');
6
+ const timeGrunt = require('time-grunt');
7
+ const jitGrunt = require('jit-grunt');
8
+ const fs = require('fs');
9
+ const readPkg = require('read-pkg');
10
+ const getGemPaths = require('@epublishing/get-gem-paths');
11
+ const cliFlags = require('../lib/cli-flags');
12
+ const initJadeConfig = require('../lib/init-jade-config');
13
+
14
+ module.exports = function(grunt) {
15
+ grunt.option('siteRoot', process.cwd())
16
+
17
+ if (!grunt.option('no-time')) timeGrunt(grunt);
18
+ jitGrunt(grunt, {
19
+ 'install-eslint': '@epublishing/grunt-install-eslint',
20
+ });
21
+
22
+ /**
23
+ * This registers a grunt task which shells out and uses bundler to
24
+ * determine the paths to the jade gem and any jade child engine gem
25
+ */
26
+ grunt.registerTask('set-jade-paths', 'Get Jade Gem Paths', function() {
27
+ let jadePath;
28
+ let jadeChildPath;
29
+ const jadeChildPaths = {};
30
+ const isTerminal = Boolean(process.stdout.isTTY);
31
+ const moduleRoot = path.resolve(__dirname, '..');
32
+ const done = this.async();
33
+ const asciiBanner = fs.readFileSync(path.join(moduleRoot, 'etc/banner.txt'), 'utf8');
34
+ const modulePkg = readPkg.sync({ cwd: moduleRoot });
35
+ const sitePkg = readPkg.sync();
36
+ const banner = [
37
+ asciiBanner,
38
+ `grunt-epublishing v${modulePkg.version}`,
39
+ `currently building: ${sitePkg.name} v${sitePkg.version}`,
40
+ cliFlags(),
41
+ ].join('\n');
42
+
43
+ if (grunt.option('env')) {
44
+ process.env.NODE_ENV = grunt.option('env');
45
+ }
46
+
47
+ if (isTerminal && !grunt.option('no-banner')) grunt.log.writeln(`\n${banner}`);
48
+
49
+ // Query bundler for Jade-related gems in a subprocess
50
+ getGemPaths('jade_?')
51
+ .then((data) => {
52
+ for (const gem of data) {
53
+ if (gem.name === 'jade') {
54
+ jadePath = gem.path;
55
+ } else if ((/^jade_/).test(gem.name)) {
56
+ jadeChildPaths[gem.name] = gem.path;
57
+ jadeChildPath = gem.path;
58
+ }
59
+ }
60
+
61
+ done();
62
+ initJadeConfig(grunt, jadePath, jadeChildPath, jadeChildPaths);
63
+ })
64
+ .catch((err) => {
65
+ grunt.fail.fatal(err);
66
+ done();
67
+ });
68
+ });
69
+
70
+ // Force-run the `set-jade-paths` task
71
+ grunt.task.run(['set-jade-paths']);
72
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * This registers a grunt task that performs an NPM install at each level of
3
+ * hierarchy that has a package.json file present (jade and child gem). Site
4
+ * level NPM installs are still manual.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ module.exports = function(grunt) {
10
+ const _ = require('lodash');
11
+ const async = require('async');
12
+ const path = require('path');
13
+ const Resolver = require('@epublishing/jade-resolver');
14
+
15
+ grunt.registerTask('npm-install', 'Install Node module dependencies', function() {
16
+ const Spinner = require('cli-spinner').Spinner;
17
+ const origPath = process.cwd();
18
+ const resolver = new Resolver(grunt.config.get('paths'));
19
+ const isTerminal = Boolean(process.stdout.isTTY);
20
+
21
+ resolver.removePath('site');
22
+
23
+ const packages = resolver.find('package.json');
24
+
25
+ if (_.isEmpty(packages)) {
26
+ grunt.log.ok('No other package.json files found in hierarchy. Skipping npm-install task.');
27
+ return;
28
+ }
29
+
30
+ const done = this.async();
31
+ const installQueue = _.map(packages, (pkg) => {
32
+ const basePath = path.dirname(pkg);
33
+ const basename = path.basename(basePath);
34
+
35
+ return (callback) => {
36
+ const spinner = new Spinner('%s installing...');
37
+
38
+ spinner.setSpinnerString(Spinner.spinners[18]);
39
+
40
+ grunt.log.writeln(`Installing NPM modules in ${basename}`);
41
+
42
+ if (isTerminal) spinner.start();
43
+
44
+ process.chdir(basePath);
45
+
46
+ grunt.util.spawn({
47
+ cmd: 'npm',
48
+ args: [ 'ci' ],
49
+ }, (err, result) => {
50
+ if (err) {
51
+ grunt.fail.warn(err);
52
+ } else {
53
+ if (isTerminal) spinner.stop(true);
54
+ grunt.verbose.writeln(result.stdout);
55
+ grunt.log.ok('Success!');
56
+ }
57
+
58
+ callback(err, result);
59
+ });
60
+ };
61
+ });
62
+
63
+ async.series(installQueue, (err, results) => {
64
+ if (err) {
65
+ return done(err);
66
+ }
67
+
68
+ grunt.log.ok('All NPM packages installed');
69
+ process.chdir(origPath);
70
+ done(results);
71
+ });
72
+ });
73
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * This allows both the standard watch task and the Webpack watcher to
3
+ * run concurrently
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ module.exports = (grunt) => {
9
+ grunt.registerTask('watch-all', 'Watch with Sass and Webpack concurrently', function() {
10
+ const done = this.async();
11
+ const { spawn } = require('child_process');
12
+ const cwd = process.cwd();
13
+ const { env } = process;
14
+ const [ nodeBin, gruntBin, ...argv ] = process.argv;
15
+ const flags = '--no-banner --no-time';
16
+ const tasks = [
17
+ `watch ${flags}`,
18
+ `webpack --watch ${flags}`,
19
+ ];
20
+
21
+ grunt.log.ok('Spawning watcher tasks...');
22
+
23
+ const subprocesses = tasks.map((task) => {
24
+ const args = [ gruntBin, ...task.split(' ') ];
25
+ const subprocess = spawn(nodeBin, args, {
26
+ cwd,
27
+ env,
28
+ stdio: 'inherit',
29
+ });
30
+
31
+ subprocess.on('error', (err) => {
32
+ grunt.log.fatal(err);
33
+ });
34
+
35
+ return subprocess;
36
+ });
37
+
38
+ const cleanup = (exit = false) => {
39
+ grunt.log.ok('Killing subprocesses...');
40
+ for (const subprocess of subprocesses) {
41
+ subprocess.kill('SIGKILL');
42
+ }
43
+ if (exit) process.exit();
44
+ };
45
+
46
+ process.on('exit', cleanup);
47
+ process.on('SIGINT', () => cleanup(true));
48
+ });
49
+ };