Skip to content

Commit bb14761

Browse files
Enoah NetzachTimer
Enoah Netzach
authored andcommitted
Add PUBLIC_URL env variable for advanced use (facebook#937)
* Add support for `PUBLIC_URL` env variable * Remove unnecessary duplications * Simplify served path choice logic * Honor PUBLIC_URL in development * Add e2e tests Enables serving static assets from specified host.
1 parent 0ac0d11 commit bb14761

File tree

13 files changed

+122
-53
lines changed

13 files changed

+122
-53
lines changed

packages/react-scripts/config/paths.js

+32-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
var path = require('path');
1313
var fs = require('fs');
14+
var url = require('url');
1415

1516
// Make sure any symlinks in the project folder are resolved:
1617
// https://github.com/facebookincubator/create-react-app/issues/637
@@ -40,6 +41,28 @@ var nodePaths = (process.env.NODE_PATH || '')
4041
.filter(folder => !path.isAbsolute(folder))
4142
.map(resolveApp);
4243

44+
var envPublicUrl = process.env.PUBLIC_URL;
45+
46+
function getPublicUrl(appPackageJson) {
47+
return envPublicUrl || require(appPackageJson).homepage;
48+
}
49+
50+
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
51+
// "public path" at which the app is served.
52+
// Webpack needs to know it to put the right <script> hrefs into HTML even in
53+
// single-page apps that may serve index.html for nested URLs like /todos/42.
54+
// We can't use a relative path in HTML because we don't want to load something
55+
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
56+
function getServedPath(appPackageJson) {
57+
var publicUrl = getPublicUrl(appPackageJson);
58+
if (!publicUrl) {
59+
return '/';
60+
} else if (envPublicUrl) {
61+
return publicUrl;
62+
}
63+
return url.parse(publicUrl).pathname;
64+
}
65+
4366
// config after eject: we're in ./config/
4467
module.exports = {
4568
appBuild: resolveApp('build'),
@@ -52,7 +75,9 @@ module.exports = {
5275
testsSetup: resolveApp('src/setupTests.js'),
5376
appNodeModules: resolveApp('node_modules'),
5477
ownNodeModules: resolveApp('node_modules'),
55-
nodePaths: nodePaths
78+
nodePaths: nodePaths,
79+
publicUrl: getPublicUrl(resolveApp('package.json')),
80+
servedPath: getServedPath(resolveApp('package.json'))
5681
};
5782

5883
// @remove-on-eject-begin
@@ -73,7 +98,9 @@ module.exports = {
7398
appNodeModules: resolveApp('node_modules'),
7499
// this is empty with npm3 but node resolution searches higher anyway:
75100
ownNodeModules: resolveOwn('../node_modules'),
76-
nodePaths: nodePaths
101+
nodePaths: nodePaths,
102+
publicUrl: getPublicUrl(resolveApp('package.json')),
103+
servedPath: getServedPath(resolveApp('package.json'))
77104
};
78105

79106
// config before publish: we're in ./packages/react-scripts/config/
@@ -89,7 +116,9 @@ if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1)
89116
testsSetup: resolveOwn('../template/src/setupTests.js'),
90117
appNodeModules: resolveOwn('../node_modules'),
91118
ownNodeModules: resolveOwn('../node_modules'),
92-
nodePaths: nodePaths
119+
nodePaths: nodePaths,
120+
publicUrl: getPublicUrl(resolveOwn('../package.json')),
121+
servedPath: getServedPath(resolveOwn('../package.json'))
93122
};
94123
}
95124
// @remove-on-eject-end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = function ensureSlash(path, needsSlash) {
2+
var hasSlash = path.endsWith('/');
3+
if (hasSlash && !needsSlash) {
4+
return path.substr(path, path.length - 1);
5+
} else if (!hasSlash && needsSlash) {
6+
return path + '/';
7+
} else {
8+
return path;
9+
}
10+
}

packages/react-scripts/config/webpack.config.dev.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
1515
var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
1616
var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
1717
var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
18+
var ensureSlash = require('./utils/ensureSlash');
1819
var getClientEnvironment = require('./env');
1920
var paths = require('./paths');
2021

@@ -29,7 +30,7 @@ var publicPath = '/';
2930
// `publicUrl` is just like `publicPath`, but we will provide it to our app
3031
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
3132
// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
32-
var publicUrl = '';
33+
var publicUrl = ensureSlash(paths.servedPath, false);
3334
// Get environment variables to inject into our app.
3435
var env = getClientEnvironment(publicUrl);
3536

packages/react-scripts/config/webpack.config.prod.js

+4-21
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin');
1616
var ManifestPlugin = require('webpack-manifest-plugin');
1717
var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
1818
var url = require('url');
19+
var ensureSlash = require('./utils/ensureSlash');
1920
var paths = require('./paths');
2021
var getClientEnvironment = require('./env');
2122

@@ -24,31 +25,13 @@ var getClientEnvironment = require('./env');
2425
var path = require('path');
2526
// @remove-on-eject-end
2627

