Skip to content

Commit ce563c0

Browse files
Andrew Lucaericclemmonsheyimalexkellyrmilligan
authored andcommitted
feat(react-scripts): allow PUBLIC_URL in develoment mode (facebook#7259)
Co-authored-by: Eric Clemmons <[email protected]> Co-authored-by: Alex Guerra <[email protected]> Co-authored-by: Kelly <[email protected]> Co-authored-by: Eric Clemmons <[email protected]> Co-authored-by: Alex Guerra <[email protected]> Co-authored-by: Kelly <[email protected]>
1 parent ad4562b commit ce563c0

14 files changed

+290
-75
lines changed

docusaurus/docs/advanced-configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ You can adjust various development and production settings by setting environmen
1717
| WDS_SOCKET_HOST | ✅ Used | 🚫 Ignored | When set, Create React App will run the development server with a custom websocket hostname for hot module reloading. Normally, `webpack-dev-server` defaults to `window.location.hostname` for the SockJS hostname. You may use this variable to start local development on more than one Create React App project at a time. See [webpack-dev-server documentation](https://webpack.js.org/configuration/dev-server/#devserversockhost) for more details. |
1818
| WDS_SOCKET_PATH | ✅ Used | 🚫 Ignored | When set, Create React App will run the development server with a custom websocket path for hot module reloading. Normally, `webpack-dev-server` defaults to `/sockjs-node` for the SockJS pathname. You may use this variable to start local development on more than one Create React App project at a time. See [webpack-dev-server documentation](https://webpack.js.org/configuration/dev-server/#devserversockpath) for more details. |
1919
| WDS_SOCKET_PORT | ✅ Used | 🚫 Ignored | When set, Create React App will run the development server with a custom websocket port for hot module reloading. Normally, `webpack-dev-server` defaults to `window.location.port` for the SockJS port. You may use this variable to start local development on more than one Create React App project at a time. See [webpack-dev-server documentation](https://webpack.js.org/configuration/dev-server/#devserversockport) for more details. |
20-
| PUBLIC_URL | 🚫 Ignored | ✅ Used | Create React App assumes your application is hosted at the serving web server's root or a subpath as specified in [`package.json` (`homepage`)](deployment#building-for-relative-paths). Normally, Create React App ignores the hostname. You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). This may be particularly useful when using a CDN to host your application. |
20+
| PUBLIC_URL | ✅ Used | ✅ Used | Create React App assumes your application is hosted at the serving web server's root or a subpath as specified in [`package.json` (`homepage`)](deployment#building-for-relative-paths). Normally, Create React App ignores the hostname. You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). This may be particularly useful when using a CDN to host your application. |
2121
| CI | ✅ Used | ✅ Used | When set to `true`, Create React App treats warnings as failures in the build. It also makes the test runner non-watching. Most CIs set this flag by default. |
2222
| REACT_EDITOR | ✅ Used | 🚫 Ignored | When an app crashes in development, you will see an error overlay with clickable stack trace. When you click on it, Create React App will try to determine the editor you are using based on currently running processes, and open the relevant source file. You can [send a pull request to detect your editor of choice](https://github.com/facebook/create-react-app/issues/2636). Setting this environment variable overrides the automatic detection. If you do it, make sure your systems [PATH](<https://en.wikipedia.org/wiki/PATH_(variable)>) environment variable points to your editor’s bin folder. You can also set it to `none` to disable it completely. |
2323
| CHOKIDAR_USEPOLLING | ✅ Used | 🚫 Ignored | When set to `true`, the watcher runs in polling mode, as necessary inside a VM. Use this option if `npm start` isn't detecting changes. |

packages/react-dev-utils/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,14 @@ getProcessForPort(3000);
287287

288288
On macOS, tries to find a known running editor process and opens the file in it. It can also be explicitly configured by `REACT_EDITOR`, `VISUAL`, or `EDITOR` environment variables. For example, you can put `REACT_EDITOR=atom` in your `.env.local` file, and Create React App will respect that.
289289

290-
#### `noopServiceWorkerMiddleware(): ExpressMiddleware`
290+
#### `noopServiceWorkerMiddleware(servedPath: string): ExpressMiddleware`
291291

292-
Returns Express middleware that serves a `/service-worker.js` that resets any previously set service worker configuration. Useful for development.
292+
Returns Express middleware that serves a `${servedPath}/service-worker.js` that resets any previously set service worker configuration. Useful for development.
293+
294+
#### `redirectServedPathMiddleware(servedPath: string): ExpressMiddleware`
295+
296+
Returns Express middleware that redirects to `${servedPath}/${req.path}`, if `req.url`
297+
does not start with `servedPath`. Useful for development.
293298

294299
#### `openBrowser(url: string): boolean`
295300

@@ -314,7 +319,7 @@ Pass your parsed `package.json` object as `appPackage`, your the URL where you p
314319

315320
```js
316321
const appPackage = require(paths.appPackageJson);
317-
const publicUrl = paths.publicUrl;
322+
const publicUrl = paths.publicUrlOrPath;
318323
const publicPath = config.output.publicPath;
319324
printHostingInstructions(appPackage, publicUrl, publicPath, 'build', true);
320325
```
@@ -344,7 +349,7 @@ The `args` object accepts a number of properties:
344349

345350
Creates a WebpackDevServer `proxy` configuration object from the `proxy` setting in `package.json`.
346351

347-
##### `prepareUrls(protocol: string, host: string, port: number): Object`
352+
##### `prepareUrls(protocol: string, host: string, port: number, pathname: string = '/'): Object`
348353

349354
Returns an object with local and remote URLs for the development server. Pass this object to `createCompiler()` described above.
350355

packages/react-dev-utils/WebpackDevServerUtils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,20 @@ const forkTsCheckerWebpackPlugin = require('./ForkTsCheckerWebpackPlugin');
2222

2323
const isInteractive = process.stdout.isTTY;
2424

25-
function prepareUrls(protocol, host, port) {
25+
function prepareUrls(protocol, host, port, pathname = '/') {
2626
const formatUrl = hostname =>
2727
url.format({
2828
protocol,
2929
hostname,
3030
port,
31-
pathname: '/',
31+
pathname,
3232
});
3333
const prettyPrintUrl = hostname =>
3434
url.format({
3535
protocol,
3636
hostname,
3737
port: chalk.bold(port),
38-
pathname: '/',
38+
pathname,
3939
});
4040

4141
const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const getPublicUrlOrPath = require('../getPublicUrlOrPath');
11+
12+
const tests = [
13+
// DEVELOPMENT with homepage
14+
{ dev: true, homepage: '/', expect: '/' },
15+
{ dev: true, homepage: '/test', expect: '/test/' },
16+
{ dev: true, homepage: '/test/', expect: '/test/' },
17+
{ dev: true, homepage: './', expect: '/' },
18+
{ dev: true, homepage: '../', expect: '/' },
19+
{ dev: true, homepage: '../test', expect: '/' },
20+
{ dev: true, homepage: './test/path', expect: '/' },
21+
{ dev: true, homepage: 'https://create-react-app.dev/', expect: '/' },
22+
{
23+
dev: true,
24+
homepage: 'https://create-react-app.dev/test',
25+
expect: '/test/',
26+
},
27+
// DEVELOPMENT with publicURL
28+
{ dev: true, publicUrl: '/', expect: '/' },
29+
{ dev: true, publicUrl: '/test', expect: '/test/' },
30+
{ dev: true, publicUrl: '/test/', expect: '/test/' },
31+
{ dev: true, publicUrl: './', expect: '/' },
32+
{ dev: true, publicUrl: '../', expect: '/' },
33+
{ dev: true, publicUrl: '../test', expect: '/' },
34+
{ dev: true, publicUrl: './test/path', expect: '/' },
35+
{ dev: true, publicUrl: 'https://create-react-app.dev/', expect: '/' },
36+
{
37+
dev: true,
38+
publicUrl: 'https://create-react-app.dev/test',
39+
expect: '/test/',
40+
},
41+
// DEVELOPMENT with publicURL and homepage
42+
{ dev: true, publicUrl: '/', homepage: '/test', expect: '/' },
43+
{ dev: true, publicUrl: '/test', homepage: '/path', expect: '/test/' },
44+
{ dev: true, publicUrl: '/test/', homepage: '/test/path', expect: '/test/' },
45+
{ dev: true, publicUrl: './', homepage: '/test', expect: '/' },
46+
{ dev: true, publicUrl: '../', homepage: '/test', expect: '/' },
47+
{ dev: true, publicUrl: '../test', homepage: '/test', expect: '/' },
48+
{ dev: true, publicUrl: './test/path', homepage: '/test', expect: '/' },
49+
{
50+
dev: true,
51+
publicUrl: 'https://create-react-app.dev/',
52+
homepage: '/test',
53+
expect: '/',
54+
},
55+
{
56+
dev: true,
57+
publicUrl: 'https://create-react-app.dev/test',
58+
homepage: '/path',
59+
expect: '/test/',
60+
},
61+
62+
// PRODUCTION with homepage
63+
{ dev: false, homepage: '/', expect: '/' },
64+
{ dev: false, homepage: '/test', expect: '/test/' },
65+
{ dev: false, homepage: '/test/', expect: '/test/' },
66+
{ dev: false, homepage: './', expect: './' },
67+
{ dev: false, homepage: '../', expect: '../' },
68+
{ dev: false, homepage: '../test', expect: '../test/' },
69+
{ dev: false, homepage: './test/path', expect: './test/path/' },
70+
{ dev: false, homepage: 'https://create-react-app.dev/', expect: '/' },
71+
{
72+
dev: false,
73+
homepage: 'https://create-react-app.dev/test',
74+
expect: '/test/',
75+
},
76+
// PRODUCTION with publicUrl
77+
{ dev: false, publicUrl: '/', expect: '/' },
78+
{ dev: false, publicUrl: '/test', expect: '/test/' },
79+
{ dev: false, publicUrl: '/test/', expect: '/test/' },
80+
{ dev: false, publicUrl: './', expect: './' },
81+
{ dev: false, publicUrl: '../', expect: '../' },
82+
{ dev: false, publicUrl: '../test', expect: '../test/' },
83+
{ dev: false, publicUrl: './test/path', expect: './test/path/' },
84+
{
85+
dev: false,
86+
publicUrl: 'https://create-react-app.dev/',
87+
expect: 'https://create-react-app.dev/',
88+
},
89+
{
90+
dev: false,
91+
publicUrl: 'https://create-react-app.dev/test',
92+
expect: 'https://create-react-app.dev/test/',
93+
},
94+
// PRODUCTION with publicUrl and homepage
95+
{ dev: false, publicUrl: '/', homepage: '/test', expect: '/' },
96+
{ dev: false, publicUrl: '/test', homepage: '/path', expect: '/test/' },
97+
{ dev: false, publicUrl: '/test/', homepage: '/test/path', expect: '/test/' },
98+
{ dev: false, publicUrl: './', homepage: '/test', expect: './' },
99+
{ dev: false, publicUrl: '../', homepage: '/test', expect: '../' },
100+
{ dev: false, publicUrl: '../test', homepage: '/test', expect: '../test/' },
101+
{
102+
dev: false,
103+
publicUrl: './test/path',
104+
homepage: '/test',
105+
expect: './test/path/',
106+
},
107+
{
108+
dev: false,
109+
publicUrl: 'https://create-react-app.dev/',
110+
homepage: '/test',
111+
expect: 'https://create-react-app.dev/',
112+
},
113+
{
114+
dev: false,
115+
publicUrl: 'https://create-react-app.dev/test',
116+
homepage: '/path',
117+
expect: 'https://create-react-app.dev/test/',
118+
},
119+
];
120+
121+
describe('getPublicUrlOrPath', () => {
122+
tests.forEach(t =>
123+
it(JSON.stringify(t), () => {
124+
const actual = getPublicUrlOrPath(t.dev, t.homepage, t.publicUrl);
125+
expect(actual).toBe(t.expect);
126+
})
127+
);
128+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
const { URL } = require('url');
11+
12+
module.exports = getPublicUrlOrPath;
13+
14+
/**
15+
* Returns a URL or a path with slash at the end
16+
* In production can be URL, abolute path, relative path
17+
* In development always will be an absolute path
18+
* In development can use `path` module functions for operations
19+
*
20+
* @param {boolean} isEnvDevelopment
21+
* @param {(string|undefined)} homepage a valid url or pathname
22+
* @param {(string|undefined)} envPublicUrl a valid url or pathname
23+
* @returns {string}
24+
*/
25+
function getPublicUrlOrPath(isEnvDevelopment, homepage, envPublicUrl) {
26+
const stubDomain = 'https://create-react-app.dev';
27+
28+
if (envPublicUrl) {
29+
// ensure last slash exists
30+
envPublicUrl = envPublicUrl.endsWith('/')
31+
? envPublicUrl
32+
: envPublicUrl + '/';
33+
34+
// validate if `envPublicUrl` is a URL or path like
35+
// `stubDomain` is ignored if `envPublicUrl` contains a domain
36+
const validPublicUrl = new URL(envPublicUrl, stubDomain);
37+
38+
return isEnvDevelopment
39+
? envPublicUrl.startsWith('.')
40+
? '/'
41+
: validPublicUrl.pathname
42+
: // Some apps do not use client-side routing with pushState.
43+
// For these, "homepage" can be set to "." to enable relative asset paths.
44+
envPublicUrl;
45+
}
46+
47+
if (homepage) {
48+
// strip last slash if exists
49+
homepage = homepage.endsWith('/') ? homepage : homepage + '/';
50+
51+
// validate if `homepage` is a URL or path like and use just pathname
52+
const validHomepagePathname = new URL(homepage, stubDomain).pathname;
53+
return isEnvDevelopment
54+
? homepage.startsWith('.')
55+
? '/'
56+
: validHomepagePathname
57+
: // Some apps do not use client-side routing with pushState.
58+
// For these, "homepage" can be set to "." to enable relative asset paths.
59+
homepage.startsWith('.')
60+
? homepage
61+
: validHomepagePathname;
62+
}
63+
64+
return '/';
65+
}

packages/react-dev-utils/noopServiceWorkerMiddleware.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
'use strict';
99

10-
module.exports = function createNoopServiceWorkerMiddleware() {
10+
const path = require('path');
11+
12+
module.exports = function createNoopServiceWorkerMiddleware(servedPath) {
1113
return function noopServiceWorkerMiddleware(req, res, next) {
12-
if (req.url === '/service-worker.js') {
14+
if (req.url === path.join(servedPath, 'service-worker.js')) {
1315
res.setHeader('Content-Type', 'text/javascript');
1416
res.send(
1517
`// This service worker file is effectively a 'no-op' that will reset any

packages/react-dev-utils/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"getCacheIdentifier.js",
3030
"getCSSModuleLocalIdent.js",
3131
"getProcessForPort.js",
32+
"getPublicUrlOrPath.js",
3233
"globby.js",
3334
"ignoredFiles.js",
3435
"immer.js",
@@ -44,6 +45,7 @@
4445
"openChrome.applescript",
4546
"printBuildError.js",
4647
"printHostingInstructions.js",
48+
"redirectServedPathMiddleware.js",
4749
"typescriptFormatter.js",
4850
"WatchMissingNodeModulesPlugin.js",
4951
"WebpackDevServerUtils.js",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
'use strict';
8+
9+
const path = require('path');
10+
11+
module.exports = function createRedirectServedPathMiddleware(servedPath) {
12+
// remove end slash so user can land on `/test` instead of `/test/`
13+
servedPath = servedPath.slice(0, -1);
14+
return function redirectServedPathMiddleware(req, res, next) {
15+
if (
16+
servedPath === '' ||
17+
req.url === servedPath ||
18+
req.url.startsWith(servedPath)
19+
) {
20+
next();
21+
} else {
22+
const newPath = path.join(servedPath, req.path !== '/' ? req.path : '');
23+
res.redirect(newPath);
24+
}
25+
};
26+
};

packages/react-scripts/config/paths.js

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,24 @@
1010

1111
const path = require('path');
1212
const fs = require('fs');
13-
const url = require('url');
13+
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
1414

1515
// Make sure any symlinks in the project folder are resolved:
1616
// https://github.com/facebook/create-react-app/issues/637
1717
const appDirectory = fs.realpathSync(process.cwd());
1818
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
1919

20-
const envPublicUrl = process.env.PUBLIC_URL;
21-
22-
function ensureSlash(inputPath, needsSlash) {
23-
const hasSlash = inputPath.endsWith('/');
24-
if (hasSlash && !needsSlash) {
25-
return inputPath.substr(0, inputPath.length - 1);
26-
} else if (!hasSlash && needsSlash) {
27-
return `${inputPath}/`;
28-
} else {
29-
return inputPath;
30-
}
31-
}
32-
33-
const getPublicUrl = appPackageJson =>
34-
envPublicUrl || require(appPackageJson).homepage;
35-
3620
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
3721
// "public path" at which the app is served.
3822
// Webpack needs to know it to put the right <script> hrefs into HTML even in
3923
// single-page apps that may serve index.html for nested URLs like /todos/42.
4024
// We can't use a relative path in HTML because we don't want to load something
4125
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
42-
function getServedPath(appPackageJson) {
43-
const publicUrl = getPublicUrl(appPackageJson);
44-
const servedUrl =
45-
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
46-
return ensureSlash(servedUrl, true);
47-
}
26+
const publicUrlOrPath = getPublicUrlOrPath(
27+
process.env.NODE_ENV === 'development',
28+
require(resolveApp('package.json')).homepage,
29+
process.env.PUBLIC_URL
30+
);
4831

4932
const moduleFileExtensions = [
5033
'web.mjs',
@@ -89,8 +72,7 @@ module.exports = {
8972
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
9073
proxySetup: resolveApp('src/setupProxy.js'),
9174
appNodeModules: resolveApp('node_modules'),
92-
publicUrl: getPublicUrl(resolveApp('package.json')),
93-
servedPath: getServedPath(resolveApp('package.json')),
75+
publicUrlOrPath,
9476
};
9577

9678
// @remove-on-eject-begin
@@ -112,8 +94,7 @@ module.exports = {
11294
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
11395
proxySetup: resolveApp('src/setupProxy.js'),
11496
appNodeModules: resolveApp('node_modules'),
115-
publicUrl: getPublicUrl(resolveApp('package.json')),
116-
servedPath: getServedPath(resolveApp('package.json')),
97+
publicUrlOrPath,
11798
// These properties only exist before ejecting:
11899
ownPath: resolveOwn('.'),
119100
ownNodeModules: resolveOwn('node_modules'), // This is empty on npm 3
@@ -148,8 +129,7 @@ if (
148129
testsSetup: resolveModule(resolveOwn, `${templatePath}/src/setupTests`),
149130
proxySetup: resolveOwn(`${templatePath}/src/setupProxy.js`),
150131
appNodeModules: resolveOwn('node_modules'),
151-
publicUrl: getPublicUrl(resolveOwn('package.json')),
152-
servedPath: getServedPath(resolveOwn('package.json')),
132+
publicUrlOrPath,
153133
// These properties only exist before ejecting:
154134
ownPath: resolveOwn('.'),
155135
ownNodeModules: resolveOwn('node_modules'),

0 commit comments

Comments
 (0)