├── .gitignore ├── README.md ├── bin └── react-scripts-x.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | .DS_Store 5 | lerna-debug.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | /.changelog -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | For now it's just a simple way to inject `postcss` plugins. 2 | 3 | In the future it might become a full-blown module to define extensions for Create React App. 4 | 5 | ```js 6 | { 7 | // ... 8 | "devDependencies": { 9 | // ... 10 | "postcss-calc": "7.0.1", 11 | "postcss-custom-properties": "8.0.9", 12 | "postcss-import": "12.0.1", 13 | "react-scripts": "3.0.0", 14 | "react-scripts-x": "1.0.0", 15 | // ... 16 | }, 17 | // ... 18 | "react-scripts-x": { 19 | "postcss": [ 20 | { 21 | "name": "postcss-import" 22 | }, 23 | { 24 | "name": "postcss-custom-properties", 25 | "config": { 26 | "preserve": true 27 | } 28 | }, 29 | { 30 | "name": "postcss-calc" 31 | } 32 | ] 33 | }, 34 | "scripts": { 35 | "start": "react-scripts-x start", 36 | "test": "react-scripts-x test", 37 | "build": "react-scripts-x build" 38 | } 39 | } 40 | ``` 41 | 42 | # Changelog 43 | 44 | ## 1.0.0 (April 26, 2019) 45 | 46 | Fixed for `react-scripts` >= 3. Continue using `react-scripts-x@0.1.3` for `react-scripts` < 3. 47 | -------------------------------------------------------------------------------- /bin/react-scripts-x.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const crypto = require('crypto'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const equal = require('deep-equal'); 8 | const spawn = require('react-dev-utils/crossSpawn'); 9 | 10 | const appDirectory = fs.realpathSync(process.cwd()); 11 | const appDescriptor = require(path.resolve(appDirectory, 'package')); 12 | const extensionsConfig = appDescriptor['react-scripts-x']; 13 | const reactScriptsDir = path.dirname(require.resolve('react-scripts/package')); 14 | const reactScriptsDescriptor = require('react-scripts/package'); 15 | const moduleDescriptor = require('react-scripts-x/package'); 16 | 17 | const pathTo = { 18 | webpackConfig: path.resolve(reactScriptsDir, 'config', 'webpack.config.js'), 19 | webpackOriginalConfig: path.resolve(reactScriptsDir, 'config', 'webpack.config.original.js'), 20 | changeLog: path.resolve(reactScriptsDir, 'react-scripts-x.json') 21 | }; 22 | 23 | function findArrayEnd(string, start) { 24 | for (let i = start, bracketDelta = 0; i < string.length; i++) { 25 | if (string.charAt(i) === '[') { 26 | bracketDelta++; 27 | } else if (string.charAt(i) === ']') { 28 | bracketDelta--; 29 | } 30 | 31 | if (bracketDelta === 0) { 32 | return i; 33 | } 34 | } 35 | 36 | return -1; 37 | } 38 | 39 | function md5(string) { 40 | const generator = crypto.createHash('md5'); 41 | generator.update(string); 42 | return generator.digest('hex'); 43 | } 44 | 45 | function changeAndReturnWebpackConfig(pathToConfig, postcssPlugins) { 46 | let configContents; 47 | 48 | if (fs.existsSync(pathToConfig + '.original')) { 49 | configContents = fs.readFileSync(pathToConfig + '.original', 'utf8'); 50 | fs.writeFileSync(pathToConfig, configContents, 'utf8'); 51 | } else { 52 | configContents = fs.readFileSync(pathToConfig, 'utf8'); 53 | fs.writeFileSync(pathToConfig + '.original', configContents, 'utf8'); 54 | } 55 | 56 | const postcssBlockStart = configContents.indexOf("require.resolve('postcss-loader')"); 57 | 58 | if (postcssBlockStart < 0) { 59 | console.error(`Failed to parse Webpack config: ${pathToConfig}`); 60 | return configContents; 61 | } 62 | 63 | const pluginArrayStartString = 'plugins: () => ['; 64 | const pluginArrayStart = configContents.indexOf(pluginArrayStartString, postcssBlockStart); 65 | 66 | if (pluginArrayStart < 0) { 67 | console.error(`Failed to parse Webpack config: ${pathToConfig}`); 68 | return configContents; 69 | } 70 | 71 | const pluginArrayEnd = findArrayEnd(configContents, pluginArrayStart + pluginArrayStartString.length - 1); 72 | 73 | if (pluginArrayEnd < 0) { 74 | console.error(`Failed to parse Webpack config: ${pathToConfig}`); 75 | return configContents; 76 | } 77 | 78 | const changedConfigContents = 79 | configContents.slice(0, pluginArrayEnd) 80 | 81 | + 82 | 83 | postcssPlugins.map(plugin => { 84 | if (plugin.config) { 85 | return `require('${plugin.name}')(${JSON.stringify(plugin.config)})`; 86 | } 87 | 88 | return `require('${plugin.name}')`; 89 | }).join() 90 | 91 | + 92 | 93 | configContents.slice(pluginArrayEnd); 94 | 95 | fs.writeFileSync(pathToConfig, changedConfigContents, 'utf8'); 96 | 97 | return changedConfigContents; 98 | } 99 | 100 | function applyExtensions() { 101 | console.info('Applying extensions'); 102 | 103 | fs.writeFileSync(pathTo.changeLog, JSON.stringify({ 104 | version: { 105 | 'react-scripts': reactScriptsDescriptor.version, 106 | 'react-scripts-x': moduleDescriptor.version 107 | }, 108 | files: { 109 | [path.relative(reactScriptsDir, pathTo.webpackConfig)]: { 110 | hash: md5(changeAndReturnWebpackConfig(pathTo.webpackConfig, extensionsConfig.postcss)), 111 | changes: extensionsConfig 112 | } 113 | } 114 | }, null, 2), 'utf8'); 115 | } 116 | 117 | if (extensionsConfig && extensionsConfig.postcss) { 118 | if (fs.existsSync(pathTo.changeLog)) { 119 | const changeLog = require(pathTo.changeLog); 120 | 121 | if (changeLog.version['react-scripts'] !== reactScriptsDescriptor.version) { 122 | applyExtensions(); 123 | } else if (changeLog.version['react-scripts-x'] !== moduleDescriptor.version) { 124 | applyExtensions(); 125 | } else if (Object.keys(changeLog.files) 126 | .filter(file => equal(changeLog.files[file].changes, extensionsConfig)) 127 | .filter(file => changeLog.files[file].hash === md5(fs.readFileSync(path.resolve(reactScriptsDir, file), 'utf8'))) 128 | .length !== Object.keys(changeLog.files).length) { 129 | applyExtensions(); 130 | } 131 | } else { 132 | applyExtensions(); 133 | } 134 | } 135 | 136 | const args = process.argv.slice(2); 137 | const result = spawn.sync( 138 | 'node', 139 | [require.resolve('react-scripts/bin/react-scripts')].concat(args), 140 | { stdio: 'inherit' } 141 | ); 142 | 143 | if (result.signal) { 144 | if (result.signal === 'SIGKILL') { 145 | console.log( 146 | 'The build failed because the process exited too early. ' + 147 | 'This probably means the system ran out of memory or someone called ' + 148 | '`kill -9` on the process.' 149 | ); 150 | } else if (result.signal === 'SIGTERM') { 151 | console.log( 152 | 'The build failed because the process exited too early. ' + 153 | 'Someone might have called `kill` or `killall`, or the system could ' + 154 | 'be shutting down.' 155 | ); 156 | } 157 | 158 | process.exit(1); 159 | } 160 | 161 | process.exit(result.status); 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scripts-x", 3 | "version": "1.0.0", 4 | "description": "Extensions for Create React App build scripts.", 5 | "repository": "mihhail-lapushkin/react-scripts-x", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">=8" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/mihhail-lapushkin/react-scripts-x/issues" 12 | }, 13 | "files": [ 14 | "bin" 15 | ], 16 | "bin": { 17 | "react-scripts-x": "./bin/react-scripts-x.js" 18 | }, 19 | "dependencies": { 20 | "deep-equal": "1.0.1" 21 | }, 22 | "devDependencies": { 23 | "react-scripts": "3.0.0" 24 | }, 25 | "peerDependencies": { 26 | "react-scripts": ">=3.0.0" 27 | } 28 | } 29 | --------------------------------------------------------------------------------