Skip to content

Commit 77ee593

Browse files
gaearonfeiqitian
authored andcommitted
Add support for public/ folder (facebook#703)
1 parent 5738078 commit 77ee593

14 files changed

+202
-122
lines changed

packages/react-scripts/config/env.js

-26
This file was deleted.

packages/react-scripts/config/paths.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ var nodePaths = (process.env.NODE_PATH || '')
3838
// config after eject: we're in ./config/
3939
module.exports = {
4040
appBuild: resolveApp('build'),
41-
appHtml: resolveApp('index.html'),
41+
appPublic: resolveApp('public'),
42+
appHtml: resolveApp('public/index.html'),
4243
appIndexJs: resolveApp('src/index.js'),
4344
appPackageJson: resolveApp('package.json'),
4445
appSrc: resolveApp('src'),
@@ -56,7 +57,8 @@ function resolveOwn(relativePath) {
5657
// config before eject: we're in ./node_modules/react-scripts/config/
5758
module.exports = {
5859
appBuild: resolveApp('build'),
59-
appHtml: resolveApp('index.html'),
60+
appPublic: resolveApp('public'),
61+
appHtml: resolveApp('public/index.html'),
6062
appIndexJs: resolveApp('src/index.js'),
6163
appPackageJson: resolveApp('package.json'),
6264
appSrc: resolveApp('src'),
@@ -71,7 +73,8 @@ module.exports = {
7173
// @remove-on-publish-begin
7274
module.exports = {
7375
appBuild: resolveOwn('../../../build'),
74-
appHtml: resolveOwn('../template/index.html'),
76+
appPublic: resolveOwn('../template/public'),
77+
appHtml: resolveOwn('../template/public/index.html'),
7578
appIndexJs: resolveOwn('../template/src/index.js'),
7679
appPackageJson: resolveOwn('../package.json'),
7780
appSrc: resolveOwn('../template/src'),

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

+21-23
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@ var autoprefixer = require('autoprefixer');
1414
var webpack = require('webpack');
1515
var HtmlWebpackPlugin = require('html-webpack-plugin');
1616
var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
17+
var InterpolateHtmlPlugin = require('../scripts/utils/InterpolateHtmlPlugin');
1718
var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeModulesPlugin');
19+
var getClientEnvironment = require('../scripts/utils/getClientEnvironment');
1820
var paths = require('./paths');
19-
var env = require('./env');
21+
22+
// Webpack uses `publicPath` to determine where the app is being served from.
23+
// In development, we always serve from the root. This makes config easier.
24+
var publicPath = '/';
25+
// `publicUrl` is just like `publicPath`, but we will provide it to our app
26+
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
27+
// Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
28+
var publicUrl = '';
29+
// Get enrivonment variables to inject into our app.
30+
var env = getClientEnvironment(publicUrl);
2031

2132
// This is the development configuration.
2233
// It is focused on developer experience and fast rebuilds.
@@ -63,8 +74,8 @@ module.exports = {
6374
// served by WebpackDevServer in development. This is the JS bundle
6475
// containing code from all our entry points, and the Webpack runtime.
6576
filename: 'static/js/bundle.js',
66-
// In development, we always serve from the root. This makes config easier.
67-
publicPath: '/'
77+
// This is the URL that app is served from. We use "/" in development.
78+
publicPath: publicPath
6879
},
6980
resolve: {
7081
// This allows you to set a fallback for where Webpack should look for modules.
@@ -129,21 +140,11 @@ module.exports = {
129140
// In production, they would get copied to the `build` folder.
130141
{
131142
test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
132-
exclude: /\/favicon.ico$/,
133143
loader: 'file',
134144
query: {
135145
name: 'static/media/[name].[hash:8].[ext]'
136146
}
137147
},
138-
// A special case for favicon.ico to place it into build root directory.
139-
{
140-
test: /\/favicon.ico$/,
141-
include: [paths.appSrc],
142-
loader: 'file',
143-
query: {
144-
name: 'favicon.ico?[hash:8]'
145-
}
146-
},
147148
// "url" loader works just like "file" loader but it also embeds
148149
// assets smaller than specified size as data URLs to avoid requests.
149150
{
@@ -153,15 +154,6 @@ module.exports = {
153154
limit: 10000,
154155
name: 'static/media/[name].[hash:8].[ext]'
155156
}
156-
},
157-
// "html" loader is used to process template page (index.html) to resolve
158-
// resources linked with <link href="./relative/path"> HTML tags.
159-
{
160-
test: /\.html$/,
161-
loader: 'html',
162-
query: {
163-
attrs: ['link:href'],
164-
}
165157
}
166158
]
167159
},
@@ -186,13 +178,19 @@ module.exports = {
186178
];
187179
},
188180
plugins: [
181+
// Makes the public URL available as %PUBLIC_URL% in index.html, e.g.:
182+
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
183+
// In development, this will be an empty string.
184+
new InterpolateHtmlPlugin({
185+
PUBLIC_URL: publicUrl
186+
}),
189187
// Generates an `index.html` file with the <script> injected.
190188
new HtmlWebpackPlugin({
191189
inject: true,
192190
template: paths.appHtml,
193191
}),
194192
// Makes some environment variables available to the JS code, for example:
195-
// if (process.env.NODE_ENV === 'development') { ... }. See `env.js`.
193+
// if (process.env.NODE_ENV === 'development') { ... }.
196194
new webpack.DefinePlugin(env),
197195
// This is necessary to emit hot updates (currently CSS only):
198196
new webpack.HotModuleReplacementPlugin(),

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

+34-29
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
1616
var ExtractTextPlugin = require('extract-text-webpack-plugin');
1717
var url = require('url');
1818
var paths = require('./paths');
19-
var env = require('./env');
19+
var InterpolateHtmlPlugin = require('../scripts/utils/InterpolateHtmlPlugin');
20+
var getClientEnvironment = require('../scripts/utils/getClientEnvironment');
2021

21-
// Assert this just to be safe.
22-
// Development builds of React are slow and not intended for production.
23-
if (env['process.env.NODE_ENV'] !== '"production"') {
24-
throw new Error('Production builds must have NODE_ENV=production.');
22+
function ensureSlash(path, needsSlash) {
23+
var hasSlash = path.endsWith('/');
24+
if (hasSlash && !needsSlash) {
25+
return path.substr(path, path.length - 1);
26+
} else if (!hasSlash && needsSlash) {
27+
return path + '/';
28+
} else {
29+
return path;
30+
}
2531
}
2632

2733
// We use "homepage" field to infer "public path" at which the app is served.
@@ -30,10 +36,21 @@ if (env['process.env.NODE_ENV'] !== '"production"') {
3036
// We can't use a relative path in HTML because we don't want to load something
3137
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
3238
var homepagePath = require(paths.appPackageJson).homepage;
33-
var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/';
34-
if (!publicPath.endsWith('/')) {
35-
// If we don't do this, file assets will get incorrect paths.
36-
publicPath += '/';
39+
var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';
40+
// Webpack uses `publicPath` to determine where the app is being served from.
41+
// It requires a trailing slash, or the file assets will get an incorrect path.
42+
var publicPath = ensureSlash(homepagePathname, true);
43+
// `publicUrl` is just like `publicPath`, but we will provide it to our app
44+
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
45+
// Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
46+
var publicUrl = ensureSlash(homepagePathname, false);
47+
// Get enrivonment variables to inject into our app.
48+
var env = getClientEnvironment(publicUrl);
49+
50+
// Assert this just to be safe.
51+
// Development builds of React are slow and not intended for production.
52+
if (env['process.env.NODE_ENV'] !== '"production"') {
53+
throw new Error('Production builds must have NODE_ENV=production.');
3754
}
3855

3956
// This is the production configuration.
@@ -139,21 +156,11 @@ module.exports = {
139156
// When you `import` an asset, you get its filename.
140157
{
141158
test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
142-
exclude: /\/favicon.ico$/,
143159
loader: 'file',
144160
query: {
145161
name: 'static/media/[name].[hash:8].[ext]'
146162
}
147163
},
148-
// A special case for favicon.ico to place it into build root directory.
149-
{
150-
test: /\/favicon.ico$/,
151-
include: [paths.appSrc],
152-
loader: 'file',
153-
query: {
154-
name: 'favicon.ico?[hash:8]'
155-
}
156-
},
157164
// "url" loader works just like "file" loader but it also embeds
158165
// assets smaller than specified size as data URLs to avoid requests.
159166
{
@@ -163,15 +170,6 @@ module.exports = {
163170
limit: 10000,
164171
name: 'static/media/[name].[hash:8].[ext]'
165172
}
166-
},
167-
// "html" loader is used to process template page (index.html) to resolve
168-
// resources linked with <link href="./relative/path"> HTML tags.
169-
{
170-
test: /\.html$/,
171-
loader: 'html',
172-
query: {
173-
attrs: ['link:href'],
174-
}
175173
}
176174
]
177175
},
@@ -198,6 +196,13 @@ module.exports = {
198196
];
199197
},
200198
plugins: [
199+
// Makes the public URL available as %PUBLIC_URL% in index.html, e.g.:
200+
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
201+
// In production, it will be an empty string unless you specify "homepage"
202+
// in `package.json`, in which case it will be the pathname of that URL.
203+
new InterpolateHtmlPlugin({
204+
PUBLIC_URL: publicUrl
205+
}),
201206
// Generates an `index.html` file with the <script> injected.
202207
new HtmlWebpackPlugin({
203208
inject: true,
@@ -216,7 +221,7 @@ module.exports = {
216221
}
217222
}),
218223
// Makes some environment variables available to the JS code, for example:
219-
// if (process.env.NODE_ENV === 'production') { ... }. See `env.js`.
224+
// if (process.env.NODE_ENV === 'production') { ... }.
220225
// It is absolutely essential that NODE_ENV was set to production here.
221226
// Otherwise React will be compiled in the very slow development mode.
222227
new webpack.DefinePlugin(env),

packages/react-scripts/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"find-cache-dir": "0.1.1",
4747
"fs-extra": "0.30.0",
4848
"gzip-size": "3.0.0",
49-
"html-loader": "0.4.3",
5049
"html-webpack-plugin": "2.22.0",
5150
"http-proxy-middleware": "0.17.1",
5251
"jest": "15.1.1",

packages/react-scripts/scripts/build.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
process.env.NODE_ENV = 'production';
1414

1515
var chalk = require('chalk');
16-
var fs = require('fs');
16+
var fs = require('fs-extra');
1717
var path = require('path');
1818
var filesize = require('filesize');
1919
var gzipSize = require('gzip-size').sync;
@@ -70,6 +70,9 @@ recursive(paths.appBuild, (err, fileNames) => {
7070

7171
// Start the webpack build
7272
build(previousSizeMap);
73+
74+
// Merge with the public folder
75+
copyPublicFolder();
7376
});
7477

7578
// Print a detailed summary of build files.
@@ -175,3 +178,10 @@ function build(previousSizeMap) {
175178
}
176179
});
177180
}
181+
182+
function copyPublicFolder() {
183+
fs.copySync(paths.appPublic, paths.appBuild, {
184+
dereference: true,
185+
filter: file => file !== paths.appHtml
186+
});
187+
}

packages/react-scripts/scripts/eject.js

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ prompt(
4646
path.join('scripts', 'start.js'),
4747
path.join('scripts', 'utils', 'checkRequiredFiles.js'),
4848
path.join('scripts', 'utils', 'chrome.applescript'),
49+
path.join('scripts', 'utils', 'getClientEnvironment.js'),
50+
path.join('scripts', 'utils', 'InterpolateHtmlPlugin.js'),
4951
path.join('scripts', 'utils', 'prompt.js'),
5052
path.join('scripts', 'utils', 'WatchMissingNodeModulesPlugin.js')
5153
];

packages/react-scripts/scripts/start.js

+15-17
Original file line numberDiff line numberDiff line change
@@ -255,23 +255,21 @@ function runDevServer(port, protocol) {
255255
// Silence WebpackDevServer's own logs since they're generally not useful.
256256
// It will still show compile warnings and errors with this setting.
257257
clientLogLevel: 'none',
258-
// By default WebpackDevServer also serves files from the current directory.
259-
// This might be useful in legacy apps. However we already encourage people
260-
// to use Webpack for importing assets in the code, so we don't need to
261-
// additionally serve files by their filenames. Otherwise, even if it
262-
// works in development, those files will be missing in production, unless
263-
// we explicitly copy them. But even if we copy all the files into
264-
// the build output (which doesn't seem to be wise because it may contain
265-
// private information such as files with API keys, for example), we would
266-
// still have a problem. Since the filenames would be the same every time,
267-
// browsers would cache their content, and updating file content would not
268-
// work correctly. This is easily solved by importing assets through Webpack
269-
// because if it can then append content hashes to filenames in production,
270-
// just like it does for JS and CSS. And because we configured "html" loader
271-
// to be used for HTML files, even <link href="./src/something.png"> would
272-
// get resolved correctly by Webpack and handled both in development and
273-
// in production without actually serving it by that path.
274-
contentBase: [],
258+
// By default WebpackDevServer serves physical files from current directory
259+
// in addition to all the virtual build products that it serves from memory.
260+
// This is confusing because those files won’t automatically be available in
261+
// production build folder unless we copy them. However, copying the whole
262+
// project directory is dangerous because we may expose sensitive files.
263+
// Instead, we establish a convention that only files in `public` directory
264+
// get served. Our build script will copy `public` into the `build` folder.
265+
// In `index.html`, you can get URL of `public` folder with %PUBLIC_PATH%:
266+
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
267+
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
268+
// Note that we only recommend to use `public` folder as an escape hatch
269+
// for files like `favicon.ico`, `manifest.json`, and libraries that are
270+
// for some reason broken when imported through Webpack. If you just want to
271+
// use an image, put it in `src` and `import` it from JavaScript instead.
272+
contentBase: paths.appPublic,
275273
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
276274
// for the WebpackDevServer client so it can learn when the files were
277275
// updated. The WebpackDevServer client is included as an entry point

packages/react-scripts/scripts/test.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
process.env.NODE_ENV = 'test';
11+
process.env.PUBLIC_URL = '';
1112

1213
const createJestConfig = require('./utils/createJestConfig');
1314
const jest = require('jest');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// @remove-on-eject-begin
2+
/**
3+
* Copyright (c) 2015-present, Facebook, Inc.
4+
* All rights reserved.
5+
*
6+
* This source code is licensed under the BSD-style license found in the
7+
* LICENSE file in the root directory of this source tree. An additional grant
8+
* of patent rights can be found in the PATENTS file in the same directory.
9+
*/
10+
// @remove-on-eject-end
11+
12+
// This Webpack plugin lets us interpolate custom variables into `index.html`.
13+
// Usage: `new InterpolateHtmlPlugin({ 'MY_VARIABLE': 42 })`
14+
// Then, you can use %MY_VARIABLE% in your `index.html`.
15+
16+
// It works in tandem with HtmlWebpackPlugin.
17+
// Learn more about creating plugins like this:
18+
// https://github.com/ampedandwired/html-webpack-plugin#events
19+
20+
'use strict';
21+
22+
class InterpolateHtmlPlugin {
23+
constructor(replacements) {
24+
this.replacements = replacements;
25+
}
26+
27+
apply(compiler) {
28+
compiler.plugin('compilation', compilation => {
29+
compilation.plugin('html-webpack-plugin-before-html-processing',
30+
(data, callback) => {
31+
// Run HTML through a series of user-specified string replacements.
32+
Object.keys(this.replacements).forEach(key => {
33+
const value = this.replacements[key];
34+
data.html = data.html.replace('%' + key + '%', value);
35+
});
36+
callback(null, data);
37+
}
38+
);
39+
});
40+
}
41+
}
42+
43+
module.exports = InterpolateHtmlPlugin;

0 commit comments

Comments
 (0)