├── .travis.yml ├── src ├── .eslintrc ├── test.js ├── styles.scss └── index.js ├── example ├── demo.gif ├── src │ ├── index.js │ ├── helpers.js │ ├── index.css │ ├── mocks.js │ └── App.js ├── README.md ├── public │ ├── manifest.json │ └── index.html ├── config │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── pnpTs.js │ ├── modules.js │ ├── paths.js │ ├── env.js │ ├── webpackDevServer.config.js │ └── webpack.config.js ├── scripts │ ├── test.js │ ├── start.js │ └── build.js └── package.json ├── .babelrc ├── .editorconfig ├── .gitignore ├── .eslintrc ├── .github └── FUNDING.yml ├── rollup.config.js ├── LICENSE ├── package.json ├── CONTRIBUTING.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | - 8 5 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dsternlicht/react-dynamic-charts/HEAD/example/demo.gif -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }], 6 | "stage-0", 7 | "react" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import ExampleComponent from './' 2 | 3 | describe('ExampleComponent', () => { 4 | it('is truthy', () => { 5 | expect(ExampleComponent).toBeTruthy() 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import './index.css' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.getElementById('root')) 8 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-dynamic-charts - example app 2 | 3 | ## Installing dependencies 4 | 5 | `npm i` 6 | 7 | ## Running 8 | 9 | `npm start` 10 | 11 | The app will be available by default in **port 3000**. -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-dynamic-charts", 3 | "name": "react-dynamic-charts", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /example/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "env": { 8 | "es6": true 9 | }, 10 | "plugins": [ 11 | "react" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "space-before-function-paren": 0, 18 | "react/jsx-boolean-value": 0, 19 | "semi": 0, 20 | "jsx-quotes": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | react-dynamic-charts 11 | 12 | 13 | 14 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dsternlicht] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /example/config/pnpTs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { resolveModuleName } = require('ts-pnp'); 4 | 5 | exports.resolveModuleName = ( 6 | typescript, 7 | moduleName, 8 | containingFile, 9 | compilerOptions, 10 | resolutionHost 11 | ) => { 12 | return resolveModuleName( 13 | moduleName, 14 | containingFile, 15 | compilerOptions, 16 | resolutionHost, 17 | typescript.resolveModuleName 18 | ); 19 | }; 20 | 21 | exports.resolveTypeReferenceDirective = ( 22 | typescript, 23 | moduleName, 24 | containingFile, 25 | compilerOptions, 26 | resolutionHost 27 | ) => { 28 | return resolveModuleName( 29 | moduleName, 30 | containingFile, 31 | compilerOptions, 32 | resolutionHost, 33 | typescript.resolveTypeReferenceDirective 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /example/src/helpers.js: -------------------------------------------------------------------------------- 1 | function getRandomNumber(min, max) { 2 | return Math.floor(Math.random() * (max - min + 1) + min); 3 | }; 4 | 5 | function generateData(iterations = 100, defaultValues = [], namePrefix = {}, maxJump = 100) { 6 | const arr = []; 7 | for (let i = 0; i <= iterations; i++) { 8 | const values = defaultValues.map((v, idx) => { 9 | if (i === 0 && typeof v.value === 'number') { 10 | return v; 11 | } 12 | return { 13 | ...v, 14 | value: i === 0 ? this.getRandomNumber(1, 1000) : arr[i - 1].values[idx].value + this.getRandomNumber(0, maxJump) 15 | } 16 | }); 17 | arr.push({ 18 | name: `${namePrefix.prefix || ''} ${(namePrefix.initialValue || 0) + i}`, 19 | values 20 | }); 21 | } 22 | return arr; 23 | }; 24 | 25 | export default { 26 | getRandomNumber, 27 | generateData 28 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import external from 'rollup-plugin-peer-deps-external' 4 | import postcss from 'rollup-plugin-postcss' 5 | import resolve from 'rollup-plugin-node-resolve' 6 | import url from 'rollup-plugin-url' 7 | import svgr from '@svgr/rollup' 8 | 9 | import pkg from './package.json' 10 | 11 | export default { 12 | input: 'src/index.js', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | sourcemap: true 18 | }, 19 | { 20 | file: pkg.module, 21 | format: 'es', 22 | sourcemap: true 23 | } 24 | ], 25 | plugins: [ 26 | external(), 27 | postcss({ 28 | modules: false, 29 | extract: true 30 | }), 31 | url(), 32 | svgr(), 33 | babel({ 34 | exclude: 'node_modules/**', 35 | plugins: [ 'external-helpers' ] 36 | }), 37 | resolve(), 38 | commonjs() 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Sternlicht 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap'); 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Open Sans', sans-serif; 7 | } 8 | 9 | *:focus { 10 | outline: 0; 11 | } 12 | 13 | button { 14 | font-size: 16px; 15 | background: #333; 16 | color: #fff; 17 | border: none; 18 | border-radius: 5px; 19 | padding: 10px 20px; 20 | cursor: pointer; 21 | } 22 | 23 | button:hover { 24 | background: #111; 25 | } 26 | 27 | .main-section { 28 | position: relative; 29 | } 30 | 31 | .main-section > a { 32 | position: fixed; 33 | z-index: 1000; 34 | top: 0px; 35 | right: 0px; 36 | border: 0px; 37 | } 38 | 39 | .chart-row { 40 | display: flex; 41 | justify-content: flex-start; 42 | align-items: stretch; 43 | background: #f8f8f8; 44 | } 45 | 46 | .chart-row .description { 47 | min-width: 420px; 48 | max-width: 420px; 49 | box-sizing: border-box; 50 | padding: 20px; 51 | border-right: 1px solid #ccc; 52 | } 53 | 54 | .chart-row .description h3 { 55 | margin: 0 0 20px; 56 | } 57 | 58 | .chart-row .description .buttons { 59 | text-align: center; 60 | margin-bottom: 20px; 61 | } 62 | 63 | .chart-row .description .buttons > * { 64 | margin: 0 8px; 65 | } -------------------------------------------------------------------------------- /example/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFileName = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFileName}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /example/scripts/test.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.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--watchAll') === -1 45 | ) { 46 | // https://github.com/facebook/create-react-app/issues/5210 47 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 48 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 49 | } 50 | 51 | 52 | jest.run(argv); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dynamic-charts", 3 | "version": "1.0.5", 4 | "description": "", 5 | "author": "dsternlicht", 6 | "license": "MIT", 7 | "repository": "dsternlicht/react-dynamic-charts", 8 | "main": "dist/index.js", 9 | "module": "dist/index.es.js", 10 | "jsnext:main": "dist/index.es.js", 11 | "engines": { 12 | "node": ">=8", 13 | "npm": ">=5" 14 | }, 15 | "scripts": { 16 | "test": "cross-env CI=1 react-scripts test --env=jsdom", 17 | "test:watch": "react-scripts test --env=jsdom", 18 | "build": "rollup -c", 19 | "start": "rollup -c -w", 20 | "prepare": "npm run build", 21 | "predeploy": "cd example && npm install && npm run build", 22 | "deploy": "gh-pages -d example/build" 23 | }, 24 | "peerDependencies": { 25 | "prop-types": "^15.5.4", 26 | "react": "^15.0.0 || ^16.0.0", 27 | "react-dom": "^15.0.0 || ^16.0.0" 28 | }, 29 | "devDependencies": { 30 | "@svgr/rollup": "^2.4.1", 31 | "babel-core": "^6.26.3", 32 | "babel-plugin-external-helpers": "^6.22.0", 33 | "babel-preset-env": "^1.7.0", 34 | "babel-preset-react": "^6.24.1", 35 | "babel-preset-stage-0": "^6.24.1", 36 | "cross-env": "^5.1.4", 37 | "eslint": "^5.0.1", 38 | "eslint-config-standard": "^11.0.0", 39 | "eslint-config-standard-react": "^6.0.0", 40 | "eslint-plugin-import": "^2.13.0", 41 | "eslint-plugin-node": "^7.0.1", 42 | "eslint-plugin-promise": "^4.0.0", 43 | "eslint-plugin-react": "^7.10.0", 44 | "eslint-plugin-standard": "^3.1.0", 45 | "gh-pages": "^1.2.0", 46 | "node-sass": "^4.12.0", 47 | "react": "^16.8.6", 48 | "react-dom": "^16.8.6", 49 | "react-scripts": "^3.0.1", 50 | "rollup": "^0.64.1", 51 | "rollup-plugin-babel": "^3.0.7", 52 | "rollup-plugin-commonjs": "^9.1.3", 53 | "rollup-plugin-node-resolve": "^3.3.0", 54 | "rollup-plugin-peer-deps-external": "^2.2.0", 55 | "rollup-plugin-postcss": "^1.6.2", 56 | "rollup-plugin-url": "^1.4.0" 57 | }, 58 | "files": [ 59 | "dist" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /example/config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | 8 | /** 9 | * Get the baseUrl of a compilerOptions object. 10 | * 11 | * @param {Object} options 12 | */ 13 | function getAdditionalModulePaths(options = {}) { 14 | const baseUrl = options.baseUrl; 15 | 16 | // We need to explicitly check for null and undefined (and not a falsy value) because 17 | // TypeScript treats an empty string as `.`. 18 | if (baseUrl == null) { 19 | // If there's no baseUrl set we respect NODE_PATH 20 | // Note that NODE_PATH is deprecated and will be removed 21 | // in the next major release of create-react-app. 22 | 23 | const nodePath = process.env.NODE_PATH || ''; 24 | return nodePath.split(path.delimiter).filter(Boolean); 25 | } 26 | 27 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 28 | 29 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 30 | // the default behavior. 31 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 32 | return null; 33 | } 34 | 35 | // Allow the user set the `baseUrl` to `appSrc`. 36 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 37 | return [paths.appSrc]; 38 | } 39 | 40 | // Otherwise, throw an error. 41 | throw new Error( 42 | chalk.red.bold( 43 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 44 | ' Create React App does not support other values at this time.' 45 | ) 46 | ); 47 | } 48 | 49 | function getModules() { 50 | // Check if TypeScript is setup 51 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 52 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 53 | 54 | if (hasTsConfig && hasJsConfig) { 55 | throw new Error( 56 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 57 | ); 58 | } 59 | 60 | let config; 61 | 62 | // If there's a tsconfig.json we assume it's a 63 | // TypeScript project and set up the config 64 | // based on tsconfig.json 65 | if (hasTsConfig) { 66 | config = require(paths.appTsConfig); 67 | // Otherwise we'll check if there is jsconfig.json 68 | // for non TS projects. 69 | } else if (hasJsConfig) { 70 | config = require(paths.appJsConfig); 71 | } 72 | 73 | config = config || {}; 74 | const options = config.compilerOptions || {}; 75 | 76 | const additionalModulePaths = getAdditionalModulePaths(options); 77 | 78 | return { 79 | additionalModulePaths: additionalModulePaths, 80 | hasTsConfig, 81 | }; 82 | } 83 | 84 | module.exports = getModules(); 85 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | .live-chart { 2 | width: 100%; 3 | padding: 20px; 4 | box-sizing: border-box; 5 | position: relative; 6 | text-align: center; 7 | 8 | .start-button { 9 | margin: 0 auto; 10 | } 11 | 12 | h1 { 13 | font-weight: 700; 14 | font-size: 60px; 15 | text-transform: uppercase; 16 | text-align: center; 17 | padding: 20px 10px; 18 | margin: 0; 19 | } 20 | 21 | .chart { 22 | position: relative; 23 | margin: 20px auto; 24 | } 25 | 26 | .baseline { 27 | position: absolute; 28 | top: 0; 29 | left: 50%; 30 | width: 2px; 31 | height: 100%; 32 | z-index: 2; 33 | background: #555; 34 | 35 | span { 36 | position: absolute; 37 | left: 0; 38 | top: -25px; 39 | transform: translateX(-50%); 40 | font-style: italic; 41 | } 42 | } 43 | 44 | .chart-bars { 45 | position: relative; 46 | width: 100%; 47 | 48 | &.with-baseline { 49 | .bar-wrapper { 50 | left: 0; 51 | padding-left: 50%; 52 | 53 | label { 54 | right: 50%; 55 | width: auto; 56 | } 57 | 58 | &.behind-baseline { 59 | left: auto; 60 | right: 0; 61 | padding-left: 0; 62 | padding-right: 50%; 63 | flex-direction: row-reverse; 64 | 65 | label { 66 | right: auto; 67 | left: 50%; 68 | text-align: left; 69 | flex-direction: row-reverse; 70 | 71 | img { 72 | margin: 0 10px 0 0; 73 | } 74 | } 75 | 76 | .value { 77 | right: 100%; 78 | left: auto; 79 | margin-left: 0; 80 | margin-right: 10px; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | .bar-wrapper { 88 | display: flex; 89 | flex-wrap: wrap; 90 | align-items: center; 91 | position: absolute; 92 | top: 0; 93 | left: 0; 94 | transform: translateY(0); 95 | transition: transform 0.5s linear; 96 | padding-left: 200px; 97 | box-sizing: border-box; 98 | width: 100%; 99 | justify-content: flex-start; 100 | 101 | label { 102 | position: absolute; 103 | height: 100%; 104 | width: 200px; 105 | left: 0; 106 | padding: 0 10px; 107 | box-sizing: border-box; 108 | text-align: right; 109 | top: 50%; 110 | transform: translateY(-50%); 111 | font-size: 16px; 112 | font-weight: 700; 113 | display: flex; 114 | justify-content: flex-end; 115 | align-items: center; 116 | 117 | img { 118 | max-height: 100%; 119 | max-width: 100%; 120 | margin-left: 10px; 121 | } 122 | } 123 | 124 | .value { 125 | font-size: 16px; 126 | font-weight: 700; 127 | margin-left: 10px; 128 | } 129 | 130 | .bar { 131 | width: 0%; 132 | transition: width 0.5s linear; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /example/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right