Skip to content

Commit

Permalink
Add support for public/ folder (#703)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon authored Sep 22, 2016
1 parent 5b85a36 commit bc6392a
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 122 deletions.
26 changes: 0 additions & 26 deletions packages/react-scripts/config/env.js

This file was deleted.

9 changes: 6 additions & 3 deletions packages/react-scripts/config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ var nodePaths = (process.env.NODE_PATH || '')
// config after eject: we're in ./config/
module.exports = {
appBuild: resolveApp('build'),
appHtml: resolveApp('index.html'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveApp('src/index.js'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
Expand All @@ -56,7 +57,8 @@ function resolveOwn(relativePath) {
// config before eject: we're in ./node_modules/react-scripts/config/
module.exports = {
appBuild: resolveApp('build'),
appHtml: resolveApp('index.html'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveApp('src/index.js'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
Expand All @@ -71,7 +73,8 @@ module.exports = {
// @remove-on-publish-begin
module.exports = {
appBuild: resolveOwn('../../../build'),
appHtml: resolveOwn('../template/index.html'),
appPublic: resolveOwn('../template/public'),
appHtml: resolveOwn('../template/public/index.html'),
appIndexJs: resolveOwn('../template/src/index.js'),
appPackageJson: resolveOwn('../package.json'),
appSrc: resolveOwn('../template/src'),
Expand Down
44 changes: 21 additions & 23 deletions packages/react-scripts/config/webpack.config.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,20 @@ var autoprefixer = require('autoprefixer');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
var InterpolateHtmlPlugin = require('../scripts/utils/InterpolateHtmlPlugin');
var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeModulesPlugin');
var getClientEnvironment = require('../scripts/utils/getClientEnvironment');
var paths = require('./paths');
var env = require('./env');

// Webpack uses `publicPath` to determine where the app is being served from.
// In development, we always serve from the root. This makes config easier.
var publicPath = '/';
// `publicUrl` is just like `publicPath`, but we will provide it to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
var publicUrl = '';
// Get enrivonment variables to inject into our app.
var env = getClientEnvironment(publicUrl);

// This is the development configuration.
// It is focused on developer experience and fast rebuilds.
Expand Down Expand Up @@ -63,8 +74,8 @@ module.exports = {
// served by WebpackDevServer in development. This is the JS bundle
// containing code from all our entry points, and the Webpack runtime.
filename: 'static/js/bundle.js',
// In development, we always serve from the root. This makes config easier.
publicPath: '/'
// This is the URL that app is served from. We use "/" in development.
publicPath: publicPath
},
resolve: {
// This allows you to set a fallback for where Webpack should look for modules.
Expand Down Expand Up @@ -129,21 +140,11 @@ module.exports = {
// In production, they would get copied to the `build` folder.
{
test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
exclude: /\/favicon.ico$/,
loader: 'file',
query: {
name: 'static/media/[name].[hash:8].[ext]'
}
},
// A special case for favicon.ico to place it into build root directory.
{
test: /\/favicon.ico$/,
include: [paths.appSrc],
loader: 'file',
query: {
name: 'favicon.ico?[hash:8]'
}
},
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{
Expand All @@ -153,15 +154,6 @@ module.exports = {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]'
}
},
// "html" loader is used to process template page (index.html) to resolve
// resources linked with <link href="./relative/path"> HTML tags.
{
test: /\.html$/,
loader: 'html',
query: {
attrs: ['link:href'],
}
}
]
},
Expand All @@ -186,13 +178,19 @@ module.exports = {
];
},
plugins: [
// Makes the public URL available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In development, this will be an empty string.
new InterpolateHtmlPlugin({
PUBLIC_URL: publicUrl
}),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({
inject: true,
template: paths.appHtml,
}),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'development') { ... }. See `env.js`.
// if (process.env.NODE_ENV === 'development') { ... }.
new webpack.DefinePlugin(env),
// This is necessary to emit hot updates (currently CSS only):
new webpack.HotModuleReplacementPlugin(),
Expand Down
63 changes: 34 additions & 29 deletions packages/react-scripts/config/webpack.config.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var url = require('url');
var paths = require('./paths');
var env = require('./env');
var InterpolateHtmlPlugin = require('../scripts/utils/InterpolateHtmlPlugin');
var getClientEnvironment = require('../scripts/utils/getClientEnvironment');

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

// We use "homepage" field to infer "public path" at which the app is served.
Expand All @@ -30,10 +36,21 @@ if (env['process.env.NODE_ENV'] !== '"production"') {
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
var homepagePath = require(paths.appPackageJson).homepage;
var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/';
if (!publicPath.endsWith('/')) {
// If we don't do this, file assets will get incorrect paths.
publicPath += '/';
var homepagePathname = homepagePath ? url.parse(homepagePath).pathname : '/';
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
var publicPath = ensureSlash(homepagePathname, true);
// `publicUrl` is just like `publicPath`, but we will provide it to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
var publicUrl = ensureSlash(homepagePathname, false);
// Get enrivonment variables to inject into our app.
var env = getClientEnvironment(publicUrl);

// Assert this just to be safe.
// Development builds of React are slow and not intended for production.
if (env['process.env.NODE_ENV'] !== '"production"') {
throw new Error('Production builds must have NODE_ENV=production.');
}

// This is the production configuration.
Expand Down Expand Up @@ -139,21 +156,11 @@ module.exports = {
// When you `import` an asset, you get its filename.
{
test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
exclude: /\/favicon.ico$/,
loader: 'file',
query: {
name: 'static/media/[name].[hash:8].[ext]'
}
},
// A special case for favicon.ico to place it into build root directory.
{
test: /\/favicon.ico$/,
include: [paths.appSrc],
loader: 'file',
query: {
name: 'favicon.ico?[hash:8]'
}
},
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{
Expand All @@ -163,15 +170,6 @@ module.exports = {
limit: 10000,
name: 'static/media/[name].[hash:8].[ext]'
}
},
// "html" loader is used to process template page (index.html) to resolve
// resources linked with <link href="./relative/path"> HTML tags.
{
test: /\.html$/,
loader: 'html',
query: {
attrs: ['link:href'],
}
}
]
},
Expand All @@ -198,6 +196,13 @@ module.exports = {
];
},
plugins: [
// Makes the public URL available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In production, it will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin({
PUBLIC_URL: publicUrl
}),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({
inject: true,
Expand All @@ -216,7 +221,7 @@ module.exports = {
}
}),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `env.js`.
// if (process.env.NODE_ENV === 'production') { ... }.
// It is absolutely essential that NODE_ENV was set to production here.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env),
Expand Down
1 change: 0 additions & 1 deletion packages/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"find-cache-dir": "0.1.1",
"fs-extra": "0.30.0",
"gzip-size": "3.0.0",
"html-loader": "0.4.3",
"html-webpack-plugin": "2.22.0",
"http-proxy-middleware": "0.17.1",
"jest": "15.1.1",
Expand Down
12 changes: 11 additions & 1 deletion packages/react-scripts/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
process.env.NODE_ENV = 'production';

var chalk = require('chalk');
var fs = require('fs');
var fs = require('fs-extra');
var path = require('path');
var filesize = require('filesize');
var gzipSize = require('gzip-size').sync;
Expand Down Expand Up @@ -70,6 +70,9 @@ recursive(paths.appBuild, (err, fileNames) => {

// Start the webpack build
build(previousSizeMap);

// Merge with the public folder
copyPublicFolder();
});

// Print a detailed summary of build files.
Expand Down Expand Up @@ -175,3 +178,10 @@ function build(previousSizeMap) {
}
});
}

function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml
});
}
2 changes: 2 additions & 0 deletions packages/react-scripts/scripts/eject.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ prompt(
path.join('scripts', 'start.js'),
path.join('scripts', 'utils', 'checkRequiredFiles.js'),
path.join('scripts', 'utils', 'chrome.applescript'),
path.join('scripts', 'utils', 'getClientEnvironment.js'),
path.join('scripts', 'utils', 'InterpolateHtmlPlugin.js'),
path.join('scripts', 'utils', 'prompt.js'),
path.join('scripts', 'utils', 'WatchMissingNodeModulesPlugin.js')
];
Expand Down
32 changes: 15 additions & 17 deletions packages/react-scripts/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,23 +255,21 @@ function runDevServer(port, protocol) {
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer also serves files from the current directory.
// This might be useful in legacy apps. However we already encourage people
// to use Webpack for importing assets in the code, so we don't need to
// additionally serve files by their filenames. Otherwise, even if it
// works in development, those files will be missing in production, unless
// we explicitly copy them. But even if we copy all the files into
// the build output (which doesn't seem to be wise because it may contain
// private information such as files with API keys, for example), we would
// still have a problem. Since the filenames would be the same every time,
// browsers would cache their content, and updating file content would not
// work correctly. This is easily solved by importing assets through Webpack
// because if it can then append content hashes to filenames in production,
// just like it does for JS and CSS. And because we configured "html" loader
// to be used for HTML files, even <link href="./src/something.png"> would
// get resolved correctly by Webpack and handled both in development and
// in production without actually serving it by that path.
contentBase: [],
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files won’t automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_PATH%:
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through Webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
Expand Down
1 change: 1 addition & 0 deletions packages/react-scripts/scripts/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';

const createJestConfig = require('./utils/createJestConfig');
const jest = require('jest');
Expand Down
43 changes: 43 additions & 0 deletions packages/react-scripts/scripts/utils/InterpolateHtmlPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end

// This Webpack plugin lets us interpolate custom variables into `index.html`.
// Usage: `new InterpolateHtmlPlugin({ 'MY_VARIABLE': 42 })`
// Then, you can use %MY_VARIABLE% in your `index.html`.

// It works in tandem with HtmlWebpackPlugin.
// Learn more about creating plugins like this:
// https://github.com/ampedandwired/html-webpack-plugin#events

'use strict';

class InterpolateHtmlPlugin {
constructor(replacements) {
this.replacements = replacements;
}

apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-before-html-processing',
(data, callback) => {
// Run HTML through a series of user-specified string replacements.
Object.keys(this.replacements).forEach(key => {
const value = this.replacements[key];
data.html = data.html.replace('%' + key + '%', value);
});
callback(null, data);
}
);
});
}
}

module.exports = InterpolateHtmlPlugin;
Loading

0 comments on commit bc6392a

Please sign in to comment.