27-
function ensureSlash(path, needsSlash) {
28-
var hasSlash = path.endsWith('/');
29-
if (hasSlash && !needsSlash) {
30-
return path.substr(path, path.length - 1);
31-
} else if (!hasSlash && needsSlash) {
32-
return path + '/';
33-
} else {
34-
return path;
35-
}
36-
}
37-
38-
// We use "homepage" field to infer "public path" at which the app is served.
39-
// Webpack needs to know it to put the right <script> hrefs into HTML even in
40-
// single-page apps that may serve index.html for nested URLs like /todos/42.
41-
// We can't use a relative path in HTML because we don't want to load something
42-
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
43-
var homepagePath = require(paths.appPackageJson).homepage;
44-
var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';
4528
// Webpack uses `publicPath` to determine where the app is being served from.
4629
// It requires a trailing slash, or the file assets will get an incorrect path.
47-
var publicPath = ensureSlash(homepagePathname, true);
30+
var publicPath = ensureSlash(paths.servedPath, true);
4831
// `publicUrl` is just like `publicPath`, but we will provide it to our app
4932
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
50-
// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
51-
var publicUrl = ensureSlash(homepagePathname, false);
33+
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
34+
var publicUrl = ensureSlash(paths.servedPath, false);
5235
// Get environment variables to inject into our app.
5336
var env = getClientEnvironment(publicUrl);
5437

packages/react-scripts/fixtures/kitchensink/integration/env.test.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,30 @@ import initDOM from './initDOM'
33

