Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add createModernBuild option #188

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions flow-typed/defs.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ declare type BatfishConfiguration = {
rehypePlugins?: Array<Function>
},
includePromisePolyfill: boolean,
createModernBuild: boolean,
inlineJs?: Array<InlineJsEntry>,
production?: boolean,
developmentDevtool: string | false,
Expand Down Expand Up @@ -103,3 +104,9 @@ declare module 'batfish-internal/context' {
declare module 'batfish-internal/application-wrapper' {
declare export default React.Element<*>
}

declare type AssetsJson = {
runtime: { js: string },
app: { js: string },
vendor: { js: string }
};
261 changes: 260 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@
"sitemap-static": "^0.4.2",
"slugg": "^1.2.1",
"source-map-support": "^0.4.18",
"strip-color": "^0.1.0",
"tempy": "^0.2.0",
"time-stamp": "^2.0.0",
"uglify-js": "^3.1.0",
Expand All @@ -174,6 +173,7 @@
"@mapbox/vnu-validate-html": "^0.1.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^8.0.0",
"babel-minify-webpack-plugin": "^0.2.0",
"cpy-cli": "^1.0.1",
"enzyme": "^2.9.1",
"enzyme-to-json": "^1.6.0",
Expand Down
43 changes: 17 additions & 26 deletions src/node/build-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,16 @@ const joinUrlParts = require('./join-url-parts');
const constants = require('./constants');
const errorTypes = require('./error-types');
const wrapError = require('./wrap-error');
const UglifyJs = require('uglify-js');
const getWebpackAssetAbsolutePath = require('./get-webpack-asset-absolute-path');

// We need to define this type because Flow can't understand the non-literal
// require that pulls in static-render-pages.js below.
declare type StaticRenderPagesFunction = (
BatfishConfiguration,
{
vendor: { js: string },
app: { js: string }
},
string,
cssUrl?: string
options: {
assets: AssetsJson,
modernAssets?: AssetsJson,
cssUrl?: string
}
) => Promise<void>;

function buildHtml(
Expand All @@ -40,20 +37,15 @@ function buildHtml(
path.join(assetsDirectory, 'assets.json'),
'utf8'
);
const assets: {
manifest: { js: string },
app: { js: string },
vendor: { js: string }
} = Object.freeze(JSON.parse(rawAssets));
const manifestJs = fs.readFileSync(
getWebpackAssetAbsolutePath(batfishConfig, assets.manifest.js),
'utf8'
);

const uglified = UglifyJs.minify(manifestJs);
if (uglified.error) throw uglified.error;
const uglifiedManifestJs: string = uglified.code;

const assets: AssetsJson = Object.freeze(JSON.parse(rawAssets));
let modernAssets: AssetsJson | void;
if (batfishConfig.createModernBuild) {
const modernRawAssets = fs.readFileSync(
path.join(assetsDirectory, 'm-assets.json'),
'utf8'
);
modernAssets = Object.freeze(JSON.parse(modernRawAssets));
}
let cssUrl;
if (!_.isEmpty(batfishConfig.stylesheets) && cssFilename) {
cssUrl = joinUrlParts(
Expand All @@ -68,12 +60,11 @@ function buildHtml(
assetsDirectory,
'static-render-pages.js'
)).default;
return staticRenderPages(
batfishConfig,
return staticRenderPages(batfishConfig, {
assets,
uglifiedManifestJs,
modernAssets,
cssUrl
).catch(error => {
}).catch(error => {
throw wrapError(error, errorTypes.WebpackNodeExecutionError);
});
} catch (requireError) {
Expand Down
23 changes: 21 additions & 2 deletions src/node/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ function build(rawConfig?: Object, projectDirectory?: string): EventEmitter {

const buildClient = (): Promise<void> => {
return createWebpackConfigClient(tailoredBatfishConfig)
.then(webpackCompilePromise)
.then(stats => {
// If we're doing a modern build, let the modern build write the stats.
if (!batfishConfig.createModernBuild) {
return writeWebpackStats(outputDirectory, stats);
}
});
};

const buildModernClient = (): Promise<void> => {
// babelPresetEnvOptions will be overridden if user has also set
// includeModernBuild. User must pick one.
return createWebpackConfigClient(tailoredBatfishConfig, { modern: true })
.then(webpackCompilePromise)
.then(stats => {
return writeWebpackStats(outputDirectory, stats);
Expand Down Expand Up @@ -81,8 +94,14 @@ function build(rawConfig?: Object, projectDirectory?: string): EventEmitter {
})
.then(() => {
emitNotification('Starting the Webpack bundling.');
emitNotification('Creating the client bundle.');
return buildClient();
const promisesToKeep = [buildClient()];
if (batfishConfig.createModernBuild) {
emitNotification('Creating both modern and legacy client bundles.');
promisesToKeep.push(buildModernClient());
} else {
emitNotification('Creating the client bundles.');
}
return Promise.all(promisesToKeep);
})
.then(() => {
emitNotification('Creating the static-page-rendering Node bundle.');
Expand Down
12 changes: 1 addition & 11 deletions src/node/create-webpack-config-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,10 @@ const joinUrlParts = require('./join-url-parts');
const constants = require('./constants');
const getPostcssPlugins = require('./get-postcss-plugins');

// Cache it to ensure we don't do unnecessary work within one process.
let cachedConfig;

// Create the base Webpack configuration, shared by both client and static builds.
function createWebpackConfigBase(
batfishConfig: BatfishConfiguration
): Promise<webpack$Configuration> {
if (cachedConfig) return Promise.resolve(cachedConfig);
const isExample = path
.dirname(batfishConfig.pagesDirectory)
.startsWith(path.join(__dirname, '../../examples'));
Expand Down Expand Up @@ -74,7 +70,7 @@ function createWebpackConfigBase(
presets: babelPresets,
plugins: babelPlugins,
babelrc: false,
compact: true
compact: false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this back to avoid those annoying Webpack messages about deoptimization.

}
};

Expand Down Expand Up @@ -206,14 +202,8 @@ function createWebpackConfigBase(
bail: batfishConfig.production
};

cachedConfig = config;
return config;
});
}

// For tests.
createWebpackConfigBase._clearCache = () => {
cachedConfig = null;
};

module.exports = createWebpackConfigBase;
67 changes: 52 additions & 15 deletions src/node/create-webpack-config-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ const AssetsPlugin = require('assets-webpack-plugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
const resolveFrom = require('resolve-from');
const createWebpackConfigBase = require('./create-webpack-config-base');
const MinifyPlugin = require('babel-minify-webpack-plugin');

// These browsers support classes and arrow functions. The runtime test
// in static-render-pages checks for these two features when determining
// which build to load.
const modernBabelPresetEnvOptions = {
useBuiltIns: true,
targets: {
browsers: [
'Edge >= 13', // Should be 12?
'Firefox >= 45',
'Chrome >= 49',
'Safari >= 10',
'iOS >= 10.2',
'Opera >= 36'
]
}
};

// We need the directory for the module instead of the filename to its main
// file.
Expand All @@ -22,7 +40,13 @@ function resolveModuleDirectoryFrom(src: string, name: string): string {
// Create a Webpack configuration for all the assets that will be loaded by the client.
function createWebpackConfigClient(
batfishConfig: BatfishConfiguration,
options?: { devServer?: boolean }
options?: {
// Indicates that we're building this for the development server, not for the
// static build.
devServer?: boolean,
// Indicates that we're making a modern build.
modern?: boolean
} = {}
): Promise<webpack$Configuration> {
// Resolve these peerDependencies from the pagesDirectory so we are sure
// to get the same version that the pages are getting. Alias them below.
Expand All @@ -38,16 +62,23 @@ function createWebpackConfigClient(
batfishConfig.pagesDirectory,
'react-helmet'
);
return createWebpackConfigBase(batfishConfig).then(baseConfig => {
const filenamePrefix = options.modern ? `m-` : '';

const tailoredBatfishConfig = !options.modern
? batfishConfig
: Object.assign({}, batfishConfig, {
babelPresetEnvOptions: modernBabelPresetEnvOptions
});

return createWebpackConfigBase(tailoredBatfishConfig).then(baseConfig => {
let vendorModules = [
reactPath,
reactDomPath,
reactHelmetPath,
require.resolve('@mapbox/scroll-restorer'),
require.resolve('@mapbox/link-hijacker'),
require.resolve('strip-color')
require.resolve('@mapbox/link-hijacker')
];
if (batfishConfig.includePromisePolyfill) {
if (batfishConfig.includePromisePolyfill && !options.modern) {
vendorModules.unshift(require.resolve('es6-promise/auto'));
}
if (batfishConfig.vendorModules !== undefined) {
Expand All @@ -60,7 +91,7 @@ function createWebpackConfigClient(
// include hashes so cannot be known without this dictionary.
new AssetsPlugin({
path: path.resolve(batfishConfig.outputDirectory),
filename: 'assets.json',
filename: `${filenamePrefix}assets.json`,
processOutput: x => JSON.stringify(x, null, 2)
}),
// Extract universal vendor files (defined above) from everything else.
Expand All @@ -75,17 +106,23 @@ function createWebpackConfigClient(
}),
// Trying to follow advice for long-term caching described here:
// https://jeremygayed.com/dynamic-vendor-bundling-in-webpack-528993e48aab#.hjgai17ap
new webpack.optimize.CommonsChunkPlugin('manifest'),
// Define an environment variable for special cases
new webpack.optimize.CommonsChunkPlugin('runtime'),
// Define an environment variable for special cases.
new webpack.DefinePlugin({
'process.env.DEV_SERVER': (options && options.devServer) || false
'process.env.DEV_SERVER': options.devServer || false
})
].concat(batfishConfig.webpackPlugins || []);
const uglifyPlugin = new ParallelUglifyPlugin({
sourceMap: !!batfishConfig.productionDevtool
});

if (batfishConfig.production) {
clientPlugins.push(uglifyPlugin);
let minifyPlugin;
if (options.modern) {
minifyPlugin = new MinifyPlugin();
} else {
minifyPlugin = new ParallelUglifyPlugin({
sourceMap: !!batfishConfig.productionDevtool
});
}
clientPlugins.push(minifyPlugin);
}

const appEntry = [];
Expand All @@ -104,10 +141,10 @@ function createWebpackConfigClient(
output: {
filename: !batfishConfig.production
? '[name].js'
: '[name]-[chunkhash].js',
: `${filenamePrefix}[name]-[chunkhash].js`,
chunkFilename: !batfishConfig.production
? '[name].chunk.js'
: '[name]-[chunkhash].chunk.js'
: `${filenamePrefix}[name]-[chunkhash].chunk.js`
},
resolve: {
alias: Object.assign({}, _.get(baseConfig, 'resolve.alias'), {
Expand Down
11 changes: 10 additions & 1 deletion src/node/create-webpack-config-static.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@ const createWebpackConfigBase = require('./create-webpack-config-base');
function createWebpackConfigStatic(
batfishConfig: BatfishConfiguration
): Promise<webpack$Configuration> {
return createWebpackConfigBase(batfishConfig).then(baseConfig => {
// For the static build always set up Babel to compile only what's necessary
// for the current version of Node.
return createWebpackConfigBase(
Object.assign({}, batfishConfig, {
babelPresetEnvOptions: {
useBuiltIns: true,
targets: { node: 'current' }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we only need to compile what's necessary for the current version of Node. Can you explain why this is the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we're using Node 6, we get a lot of ES2015 without compilation. So we should in theory have faster compile-time and maybe even faster runtime if, for example, we don't compile classes and arrow functions, which Node 6 understands.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the set of ES features that get compiled, and end up in the static bundle, are driven by the version of Node that compiles the assets? If I'm understanding this correctly, I feel like this could result in some confusion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we clarified in chat: The misunderstanding here was about where and when the static bundle runs. The output of this Webpack task is just static-render-pages.js, and that only runs in Node.

}
})
).then(baseConfig => {
const staticConfig: webpack$Configuration = {
entry: {
static: path.join(__dirname, '../webpack/static-render-pages.js')
Expand Down
17 changes: 17 additions & 0 deletions src/node/validate-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ const configSchema = {
validator: _.isBoolean,
description: 'boolean'
},
createModernBuild: {
validator: _.isBoolean,
description: 'boolean'
},
inlineJs: {
validator: isArrayOf(x => isAbsolutePath(x.filename) || _.isBoolean(x)),
description:
Expand Down Expand Up @@ -205,6 +209,7 @@ function validateConfig(
],
jsxtremeMarkdownOptions: {},
includePromisePolyfill: true,
createModernBuild: true,
pageSpecificCss: true,
developmentDevtool: 'source-map',
productionDevtool: false,
Expand All @@ -231,6 +236,18 @@ function validateConfig(
config.postcssPlugins = defaultPostcssPlugins;
}

if (config.createModernBuild) {
if (config.babelPresetEnvOptions) {
configErrors.push(
`The option ${chalk.yellow(
'createModernBuild'
)} will override your ${chalk.yellow(
'babelPresetEnvOptions'
)} setting. You should pick one option or the other.`
);
}
}

const validatePropertyType = (
propertyName: string,
predicate: any => boolean,
Expand Down
3 changes: 2 additions & 1 deletion src/node/watch-webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ function watchWebpack(
let hasCompiled = false;

createWebpackConfigClient(batfishConfig, {
devServer: true
devServer: true,
modern: batfishConfig.createModernBuild
})
.then(clientConfig => {
// Create an HTML file to load the assets in the browser.
Expand Down
Loading