├── scripts ├── test.js ├── create-entry.js ├── utils │ └── createWebpackCompiler.js ├── start.js └── build.js ├── .gitignore ├── .node-version ├── template ├── index.css ├── index.js └── entry.js ├── babelrc ├── eslintrc.prod ├── eslintrc ├── config ├── polyfills.js ├── env.js ├── devNiceties.js ├── webpackDevServer.config.js ├── paths.js ├── webpack.config.prod.js └── webpack.config.dev.js ├── bin └── icelab-assets.js ├── CHANGELOG.md ├── package.json └── README.md /scripts/test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 6.9.5 2 | -------------------------------------------------------------------------------- /template/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Start writing CSS! 3 | */ -------------------------------------------------------------------------------- /template/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Start writing JS! 3 | */ 4 | -------------------------------------------------------------------------------- /babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": ["syntax-dynamic-import"] 4 | } -------------------------------------------------------------------------------- /eslintrc.prod: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | rules: { 4 | "no-debugger": "error" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "plugins": [ 4 | "prettier" 5 | ], 6 | "rules": { 7 | "prettier/prettier": "error" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /template/entry.js: -------------------------------------------------------------------------------- 1 | // Import the base CSS, basically our entry point for CSS. If your entry 2 | // doesn't require CSS, you can comment this out or remove it. 3 | import "./index.css"; 4 | 5 | // Import the base JS, basically our entry point for JS. If your entry 6 | // doesn't require JS, you can comment this out or remove it. 7 | import "./index.js"; 8 | 9 | // This will inspect all subdirectories from the context (this file) and 10 | // require files matching the regex. 11 | // https://webpack.js.org/guides/dependency-management/#require-context 12 | require.context(".", true, /^\.\/.*\.(jpe?g|png|gif|svg|woff2?|ttf|otf|eot|ico)$/); 13 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Grab NODE_ENV and APP_ASSETS_* environment variables and prepare them to be 4 | // injected into the application via DefinePlugin in Webpack configuration. 5 | const ASSETS = /^ASSETS_/i; 6 | 7 | function getClientEnvironment() { 8 | const raw = Object.keys(process.env) 9 | .filter(key => ASSETS.test(key)) 10 | .reduce( 11 | (env, key) => { 12 | env[key] = process.env[key]; 13 | return env; 14 | }, 15 | { 16 | // Useful for determining whether we’re running in production mode. 17 | // Most importantly, it switches React into the correct mode. 18 | NODE_ENV: process.env.NODE_ENV || 'development', 19 | } 20 | ); 21 | // Stringify all values so we can feed into Webpack DefinePlugin 22 | const stringified = { 23 | 'process.env': Object.keys(raw).reduce( 24 | (env, key) => { 25 | env[key] = JSON.stringify(raw[key]); 26 | return env; 27 | }, 28 | {} 29 | ), 30 | }; 31 | 32 | return { raw, stringified }; 33 | } 34 | 35 | module.exports = getClientEnvironment; -------------------------------------------------------------------------------- /scripts/create-entry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | const isEmptyDir = require('empty-dir'); 5 | const path = require('path'); 6 | const chalk = require('chalk'); 7 | 8 | const ownPath = path.join(__dirname, '..'); 9 | 10 | createEntry(process.argv.slice(2)) 11 | 12 | function createEntry(args) { 13 | const [entryPath] = args 14 | // Check if directory exists 15 | const entryDirExists = fs.existsSync(entryPath); 16 | if (!entryDirExists) { 17 | // Create directory 18 | fs.ensureDirSync(entryPath); 19 | } 20 | // Check if the target directory is empty 21 | const entryDirIsEmpty = isEmptyDir.sync(entryPath) 22 | if (entryDirIsEmpty) { 23 | // Copy all the files from our ./template directory 24 | const templatePath = path.join(ownPath, 'template'); 25 | if (fs.existsSync(templatePath)) { 26 | fs.copySync(templatePath, entryPath); 27 | console.log(`Success! Created entry at: ${chalk.green(entryPath)}`); 28 | } else { 29 | console.error( 30 | `Could not locate supplied template: ${chalk.green(templatePath)}` 31 | ); 32 | return; 33 | } 34 | } else { 35 | console.error( 36 | `Could not create entry at ${chalk.green(entryPath)} as it contains existing files.` 37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /bin/icelab-assets.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var spawn = require('cross-spawn'); 6 | var script = process.argv[2]; 7 | var args = process.argv.slice(3); 8 | 9 | switch (script) { 10 | case 'build': 11 | case 'create-entry': 12 | case 'start': 13 | case 'test': 14 | var result = spawn.sync( 15 | 'node', 16 | [require.resolve('../scripts/' + script)].concat(args), 17 | { stdio: 'inherit' } 18 | ); 19 | if (result.signal) { 20 | if (result.signal === 'SIGKILL') { 21 | console.log( 22 | 'The build failed because the process exited too early. ' + 23 | 'This probably means the system ran out of memory or someone called ' + 24 | '`kill -9` on the process.' 25 | ); 26 | } else if (result.signal === 'SIGTERM') { 27 | console.log( 28 | 'The build failed because the process exited too early. ' + 29 | 'Someone might have called `kill` or `killall`, or the system could ' + 30 | 'be shutting down.' 31 | ); 32 | } 33 | process.exit(1); 34 | } 35 | process.exit(result.status); 36 | break; 37 | default: 38 | console.log('Unknown script "' + script + '".'); 39 | console.log('Perhaps you need to update icelab-assets?'); 40 | break; 41 | } -------------------------------------------------------------------------------- /scripts/utils/createWebpackCompiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | const chalk = require('chalk'); 6 | const webpack = require('webpack'); 7 | 8 | const isInteractive = process.stdout.isTTY; 9 | let handleCompile; 10 | 11 | module.exports = function createWebpackCompiler(config, onReadyCallback) { 12 | // "Compiler" is a low-level interface to Webpack. 13 | // It lets us listen to some events and provide our own custom messages. 14 | let compiler; 15 | try { 16 | compiler = webpack(config, handleCompile); 17 | } catch (err) { 18 | console.log(chalk.red('Failed to compile.')); 19 | console.log(); 20 | console.log(err.message || err); 21 | console.log(); 22 | process.exit(1); 23 | } 24 | 25 | let isFirstCompile = true; 26 | 27 | // "done" event fires when Webpack has finished recompiling the bundle. 28 | // Whether or not you have warnings or errors, you will get this event. 29 | compiler.plugin('done', stats => { 30 | if (typeof onReadyCallback === 'function') { 31 | // Extract any CSS/JS assets 32 | var assets = stats 33 | .toJson() 34 | .assets.filter(asset => ( 35 | /\.(js|css)$/.test(asset.name) && !/^chunk/.test(asset.name) 36 | )) 37 | .map(asset => { 38 | return asset.name; 39 | }); 40 | onReadyCallback(isFirstCompile, assets); 41 | } 42 | isFirstCompile = false; 43 | }); 44 | 45 | return compiler; 46 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | # v2.0.6 2019-06-24 7 | 8 | * Fix missing plugin reference to ResolveEntryModulesPlugin 9 | 10 | # v2.0.5 2019-06-24 11 | 12 | * Fix deprecation warning in Prettier output 13 | 14 | # v2.0.4 2019-06-24 15 | 16 | * Bump eslint past affected versions for [CVE](https://snyk.io/vuln/npm:eslint:20180222) 17 | 18 | # v2.0.3 2018-06-22 19 | 20 | * Fix top-level absolute requires 21 | 22 | # v2.0.2 2018-06-05 23 | 24 | * Allow `build` to be run without hashing filenames 25 | 26 | # v2.0.1 2018-03-20 27 | 28 | * Supress warnings in postcss-custom-properties (through postcss-cssnext) 29 | 30 | # v2.0.0 2018-03-20 31 | 32 | * Breaking change: Add [`postcss-url`](https://github.com/postcss/postcss-url) to the postcss pipeline. Any `url()` references will need to be updated to be relative paths to the file the reference is included in. 33 | 34 | # v1.0.6 2018-03-14 35 | 36 | * Ensure production CSS is minified. 37 | 38 | # v1.0.5 2018-01-10 39 | 40 | * Add `.ico` to default list of files included in bundle when *not* explicitly included by other files. 41 | 42 | # v1.0.4 2017-12-13 43 | 44 | * Bump webpack deps to avoid issue with child cache. 45 | 46 | # v1.0.3 2017-10-19 47 | 48 | * Set ASSETS_ENV=test to exclude dev-related entry injections. This is useful in fake-browser environments like phantomjs et al. 49 | 50 | # v1.0.2 2017-10-18 51 | 52 | * Update prettier parser value to avoid deprecation warning 53 | 54 | # v1.0.1 2017-09-28 55 | 56 | * Update dependencies across the board. Note: this release disables Hot Module Replacement as it’s not supported by the ExtractTextPlugin (and never really was). 57 | 58 | # v1.0.0 2017-09-27 59 | 60 | * Initial release 61 | -------------------------------------------------------------------------------- /config/devNiceties.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // This is a workaround used alongside the webpack-dev-server hot-module-reload 4 | // feature as it's quite chatty on the console, and there's no currently no 5 | // configuration option to silence it. Only used in development. Prevent 6 | // messages starting with [HMR] or [WDS] from being printed to the console 7 | // when using console.log or console.warn 8 | (function(global) { 9 | var console_log = global.console.log; 10 | global.console.log = function() { 11 | if ( 12 | !(arguments.length == 1 && 13 | typeof arguments[0] === "string" && 14 | arguments[0].match(/^\[(HMR|WDS)\]/)) 15 | ) { 16 | console_log.apply(global.console, arguments); 17 | } 18 | }; 19 | var console_warn = global.console.warn; 20 | global.console.warn = function() { 21 | if ( 22 | !(arguments.length == 1 && 23 | typeof arguments[0] === "string" && 24 | arguments[0].match(/^\[(HMR|WDS)\]/)) 25 | ) { 26 | console_warn.apply(global.console, arguments); 27 | } 28 | }; 29 | 30 | // Automatically reload *extracted* CSS in development when hotUpdates are issued 31 | // https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/30#issuecomment-231010662 32 | global.addEventListener( 33 | "message", 34 | function(e) { 35 | if ( 36 | typeof e.data !== "String" || 37 | e.data.search("webpackHotUpdate") === -1 38 | ) 39 | return; 40 | global.document 41 | .querySelectorAll("link[href][rel=stylesheet]") 42 | .forEach(function(link) { 43 | if (link.href.search("localhost") > -1) { 44 | var nextStyleHref = link.href.replace( 45 | /(\?\d+)?$/, 46 | "?" + Date.now() 47 | ); 48 | link.href = nextStyleHref; 49 | } 50 | }); 51 | }, 52 | false 53 | ); 54 | })(window); 55 | -------------------------------------------------------------------------------- /config/webpackDevServer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require('./webpack.config.dev'); 4 | const paths = require('./paths'); 5 | 6 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 7 | const host = process.env.HOST || 'localhost'; 8 | 9 | module.exports = { 10 | // Enable gzip compression of generated files. 11 | compress: true, 12 | // Silence WebpackDevServer's own logs since they're generally not useful. 13 | // It will still show compile warnings and errors with this setting. 14 | clientLogLevel: 'none', 15 | contentBase: false, 16 | // Allow all CORS requests in development 17 | headers: { "Access-Control-Allow-Origin": "*" }, 18 | // Enable hot reloading server. It will provide /sockjs-node/ endpoint 19 | // for the WebpackDevServer client so it can learn when the files were 20 | // updated. The WebpackDevServer client is included as an entry point 21 | // in the Webpack development configuration. Note that only changes 22 | // to CSS are currently hot reloaded. JS changes will refresh the browser. 23 | // Disabled until the ExtractText plugin supports HMR 24 | // https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/592 25 | hot: false, 26 | // It is important to tell WebpackDevServer to use the same "root" path 27 | // as we specified in the config. In development, we always serve from /. 28 | publicPath: config.output.publicPath, 29 | // WebpackDevServer is noisy by default so we emit custom message instead 30 | // by listening to the compiler events with `compiler.plugin` calls above. 31 | quiet: true, 32 | // Reportedly, this avoids CPU overload on some systems. 33 | // https://github.com/facebookincubator/create-react-app/issues/293 34 | watchOptions: { 35 | ignored: /node_modules/, 36 | }, 37 | // Enable HTTPS if the HTTPS environment variable is set to 'true' 38 | https: protocol === 'https', 39 | host: host, 40 | overlay: false, 41 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icelab-assets", 3 | "version": "2.0.6", 4 | "description": "Configuration and scripts for assets.", 5 | "repository": "icelab/icelab-assets", 6 | "engines": { 7 | "node": ">=4" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/icelab/icelab-assets/issues" 11 | }, 12 | "files": [ 13 | "babelrc", 14 | "bin", 15 | "config", 16 | "eslintrc", 17 | "eslintrc.prod", 18 | "scripts", 19 | "template", 20 | "utils" 21 | ], 22 | "bin": { 23 | "icelab-assets": "./bin/icelab-assets.js" 24 | }, 25 | "dependencies": { 26 | "babel-core": "^6.26.0", 27 | "babel-eslint": "^7.2.3", 28 | "babel-jest": "^20.0.3", 29 | "babel-loader": "^7.1.1", 30 | "babel-preset-react-app": "^3.0.2", 31 | "babel-runtime": "^6.26.0", 32 | "chalk": "^1.1.3", 33 | "cross-spawn": "^4.0.2", 34 | "css-loader": "^0.28.4", 35 | "detect-port": "^1.0.1", 36 | "dotenv": "^4.0.0", 37 | "empty-dir": "^0.2.1", 38 | "eslint": "^4.19.1", 39 | "eslint-config-react-app": "^2.0.0", 40 | "eslint-loader": "^1.9.0", 41 | "eslint-plugin-flowtype": "^2.35.0", 42 | "eslint-plugin-import": "^2.7.0", 43 | "eslint-plugin-jsx-a11y": "^5.1.1", 44 | "eslint-plugin-prettier": "^2.3.1", 45 | "eslint-plugin-react": "^7.1.0", 46 | "extract-text-webpack-plugin": "^3.0.2", 47 | "file-loader": "^0.11.2", 48 | "friendly-errors-webpack-plugin": "^1.6.1", 49 | "fs-extra": "0.30.0", 50 | "glob": "^7.1.1", 51 | "http-proxy-middleware": "0.17.3", 52 | "imports-loader": "^0.7.1", 53 | "jest": "20.0.4", 54 | "minimist": "^1.2.0", 55 | "object-assign": "4.1.1", 56 | "postcss-cssnext": "^3.0.2", 57 | "postcss-import": "^11.0.0", 58 | "postcss-loader": "2.0.6", 59 | "postcss-url": "7.3.1", 60 | "prettier": "^1.7.0", 61 | "prettier-webpack-plugin": "^0.2.2", 62 | "promise": "8.0.1", 63 | "react-dev-utils": "^4.0.1", 64 | "resolve-entry-modules-webpack-plugin": "^1.0.1", 65 | "style-loader": "0.18.2", 66 | "webpack": "3.10.0", 67 | "webpack-dev-server": "2.8.2", 68 | "webpack-manifest-plugin": "1.2.0", 69 | "webpack-merge": "^4.1.1", 70 | "whatwg-fetch": "2.0.3" 71 | }, 72 | "optionalDependencies": { 73 | "fsevents": "1.0.17" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | var glob = require('glob'); 6 | const url = require('url'); 7 | const parseArgs = require('minimist'); 8 | const argsv = parseArgs(process.argv.slice(2)) 9 | 10 | // Make sure any symlinks in the project folder are resolved: 11 | // https://github.com/facebookincubator/create-react-app/issues/637 12 | const appDirectory = fs.realpathSync(process.cwd()); 13 | function resolveApp(relativePath) { 14 | return path.resolve(appDirectory, relativePath); 15 | } 16 | 17 | // We support resolving modules according to `NODE_PATH`. 18 | // This lets you use absolute paths in imports inside large monorepos: 19 | // https://github.com/facebookincubator/create-react-app/issues/253. 20 | 21 | // It works similar to `NODE_PATH` in Node itself: 22 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 23 | 24 | // We will export `nodePaths` as an array of absolute paths. 25 | // It will then be used by Webpack configs. 26 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 27 | 28 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 29 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 30 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 31 | 32 | const nodePaths = (process.env.NODE_PATH || '') 33 | .split(process.platform === 'win32' ? ';' : ':') 34 | .filter(Boolean) 35 | .filter(folder => !path.isAbsolute(folder)) 36 | .map(resolveApp); 37 | 38 | const envPublicUrl = process.env.PUBLIC_URL; 39 | 40 | function resolveOwn(relativePath) { 41 | return path.resolve(__dirname, '..', relativePath); 42 | } 43 | 44 | // Set the base folder for app code 45 | const appSrc = resolveApp(argsv['source-path'] || 'apps'); 46 | 47 | // Find each app within the base appSrc directory 48 | const appEntryDirs = [] 49 | const appEntries = glob.sync(`${appSrc}/*`) 50 | // Then find the entries in each app and reate a set of entries with a 51 | // consistent naming convention so we can easily reference in templates: 52 | // `${appName}__${entryName}` 53 | .map((dir) => { 54 | const appName = path.basename(dir) 55 | const entries = glob.sync(dir + "/**/entry.js") 56 | return entries.map((entry) => { 57 | // Capture the parent dir for appEntryDirs at the same time 58 | appEntryDirs.push(path.dirname(entry)) 59 | const entryName = path.basename(path.dirname(entry)) 60 | return [`${appName}__${entryName}`, entry] 61 | }) 62 | }) 63 | // Flatten 64 | .reduce((a, b) => a.concat(b), []); 65 | 66 | // We're in ./node_modules/icelab-assets/config/ 67 | module.exports = { 68 | appPath: resolveApp('.'), 69 | // Location to build to 70 | appBuild: resolveApp(argsv['build-path'] || 'public/assets'), 71 | appPackageJson: resolveApp('package.json'), 72 | appWebpackConfigDev: resolveApp('webpack.config.dev.js'), 73 | appWebpackConfigProd: resolveApp('webpack.config.prod.js'), 74 | appSrc: appSrc, 75 | // Where does the code sit? 76 | // This doesn’t actually export a path but it still feels like 77 | // right place for this stuff 78 | appEntries: appEntries, 79 | appEntryDirs: appEntryDirs, 80 | appNodeModules: resolveApp('node_modules'), 81 | nodePaths: nodePaths, 82 | ownNodeModules: resolveOwn('node_modules'), // This is empty on npm 3 83 | // The served public path 84 | publicPath: argsv['public-path'] || '/assets/', 85 | // testsSetup: resolveApp('src/setupTests.js'), 86 | yarnLockFile: resolveApp('yarn.lock'), 87 | }; -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | // Load environment variables from .env file. Suppress warnings using silent 6 | // if this file is missing. dotenv will never modify any environment variables 7 | // that have already been set. 8 | // https://github.com/motdotla/dotenv 9 | require('dotenv').config({ silent: true }); 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const chalk = require('chalk'); 14 | const detect = require('detect-port'); 15 | const merge = require('webpack-merge'); 16 | const WebpackDevServer = require('webpack-dev-server'); 17 | const clearConsole = require('react-dev-utils/clearConsole'); 18 | const getProcessForPort = require('react-dev-utils/getProcessForPort'); 19 | const paths = require('../config/paths'); 20 | let config = require('../config/webpack.config.dev'); 21 | const devServerConfig = require('../config/webpackDevServer.config'); 22 | const createWebpackCompiler = require('./utils/createWebpackCompiler'); 23 | 24 | const useYarn = fs.existsSync(paths.yarnLockFile); 25 | const cli = useYarn ? 'yarn' : 'npm'; 26 | const isInteractive = process.stdout.isTTY; 27 | 28 | const hasAppConfig = fs.existsSync(paths.appWebpackConfigDev); 29 | 30 | const DEFAULT_PORT = parseInt(process.env.ASSETS_PORT, 10) || 8080; 31 | 32 | function run(port) { 33 | const protocol = process.env.ASSETS_HTTPS === 'true' ? 'https' : 'http'; 34 | const host = process.env.ASSETS_HOST || 'localhost'; 35 | 36 | // Merge configurations using webpack-merge default smart strategy 37 | // https://github.com/survivejs/webpack-merge 38 | if (hasAppConfig) { 39 | config = merge.smart(config, require(paths.appWebpackConfigDev)) 40 | } 41 | // Create a webpack compiler that is configured with custom messages. 42 | const compiler = createWebpackCompiler( 43 | config, 44 | function onReady(showInstructions, assets) { 45 | if (!showInstructions) { 46 | return; 47 | } 48 | const serverUrl = `${protocol}://${host}:${port}` 49 | console.log(); 50 | console.log('The assets server is running at:'); 51 | console.log(); 52 | console.log(` ${chalk.cyan(`${serverUrl}`)}`); 53 | console.log(); 54 | if (assets.length > 0) { 55 | console.log('Building these CSS/JS files:'); 56 | console.log(); 57 | assets.forEach(asset => { 58 | console.log(` ${chalk.cyan(`${serverUrl}${paths.publicPath}${asset}`)}`); 59 | }); 60 | } 61 | console.log(); 62 | console.log('Note that the development build is not optimized.'); 63 | console.log( 64 | `To create a production build, use ${chalk.cyan(`${cli} run build`)}.` 65 | ); 66 | console.log(); 67 | } 68 | ); 69 | 70 | // Serve webpack assets generated by the compiler over a web sever. 71 | const devServer = new WebpackDevServer(compiler, devServerConfig); 72 | 73 | // Launch WebpackDevServer. 74 | devServer.listen(port, err => { 75 | if (err) { 76 | return console.log(err); 77 | } 78 | 79 | if (isInteractive) { 80 | clearConsole(); 81 | } 82 | console.log(chalk.cyan('Starting the development server...')); 83 | console.log(); 84 | }); 85 | } 86 | 87 | // We attempt to use the default port but if it is busy, we offer the user to 88 | // run on a different port. `detect()` Promise resolves to the next free port. 89 | detect(DEFAULT_PORT).then(port => { 90 | if (port === DEFAULT_PORT) { 91 | run(port); 92 | return; 93 | } 94 | console.log( 95 | chalk.red(`Something is already running on port ${DEFAULT_PORT}.`) 96 | ); 97 | }); -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.NODE_ENV = 'production'; 5 | 6 | // Load environment variables from .env file. Suppress warnings using silent 7 | // if this file is missing. dotenv will never modify any environment variables 8 | // that have already been set. 9 | // https://github.com/motdotla/dotenv 10 | require('dotenv').config({ silent: true }); 11 | 12 | const chalk = require('chalk'); 13 | const fs = require('fs-extra'); 14 | const path = require('path'); 15 | const url = require('url'); 16 | const webpack = require('webpack'); 17 | const merge = require('webpack-merge'); 18 | let config = require('../config/webpack.config.prod'); 19 | const paths = require('../config/paths'); 20 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 21 | 22 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 23 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 24 | const useYarn = fs.existsSync(paths.yarnLockFile); 25 | 26 | const hasAppConfig = fs.existsSync(paths.appWebpackConfigProd); 27 | 28 | // These sizes are pretty large. We'll warn for bundles exceeding them. 29 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 30 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 31 | 32 | // First, read the current file sizes in build directory. 33 | // This lets us display how much they changed later. 34 | measureFileSizesBeforeBuild(paths.appBuild).then(previousFileSizes => { 35 | // Remove all content but keep the directory so that 36 | // if you're in it, you don't end up in Trash 37 | fs.emptyDirSync(paths.appBuild); 38 | 39 | // Start the webpack build 40 | build(previousFileSizes); 41 | }); 42 | 43 | // Print out errors 44 | function printErrors(summary, errors) { 45 | console.log(chalk.red(summary)); 46 | console.log(); 47 | errors.forEach(err => { 48 | console.log(err.message || err); 49 | console.log(); 50 | }); 51 | } 52 | 53 | // Create the production build and print the deployment instructions. 54 | function build(previousFileSizes) { 55 | console.log('Creating an optimized production build...'); 56 | let compiler; 57 | // Merge configurations using webpack-merge default smart strategy 58 | // https://github.com/survivejs/webpack-merge 59 | if (hasAppConfig) { 60 | config = merge.smart(config, require(paths.appWebpackConfigProd)) 61 | } 62 | try { 63 | compiler = webpack(config); 64 | } catch (err) { 65 | printErrors('Failed to compile.', [err]); 66 | process.exit(1); 67 | } 68 | 69 | compiler.run((err, stats) => { 70 | if (err) { 71 | printErrors('Failed to compile.', [err]); 72 | process.exit(1); 73 | } 74 | 75 | if (stats.compilation.errors.length) { 76 | printErrors('Failed to compile.', stats.compilation.errors); 77 | process.exit(1); 78 | } 79 | 80 | if (process.env.CI && stats.compilation.warnings.length) { 81 | printErrors( 82 | 'Failed to compile. When process.env.CI = true, warnings are treated as failures. Most CI servers set this automatically.', 83 | stats.compilation.warnings 84 | ); 85 | process.exit(1); 86 | } 87 | 88 | console.log(chalk.green('Compiled successfully.')); 89 | console.log(); 90 | 91 | console.log('File sizes after gzip:'); 92 | console.log(); 93 | // This incorrectly prints the output directory as `build` but its tied up in react-dev-utils 94 | // and it seems not worth replicating simply to avoid that. 95 | printFileSizesAfterBuild( 96 | stats, 97 | previousFileSizes, 98 | paths.appBuild, 99 | WARN_AFTER_BUNDLE_GZIP_SIZE, 100 | WARN_AFTER_CHUNK_GZIP_SIZE 101 | ); 102 | console.log(); 103 | 104 | const openCommand = process.platform === 'win32' ? 'start' : 'open'; 105 | const appPackage = require(paths.appPackageJson); 106 | const buildPath = paths.appBuild.replace(new RegExp(`^${paths.appPath}`), ''); 107 | const publicPath = config.output.publicPath; 108 | const publicPathname = url.parse(publicPath).pathname; 109 | console.log( 110 | `Build complete in ${chalk.green(buildPath)} assuming they'll be served from ${chalk.green(publicPath)}.` 111 | ); 112 | console.log(); 113 | }); 114 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Icelab Assets 2 | 3 | An opinionated asset setup for Icelab projects. 4 | 5 | ## Installation 6 | 7 | Add `icelab-assets` as a `devDependency` to your app with one of the below tasks: 8 | 9 | ``` 10 | npm install --save-dev icelab/icelab-assets 11 | yarn add --dev icelab/icelab-assets 12 | ``` 13 | 14 | Then add this config to the `scripts` section of your `package.json` file: 15 | 16 | ``` 17 | "scripts": { 18 | "start": "icelab-assets start", 19 | "build": "icelab-assets build", 20 | "test": "icelab-assets test", 21 | "create-entry": "icelab-assets create-entry" 22 | } 23 | ``` 24 | 25 | ## Usage 26 | 27 | Once you’re set up, you’ll have four tasks available that you can run with either `npm run` or `yarn run`: 28 | 29 | * `start` will boot a development server at which will watch your asset files and rebuild on the fly. 30 | * `build` will create a production-ready build for each of your asset entry points. 31 | * `test` will use [jest](https://facebook.github.io/jest/) to run the tests for your assets. 32 | * `create-entry` allows you to create a new asset entry point at a directory path: `yarn run create-entry apps/path/to/the/entry` 33 | 34 | By default, `icelab-assets` looks for asset entry points at `apps/**/**/entry.js` and will generate a name for that entry based on its parent directories using this basic formula: `apps/:app_name/**/:entry_name/`. Thus, given the following entry points (and assuming they only return JavaScript): 35 | 36 | ``` 37 | apps/admin/assets/admin/entry.js 38 | apps/admin/assets/inline/entry.js 39 | apps/main/assets/public/entry.js 40 | apps/main/assets/nested/deeply/for/some/reason/critical/entry.js 41 | ``` 42 | 43 | These output files would be generated: 44 | 45 | ``` 46 | admin__admin.js 47 | admin__inline.js 48 | main__public.js 49 | main__critical.js 50 | ``` 51 | 52 | Note: entry names that begin with `inline` are assumed to be files that will eventually be included inline in HTML output. These files thus exclude some dev-related niceties like hot-reloading to avoid clogging up the generated HTML. 53 | 54 | ## Configuration 55 | 56 | ### Paths 57 | 58 | You can override default path options using the following command line args: 59 | 60 | * `--source-path` — defaults to `apps` 61 | * `--build-path` — defaults to `public/assets` 62 | * `--public-path` — defaults to `/assets/` 63 | 64 | If you were using this in conjunction with WordPress for example, you might do something like: 65 | 66 | ```json 67 | "scripts": { 68 | "start": "icelab-assets start --source-path=wp-content/themes", 69 | "build": "icelab-assets build --source-path=wp-content/themes", 70 | "test": "icelab-assets test --source-path=wp-content/themes" 71 | } 72 | ``` 73 | 74 | This would traverse within `wp-content/themes` for any `entry.js` files and use them as the entry points for the build. 75 | 76 | ### Webpack configurations 77 | 78 | You can adjust the webpack configuration for either development or production environments by creating a matching config file in the root of your application: 79 | 80 | ``` 81 | webpack.config.dev.js 82 | webpack.config.prod.js 83 | ``` 84 | 85 | If these files exist, they’ll be merged with the default configuration using the default “smart” strategy from [webpack-merge](https://github.com/survivejs/webpack-merge). You’ll need to match the form of the [existing](config/webpack.config.dev.js) [config](config/webpack.config.prod.js) files to get the to merge matching loaders (including the `include` values for example). 86 | 87 | Here’s a custom config that’ll add a `foobar` loader to the pipeline for both CSS and JavaScript: 88 | 89 | ``` 90 | var path = require('path'); 91 | 92 | module.exports = { 93 | module: { 94 | rules: [ 95 | { 96 | test: /\.(js|jsx)$/, 97 | // Needs to match the `include` value in the default configuration 98 | include: path.resolve('apps'), 99 | use: [{ 100 | loader: 'awesome-loader' 101 | }] 102 | }, 103 | { 104 | test: /\.css$/, 105 | use: [{ 106 | loader: 'awesome-loader' 107 | }] 108 | }, 109 | ], 110 | } 111 | } 112 | ``` 113 | 114 | This would append the theoretical `awesome-loader` loader to the end of the pipeline in both cases. 115 | 116 | ### Hashing 117 | 118 | Assets are hashed by the `build` script by default. This can be disabled by calling the script with `ASSETS_HASH_FILENAMES=false`: 119 | 120 | ``` 121 | ASSETS_HASH_FILENAMES=false icelab-assets build 122 | ``` 123 | 124 | ## CSS 125 | 126 | We post-process CSS using [CSS Next](http://cssnext.io) so all the features listed there are available to us. The top-line niceties are: 127 | 128 | * Autoprefixing 129 | * CSS variables 130 | * Nested selectors 131 | * Colour functions 132 | 133 | We also add support for importing CSS inline using `@import` via [postcss-import](https://github.com/postcss/postcss-import). Something to note about this is that _all_ references in CSS must be made relative to the root of the current entry — this includes `@import` and `url` references: 134 | 135 | ```css 136 | /** 137 | * Given a file structure like: 138 | * 139 | * /entry-name 140 | * /foo 141 | * index.js 142 | * image.jpg 143 | * 144 | * A reference to `image.jpg` must include the parent path. 145 | */ 146 | .foo { 147 | background-image: url('foo/image.jpg'); 148 | } 149 | ``` 150 | 151 | ## JavaScript 152 | 153 | ### Language features and polyfills 154 | 155 | We support a superset of the latest JavaScript standard. In addition to [ES6](https://github.com/lukehoban/es6features) syntax features, we also supports: 156 | 157 | * [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016). 158 | * [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017). 159 | * [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal). 160 | * [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (stage 2 proposal). 161 | * [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax. 162 | 163 | Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-). 164 | 165 | Note that **the project only includes a few ES6 [polyfills](https://en.wikipedia.org/wiki/Polyfill)**: 166 | 167 | * [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) via [`object-assign`](https://github.com/sindresorhus/object-assign). 168 | * [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) via [`promise`](https://github.com/then/promise). 169 | * [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) via [`whatwg-fetch`](https://github.com/github/fetch). 170 | 171 | If you use any other ES6+ features that need **runtime support** (such as `Array.from()` or `Symbol`), make sure you are including the appropriate polyfills manually, or that the browsers you are targeting already support them. 172 | 173 | #### Top-level entry point import resolution 174 | 175 | We’ve added support for top-level resolution of `import`ed modules. This means that you can specify an import as though you are at the top-level of each entry. For example: 176 | 177 | ```js 178 | // Given a file at ./entry-folder/foo/bar/index.js 179 | export default function bar () { 180 | // ... do something 181 | } 182 | // From entry-folder/foo/index.js we can do a relative call as usual: 183 | import bar from "./bar"; 184 | // ... or scope it from the root of the `entry-folder` 185 | import bar from "foo/bar"; 186 | ``` 187 | 188 | ### Code splitting 189 | 190 | We support code splitting in babel using the [syntax-dynamic-import](http://babeljs.io/docs/plugins/syntax-dynamic-import/) plugin which allows you to, as the name suggests, dynamically import modules. Here’s an example: 191 | 192 | ```js 193 | // determine-date/index.js 194 | export default function determineDate() { 195 | import('moment') 196 | .then(moment => moment().format('LLLL')) 197 | .then(str => console.log(str)) 198 | .catch(err => console.log('Failed to load moment', err)); 199 | } 200 | 201 | // other-file/index.js 202 | import determineDate from 'determine-date' 203 | determineDate(); // moment won't be loaded until this line is _executed_ 204 | ``` 205 | 206 | You can use this method for imports within your application too, they’re not restricted to external modules. 207 | 208 | The build process will automatically split any dynamic imports out into separate chunks, and the bundled JavaScript will load them on the fly without you having to do anything more. You may notice them as `0.abc1234.chunk.js` files in your final build. This should mean smaller initial payloads for apps, but it’s worth considering that separate chunks will potentially take time to load, and you’ll need to have your UI sympathetic to that. 209 | 210 | ### Code linting and formatting 211 | 212 | JS code in your source paths will be linted using [eslint](http://eslint.org). We use the base config from create-react-app, which is released as a separate package called [eslint-config-react-app](https://www.npmjs.com/package/eslint-config-react-app), and then add some small adjustments in each environment: 213 | 214 | * In development, we include [prettier](https://github.com/prettier/prettier) to enforce a code style across our projects. This integration will automatically format both JS and CSS whenever you change files. 215 | * In production, we do not include prettier so it will not break your builds. We do however add a `no-debugger` rule to ensure that you can’t push production code that includes debugging lines. 216 | 217 | If you’re using an eslint plugin/extension in your editor, you’ll need to configure it to read the `icelab-assets` configuration as its hidden within the package. For Visual Studio code you can add a workspace-specific configuration that looks like this: 218 | 219 | ```js 220 | // .vscode/settings.json 221 | // Place your settings in this file to overwrite default and user settings. 222 | { 223 | // Custom eslint config 224 | "eslint.options": { 225 | "configFile": "./node_modules/icelab-assets/eslintrc" 226 | }, 227 | "eslint.nodePath": "./node_modules/icelab-assets/node_modules" 228 | } 229 | ``` 230 | 231 | Once that’s integrated, you should be able to use eslint’s "Fix all auto-fixable problems" command to fix and format your code with prettier. 232 | 233 | ## PhantomJS usage 234 | 235 | There are some dev-related packages injected into the development build that aren’t relevant in testing environments and these can cause issues with PhantomJS. If you want to exclude them you’ll need to set: 236 | 237 | ``` 238 | ASSETS_ENV=test 239 | ``` 240 | 241 | In your ENV (either using `.env`) or when you start the development server. This is only relevant for *development*, production builds do not include these packages. 242 | 243 | ## TODOs 244 | 245 | - [ ] [Tree shaking doesn’t work at the moment](https://github.com/facebookincubator/create-react-app/pull/1742), alas. Once it’s sorted in `create-react-app` we should be able to pull it in automatically. 246 | - [ ] Enable relative import paths for CSS references (postcss-import only supports root-level resolution). 247 | 248 | ## Credits 249 | 250 | The structure, concept, and most of the code from [create-react-app](https://github.com/facebookincubator/create-react-app) forms the basis for this repo. We still leverage a bunch of stuff from that project so that we’re providing stable and ongoing improvements. 251 | 252 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const ManifestPlugin = require('webpack-manifest-plugin'); 7 | const ResolveEntryModulesPlugin = require("resolve-entry-modules-webpack-plugin"); 8 | const atImport = require('postcss-import'); 9 | const postcssURL = require("postcss-url"); 10 | const cssNext = require('postcss-cssnext'); 11 | const paths = require('./paths'); 12 | const getClientEnvironment = require('./env'); 13 | 14 | // Webpack uses `publicPath` to determine where the app is being served from. 15 | // It requires a trailing slash, or the file assets will get an incorrect path. 16 | const publicPath = paths.publicPath; 17 | // Some apps do not use client-side routing with pushState. 18 | // For these, "homepage" can be set to "." to enable relative asset paths. 19 | const shouldUseRelativeAssetPaths = publicPath === './'; 20 | // Get environment variables to inject into our app. 21 | const env = getClientEnvironment(); 22 | 23 | // Assert this just to be safe. 24 | // Development builds of React are slow and not intended for production. 25 | if (env.stringified['process.env'].NODE_ENV !== '"production"') { 26 | throw new Error('Production builds must have NODE_ENV=production.'); 27 | } 28 | 29 | // Allow hashing to be disabled by passing an ENV 30 | let hashFilenames = true; 31 | if ( 32 | env.stringified['process.env'].ASSETS_HASH_FILENAMES 33 | && env.stringified['process.env'].ASSETS_HASH_FILENAMES === '"false"' 34 | ) { 35 | hashFilenames = false; 36 | } 37 | 38 | // Note: defined here because it will be used more than once. 39 | const cssFilename = hashFilenames ? '[name].[contenthash:8].css' : '[name].css'; 40 | 41 | // ExtractTextPlugin expects the build output to be flat. 42 | // (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27) 43 | // However, our output is structured with css, js and media folders. 44 | // To have this structure working with relative paths, we have to use custom options. 45 | const extractTextPluginOptions = shouldUseRelativeAssetPaths 46 | ? // Making sure that the publicPath goes back to to build folder. 47 | { publicPath: Array(cssFilename.split('/').length).join('../') } 48 | : {}; 49 | 50 | // This is the production configuration. 51 | // It compiles slowly and is focused on producing a fast and minimal bundle. 52 | // The development configuration is different and lives in a separate file. 53 | module.exports = { 54 | // Don't attempt to continue if there are any errors. 55 | bail: true, 56 | // We generate sourcemaps in production. This is slow but gives good results. 57 | // You can exclude the *.map files from the build during deployment. 58 | // devtool: 'source-map', 59 | // Specify the context we expect app code to be loaded from 60 | context: paths.appSrc, 61 | // In production, we only want to load the polyfills and the app code. 62 | entry: paths.appEntries.reduce((output, entry) => { 63 | const [name, location] = entry 64 | output[name] = [ 65 | // We ship a few polyfills by default 66 | require.resolve('./polyfills'), 67 | location, 68 | ] 69 | return output 70 | }, {}), 71 | output: { 72 | // The build folder. 73 | path: paths.appBuild, 74 | // Generated JS file names (with nested folders). 75 | // There will be one main bundle, and one file per asynchronous chunk. 76 | // We don't currently advertise code splitting but Webpack supports it. 77 | filename: hashFilenames ? '[name].[chunkhash:8].js' : '[name].js', 78 | chunkFilename: hashFilenames ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js', 79 | // We inferred the "public path" (such as / or /my-project) from homepage. 80 | publicPath: publicPath, 81 | }, 82 | resolve: { 83 | // This allows you to set a fallback for where Webpack should look for modules. 84 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. 85 | // We placed these paths second because we want `node_modules` to "win" 86 | // if there are any conflicts. This matches Node resolution mechanism. 87 | // https://github.com/facebookincubator/create-react-app/issues/253 88 | modules: ['node_modules'] 89 | .concat(paths.nodePaths), 90 | // These are the reasonable defaults supported by the Node ecosystem. 91 | // We also include JSX as a common component filename extension to support 92 | // some tools, although we do not recommend using it, see: 93 | // https://github.com/facebookincubator/create-react-app/issues/290 94 | extensions: ['.js', '.json', '.jsx'], 95 | }, 96 | // Resolve loaders (webpack plugins for CSS, images, transpilation) from the 97 | // directory of `icelab-assets` itself rather than the project directory. 98 | resolveLoader: { 99 | modules: [ 100 | paths.ownNodeModules, 101 | paths.appNodeModules, 102 | ], 103 | }, 104 | module: { 105 | rules: [ 106 | // Disable require.ensure as it's not a standard language feature. 107 | { parser: { requireEnsure: false } }, 108 | // First, run the linter. 109 | // It's important to do this before Babel processes the JS. 110 | { 111 | test: /\.(js|jsx)$/, 112 | enforce: 'pre', 113 | use: [ 114 | { 115 | // Point ESLint to our predefined config. 116 | options: { 117 | // Separate config for production to enable no-debugger only in 118 | // production. 119 | configFile: path.join(__dirname, '../eslintrc.prod'), 120 | useEslintrc: false, 121 | }, 122 | loader: 'eslint-loader', 123 | }, 124 | ], 125 | include: paths.appSrc, 126 | }, 127 | // ** ADDING/UPDATING LOADERS ** 128 | // The "url" loader handles all assets unless explicitly excluded. 129 | // The `exclude` list *must* be updated with every change to loader extensions. 130 | // When adding a new loader, you must add its `test` 131 | // as a new entry in the `exclude` list in the "url" loader. 132 | 133 | // "file" loader makes sure those assets end up in the `build` folder. 134 | // When you `import` an asset, you get its filename. 135 | { 136 | exclude: [ 137 | /\.html$/, 138 | /\.(js|jsx)$/, 139 | /\.css$/, 140 | /\.json$/ 141 | ], 142 | use: [{ 143 | loader: 'file-loader', 144 | options: { 145 | name: hashFilenames ? '[path][name].[hash:8].[ext]' : '[path][name].[ext]', 146 | }, 147 | }], 148 | }, 149 | // Process JS with Babel. 150 | { 151 | test: /\.(js|jsx)$/, 152 | include: paths.appSrc, 153 | use: [{ 154 | loader: 'babel-loader', 155 | options: { 156 | babelrc: false, 157 | presets: [require.resolve('babel-preset-react-app')], 158 | plugins: [require.resolve('babel-plugin-syntax-dynamic-import')], 159 | }, 160 | }], 161 | }, 162 | // The notation here is somewhat confusing. 163 | // "postcss" loader applies autoprefixer to our CSS. 164 | // "css" loader resolves paths in CSS and adds assets as dependencies. 165 | // "style" loader normally turns CSS into JS modules injecting