44
describe('Integration', () => {
55
describe('Environment variables', () => {
6+
it('file env variables', async () => {
7+
const doc = await initDOM('file-env-variables')
8+
9+
expect(doc.getElementById('feature-file-env-variables').textContent).to.equal('fromtheenvfile.')
10+
})
11+
612
it('NODE_PATH', async () => {
713
const doc = await initDOM('node-path')
814

915
expect(doc.getElementById('feature-node-path').childElementCount).to.equal(4)
1016
})
1117

12-
it('shell env variables', async () => {
13-
const doc = await initDOM('shell-env-variables')
18+
it('PUBLIC_URL', async () => {
19+
const doc = await initDOM('public-url')
1420

15-
expect(doc.getElementById('feature-shell-env-variables').textContent).to.equal('fromtheshell.')
21+
expect(doc.getElementById('feature-public-url').textContent).to.equal('http://www.example.org/spa.')
22+
expect(doc.querySelector('head link[rel="shortcut icon"]').getAttribute('href'))
23+
.to.equal('http://www.example.org/spa/favicon.ico')
1624
})
1725

18-
it('file env variables', async () => {
19-
const doc = await initDOM('file-env-variables')
26+
it('shell env variables', async () => {
27+
const doc = await initDOM('shell-env-variables')
2028

21-
expect(doc.getElementById('feature-file-env-variables').textContent).to.equal('fromtheenvfile.')
29+
expect(doc.getElementById('feature-shell-env-variables').textContent).to.equal('fromtheshell.')
2230
})
2331
})
2432
})

packages/react-scripts/fixtures/kitchensink/integration/initDOM.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ if (process.env.E2E_FILE) {
1515
const markup = fs.readFileSync(file, 'utf8')
1616
getMarkup = () => markup
1717

18+
const pathPrefix = process.env.PUBLIC_URL.replace(/^https?:\/\/[^\/]+\/?/, '')
19+
1820
resourceLoader = (resource, callback) => callback(
1921
null,
20-
fs.readFileSync(path.join(path.dirname(file), resource.url.pathname), 'utf8')
22+
fs.readFileSync(path.join(path.dirname(file), resource.url.pathname.replace(pathPrefix, '')), 'utf8')
2123
)
2224
} else if (process.env.E2E_URL) {
2325
getMarkup = () => new Promise(resolve => {
@@ -37,7 +39,7 @@ if (process.env.E2E_FILE) {
3739

3840
export default feature => new Promise(async resolve => {
3941
const markup = await getMarkup()
40-
const host = process.env.E2E_URL || 'http://localhost:3000'
42+
const host = process.env.E2E_URL || 'http://www.example.org/spa:3000'
4143
const doc = jsdom.jsdom(markup, {
4244
features: {
4345
FetchExternalResources: ['script', 'css'],

packages/react-scripts/fixtures/kitchensink/src/App.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class BuiltEmitter extends Component {
66
}
77

88
componentDidMount() {
9-
const { feature } = this.props
9+
const { feature } = this.props;
1010

1111
// Class components must call this.props.onReady when they're ready for the test.
1212
// We will assume functional components are ready immediately after mounting.
@@ -44,7 +44,8 @@ class App extends Component {
4444
}
4545

4646
componentDidMount() {
47-
switch (location.hash.slice(1)) {
47+
const feature = location.hash.slice(1);
48+
switch (feature) {
4849
case 'array-destructuring':
4950
require.ensure([], () => this.setFeature(require('./features/syntax/ArrayDestructuring').default));
5051
break;
@@ -99,6 +100,9 @@ class App extends Component {
99100
case 'promises':
100101
require.ensure([], () => this.setFeature(require('./features/syntax/Promises').default));
101102
break;
103+
case 'public-url':
104+
require.ensure([], () => this.setFeature(require('./features/env/PublicUrl').default));
105+
break;
102106
case 'rest-and-default':
103107
require.ensure([], () => this.setFeature(require('./features/syntax/RestAndDefault').default));
104108
break;
@@ -117,7 +121,7 @@ class App extends Component {
117121
case 'unknown-ext-inclusion':
118122
require.ensure([], () => this.setFeature(require('./features/webpack/UnknownExtInclusion').default));
119123
break;
120-
default: throw new Error('Unknown feature!');
124+
default: throw new Error(`Missing feature "${feature}"`);
121125
}
122126
}
123127

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import React from 'react'
2+
3+
export default () => (
4+
<span id="feature-public-url">{process.env.PUBLIC_URL}.</span>
5+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import PublicUrl from './PublicUrl';
4+
5+
describe('PUBLIC_URL', () => {
6+
it('renders without crashing', () => {
7+
const div = document.createElement('div');
8+
ReactDOM.render(<PublicUrl />, div);
9+
});
10+
});

packages/react-scripts/scripts/build.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require('dotenv').config({silent: true});
2121
var chalk = require('chalk');
2222
var fs = require('fs-extra');
2323
var path = require('path');
24+
var url = require('url');
2425
var filesize = require('filesize');
2526
var gzipSize = require('gzip-size').sync;
2627
var webpack = require('webpack');
@@ -158,15 +159,16 @@ function build(previousSizeMap) {
158159

159160
var openCommand = process.platform === 'win32' ? 'start' : 'open';
160161
var appPackage = require(paths.appPackageJson);
161-
var homepagePath = appPackage.homepage;
162+
var publicUrl = paths.publicUrl;
162163
var publicPath = config.output.publicPath;
163-
if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) {
164+
var publicPathname = url.parse(publicPath).pathname;
165+
if (publicUrl && publicUrl.indexOf('.github.io/') !== -1) {
164166
// "homepage": "http://user.github.io/project"
165-
console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.');
167+
console.log('The project was built assuming it is hosted at ' + chalk.green(publicPathname) + '.');
166168
console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
167169
console.log();
168170
console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
169-
console.log('To publish it at ' + chalk.green(homepagePath) + ', run:');
171+
console.log('To publish it at ' + chalk.green(publicUrl) + ', run:');
170172
// If script deploy has been added to package.json, skip the instructions
171173
if (typeof appPackage.scripts.deploy === 'undefined') {
172174
console.log();
@@ -198,14 +200,14 @@ function build(previousSizeMap) {
198200
console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
199201
console.log();
200202
} else {
201-
// no homepage or "homepage": "http://mywebsite.com"
202-
console.log('The project was built assuming it is hosted at the server root.');
203-
if (homepagePath) {
203+
if (publicUrl) {
204204
// "homepage": "http://mywebsite.com"
205+
console.log('The project was built assuming it is hosted at ' + chalk.green(publicUrl) + '.');
205206
console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.');
206207
console.log();
207208
} else {
208209
// no homepage
210+
console.log('The project was built assuming it is hosted at the server root.');
209211
console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.');
210212
console.log('For example, add this to build it for GitHub Pages:')
211213
console.log();

packages/react-scripts/scripts/eject.js

+10-7
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ prompt(
4343
}
4444
}
4545

46-
var folders = [
47-
'config',
48-
path.join('config', 'jest'),
49-
'scripts'
50-
];
51-
5246
var files = [
5347
path.join('config', 'env.js'),
5448
path.join('config', 'paths.js'),
@@ -57,11 +51,20 @@ prompt(
5751
path.join('config', 'webpack.config.prod.js'),
5852
path.join('config', 'jest', 'cssTransform.js'),
5953
path.join('config', 'jest', 'fileTransform.js'),
54+
path.join('config', 'utils', 'ensureSlash.js'),
6055
path.join('scripts', 'build.js'),
6156
path.join('scripts', 'start.js'),
62-
path.join('scripts', 'test.js')
57+
path.join('scripts', 'test.js'),
6358
];
6459

60+
var folders = files.reduce(function(prevFolders, file) {
61+
var dirname = path.dirname(file);
62+
if (prevFolders.indexOf(dirname) === -1) {
63+
return prevFolders.concat(dirname);
64+
}
65+
return prevFolders;
66+
}, []);
67+
6568
// Ensure that the app folder is clean and we won't override any files
6669
folders.forEach(verifyAbsent);
6770
files.forEach(verifyAbsent);

packages/react-scripts/scripts/start.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ function runDevServer(host, port, protocol) {
241241
// project directory is dangerous because we may expose sensitive files.
242242
// Instead, we establish a convention that only files in `public` directory
243243
// get served. Our build script will copy `public` into the `build` folder.
244-
// In `index.html`, you can get URL of `public` folder with %PUBLIC_PATH%:
244+
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
245245
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
246246
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
247247
// Note that we only recommend to use `public` folder as an escape hatch

0 commit comments

Comments
 (0)