Skip to content
This repository has been archived by the owner on Aug 5, 2020. It is now read-only.

Commit

Permalink
Merge pull request #196 from TrueCar/toddw/multiple-entry-points
Browse files Browse the repository at this point in the history
Add support for multiple entrypoints
  • Loading branch information
christinebrass authored Jun 9, 2016
2 parents 7499e02 + 070759f commit 5138b96
Show file tree
Hide file tree
Showing 19 changed files with 645 additions and 82 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"license": "MIT",
"scripts": {
"lint": "./node_modules/.bin/eslint src test templates",
"test": "mocha --compilers js:babel-core/register --require babel-polyfill --recursive",
"test-coverage": "./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover --include-all-sources --report html --dir coverage/html ./node_modules/.bin/_mocha -- --recursive",
"test": "NODE_ENV=test mocha --compilers js:babel-core/register --require babel-polyfill --recursive",
"test-coverage": "NODE_ENV=test ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover --include-all-sources --report html --dir coverage/html ./node_modules/.bin/_mocha -- --recursive",
"prepublish": "./docker/generate-dockerfile.js",
"postpublish": "./docker/create-base-image.js"
},
Expand Down Expand Up @@ -105,6 +105,7 @@
"peerDependencies": {},
"devDependencies": {
"babel-eslint": "6.0.4",
"enzyme": "2.3.0",
"eslint": "2.10.2",
"eslint-plugin-react": "5.1.1",
"temp": "0.8.3"
Expand Down
1 change: 0 additions & 1 deletion src/autoUpgrade/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ module.exports = async function () {
// update
[
".entry.js",
".store.js",
".Dockerfile" //-> last updated in 0.2.0
].forEach((fileName) => {
const currentFile = fs.readFileSync(getCurrentFilePath(fileName));
Expand Down
22 changes: 7 additions & 15 deletions src/commands/start-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const WebpackIsomorphicToolsPlugin = require("webpack-isomorphic-tools/plugin");
const webpackSharedConfig = require("../config/webpack-shared-config");
const detectEnvironmentVariables = require("../lib/detectEnvironmentVariables");
const getWebpackAdditions = require("../lib/getWebpackAdditions").default;
const buildWebpackEntries = require("../lib/buildWebpackEntries").default;
const { additionalLoaders, additionalPreLoaders, vendor, plugins } = getWebpackAdditions();
const logger = require("../lib/logger");
const logsColorScheme = require("../lib/logsColorScheme");
Expand All @@ -19,7 +20,7 @@ if (assetPath.substr(-1) !== "/") {

const PORT = 8888;
const OUTPUT_PATH = path.join(process.cwd(), "build");
const OUTPUT_FILE = "main.bundle.js";
const OUTPUT_FILE = "app.bundle.js";
const PUBLIC_PATH = assetPath;

const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require("../config/webpack-isomorphic-tools-config"))
Expand All @@ -28,10 +29,6 @@ const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(require(".
process.env.NODE_PATH = path.join(__dirname, "../..");
const isProduction = process.env.NODE_ENV === "production";

const entry = [
path.join(__dirname, "../entrypoints/client.js")
];

let environmentPlugins = [];

if (isProduction) {
Expand All @@ -51,12 +48,6 @@ else {
]);
}

// Include hot middleware in development mode only
if (!isProduction) {
entry.unshift("webpack-hot-middleware/client");
}


// The config/application.js file is consumed on both the server side and the
// client side. However, we want developers to have access to environment
// constiables in there so they can override defaults with an environment
Expand All @@ -74,7 +65,7 @@ const compiler = webpack({
context: process.cwd(),
devtool: isProduction ? null : "cheap-module-eval-source-map",
entry: {
main: entry,
...buildWebpackEntries(isProduction),
vendor: vendor
},
module: {
Expand All @@ -89,20 +80,20 @@ const compiler = webpack({
},
output: {
path: OUTPUT_PATH,
filename: OUTPUT_FILE,
chunkFilename: `[name]-${OUTPUT_FILE}`,
filename: `[name]-${OUTPUT_FILE}`,
chunkFilename: `[name]-chunk-${OUTPUT_FILE}`,
publicPath: PUBLIC_PATH
},
plugins: [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.DefinePlugin({
"__PATH_TO_ENTRY__": JSON.stringify(path.join(process.cwd(), "src/config/.entry")),
"process.env": exposedEnvironmentVariables
}),
new webpack.IgnorePlugin(/\.server(\.js)?$/),
new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.bundle.js"),
new webpack.optimize.CommonsChunkPlugin("commons", "commons.bundle.js"),
new webpack.optimize.AggressiveMergingPlugin()
].concat(environmentPlugins, webpackSharedConfig.plugins, plugins),
resolve: {
Expand Down Expand Up @@ -155,3 +146,4 @@ module.exports = function (buildOnly) {
});
}
};

3 changes: 0 additions & 3 deletions src/entrypoints/client.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
/*global __PATH_TO_ENTRY__*/
/*global __webpack_public_path__*/
/*exported __webpack_public_path__*/
require("match-media/matchMedia.js");
require("match-media/matchMedia.addListener.js");
require("babel-polyfill");
__webpack_public_path__ = window.__GS_PUBLIC_PATH__;
const Entry = require(__PATH_TO_ENTRY__).default;
Entry.start();

110 changes: 110 additions & 0 deletions src/lib/buildWebpackEntries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import fs from "fs-extra";
import path from "path";
import getWebpackAdditions from "./getWebpackAdditions";

function getBasePath () {
return path.join(process.cwd(), "src", "config", ".entries");
}

/**
* This method is a information preparer/gatherer. This information is used by
* `buildWebpackEntries` and the server request handler. It sets up the default
* entry point that will be used by most apps, as well as combining any
* additional entry points specified in the webpack additions.
*/
export function getWebpackEntries () {
const output = {};
const cwd = process.cwd();
const basePath = getBasePath();

// setup default
const entryPoints = {
"/": {
name: "main",
routes: path.join(cwd, "src", "config", "routes"),
reducers: path.join(cwd, "src", "reducers")
},
...getWebpackAdditions().entryPoints
};

Object.keys(entryPoints).forEach((key) => {
const entry = entryPoints[key];
const fileName = entry.name.replace(/\W/, "-");
output[key] = {
...entry,
fileName: fileName,
filePath: `${path.join(basePath, fileName)}.js`,
routes: entry.routes || path.join(cwd, "src", "config", "routes", fileName),
reducers: entry.reducers || path.join(cwd, "src", "reducers", fileName),
index: entry.index || path.join(cwd, "Index")
};
});

return output;
}


/**
* This method will wipe out the `config/.entries` hidden folder and rebuild it
* it based on the defined entry points. It creates entry point files for each
* of the entry points and returns the hash that webpack uses when bundling
* code.
*
* Each entry point will be an array including the shared client entry point
* file which includes global dependencies like the babel polyfill. It then
* includes the generated entry point. Finally, if we are in development mode
* it will start the array off with the webpack hot middleware client.
*/
export default function buildWebpackEntries (isProduction) {
const output = {};
const basePath = getBasePath();

// Clean slate
fs.removeSync(basePath);
fs.ensureDirSync(basePath);

const entries = getWebpackEntries();
for (const key in entries) {
const entry = entries[key];
const { filePath, fileName } = entry;
fs.outputFileSync(filePath, getEntryPointContent(entry));
output[fileName] = [path.join(__dirname, "..", "entrypoints", "client.js"), filePath];

// Include hot middleware in development mode only
if (!isProduction) {
output[fileName].unshift("webpack-hot-middleware/client");
}
}

return output;
}

function getEntryPointContent (entry) {
const cwd = process.cwd();
const { routes, index, reducers } = entry;
const reduxMiddlewarePath = path.join(cwd, "src", "config", "redux-middleware");
const config = path.join(cwd, "src", "config", "application");
const mainEntry = path.join(cwd, "src", "config", ".entry");
const output = `import getRoutes from "${routes}";
// Make sure that webpack considers new dependencies introduced in the Index
// file
import "${index}";
import config from "${config}";
import Entry from "${mainEntry}";
import { createStore } from "gluestick-shared";
import middleware from "${reduxMiddlewarePath}";
export function getStore (httpClient) {
const store = createStore(httpClient, () => require("${reducers}"), middleware, (cb) => module.hot && module.hot.accept("${reducers}", cb), !!module.hot);
return store;
}
if (typeof window === "object") {
Entry.start(getRoutes, getStore);
}
`;

return output;
}

8 changes: 5 additions & 3 deletions src/lib/getWebpackAdditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function prepareUserAdditionsForWebpack (additions) {
* avoid having the client application having to include path in its dependencies by creating
* the paths here rather than in webpack-additions.js
*/
function makeUserAdditionalAliases (aliasesDictionary) {
function makeUserAdditionalAliases (aliasesDictionary={}) {
return Object.entries(aliasesDictionary).reduce((map, [aliasName, aliasPath]) => {
map[aliasName] = path.join(process.cwd(), ...aliasPath);
return map;
Expand All @@ -55,6 +55,7 @@ export default function (isomorphic=false) {
additionalPreLoaders: [],
additionalAliases: {},
vendor: [],
entryPoints: {},
plugins: []
};

Expand All @@ -64,13 +65,14 @@ export default function (isomorphic=false) {
try {
const webpackAdditionsPath = path.join(process.cwd(), "src", "config", "webpack-additions.js");
fs.statSync(webpackAdditionsPath);
const { additionalLoaders, additionalPreLoaders, additionalAliases, vendor, plugins } = require(webpackAdditionsPath);
const { additionalLoaders, additionalPreLoaders, additionalAliases, vendor, plugins, entryPoints } = require(webpackAdditionsPath);
userAdditions = {
additionalLoaders: isomorphic ? additionalLoaders : prepareUserAdditionsForWebpack(additionalLoaders),
additionalPreLoaders: isomorphic ? additionalPreLoaders : prepareUserAdditionsForWebpack(additionalPreLoaders),
additionalAliases: makeUserAdditionalAliases(additionalAliases) || {},
vendor: vendor || [],
plugins: plugins || []
plugins: plugins || [],
entryPoints: entryPoints || {}
};
}
catch (e) {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/isChildPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { relative } from "path";

export default function isChildPath (parent, child) {
if (parent === "/") {
return true;
}

return relative(parent, child).substr(0, 1) !== ".";
}

5 changes: 4 additions & 1 deletion src/lib/server/Body.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default class Body extends Component {
config: PropTypes.object.isRequired,
html: PropTypes.string.isRequired,
isEmail: PropTypes.bool.isRequired,
entryPoint: PropTypes.string.isRequired,
initialState: PropTypes.any.isRequired
};

Expand All @@ -28,15 +29,17 @@ export default class Body extends Component {
_renderWithScriptTags () {
const {
initialState,
entryPoint,
config
} = this.props;

return (
<div>
{ this._renderMainContent() }
<script type="text/javascript" dangerouslySetInnerHTML={{__html: `window.__INITIAL_STATE__=${serialize(initialState)};`}}></script>
<script type="text/javascript" src={`${config.assetPath}/commons.bundle.js`}></script>
<script type="text/javascript" src={`${config.assetPath}/vendor.bundle.js`}></script>
<script type="text/javascript" src={`${config.assetPath}/main.bundle.js`}></script>
<script type="text/javascript" src={`${config.assetPath}/${entryPoint}-app.bundle.js`}></script>
</div>
);
}
Expand Down
2 changes: 0 additions & 2 deletions src/lib/server/addProxies.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { parse } from "url";
import httpProxyMiddleware from "http-proxy-middleware";
import logger from "../../lib/logger";

/**
* The array of proxy objects follow the following pattern
Expand Down
49 changes: 49 additions & 0 deletions src/lib/server/getRenderRequirementsFromEntrypoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { parse as parseURL } from "url";
import { getWebpackEntries } from "../buildWebpackEntries";
import isChildPath from "../isChildPath";
import { getHttpClient } from "gluestick-shared";

/**
* This method takes the server request object, determines which entry point
* the server should use for rendering and then prepares the necessary
* variables that the server needs to render. These variables include Index,
* store, getRoutes and fileName.
*/
export default function getRenderRequirementsFromEntrypoints (req, config={}, customRequire=require) {
const httpClient = getHttpClient(config.httpClient, req);
const entryPoints = getWebpackEntries();

/**
* Sort through all of the entry points based on the number of `/` characters
* found in the url. It will test the most deeply nested entry points first
* while finally falling back to the default index parameter.
*/
const sortedEntries = Object.keys(entryPoints).sort((a, b) => {
const bSplitLength = b.split("/").length;
const aSplitLength = a.split("/").length;
if (bSplitLength === aSplitLength) {
return b.length - a.length;
}

return bSplitLength - aSplitLength;
});

const { path: urlPath } = parseURL(req.url);

/**
* Loop through the sorted entry points and return the variables that the
* server needs to render based on the best matching entry point.
*/
for (const path of sortedEntries) {
if (isChildPath(path, urlPath)) {
const { routes, index, fileName, filePath } = entryPoints[path];
return {
Index: customRequire(index + ".js").default,
store: customRequire(filePath).getStore(httpClient),
getRoutes: customRequire(routes).default,
fileName
};
}
}
}

Loading

0 comments on commit 5138b96

Please sign in to comment.