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

Rhys/decouple asset hashing #920

Merged
merged 8 commits into from
Apr 24, 2017
Merged
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
8 changes: 3 additions & 5 deletions _entry.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// for the purposes of exposing in a shared n-ui bundle
// This will mean webpack can find them in this bundle under n-ui/componentName
module.exports = function (withPreact, exclusions) {
module.exports = function (exclusions) {
const entry = {
// n-ui components
'n-ui': 'window.ftNextUi',
Expand All @@ -23,10 +23,8 @@ module.exports = function (withPreact, exclusions) {
'superstore-sync': 'window.ftNextUi._superstoreSync',
}

if (withPreact) {
entry.react = 'window.ftNextUi._React';
entry['react-dom'] = 'window.ftNextUi._ReactDom';
}
entry.react = 'window.ftNextUi._React';
entry['react-dom'] = 'window.ftNextUi._ReactDom';

if (exclusions) {
exclusions.forEach(exc => delete entry[exc]);
Expand Down
20 changes: 20 additions & 0 deletions build/app/asset-hashes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');

module.exports = () => {
const hashes = fs.readdirSync(path.join(process.cwd(), 'public'))
.filter(asset => !/(\.map|about\.json|asset-hashes\.json)$/.test(asset))
.map(name => {
const file = fs.readFileSync(path.join(process.cwd(), 'public', name), 'utf8');
const hash = crypto.createHash('sha1').update(file).digest('hex');
const hashedName = `${hash.substring(0, 8)}/${name}`;
return { name, hashedName };
})
.reduce((previous, current) => {
previous[current.name] = current.hashedName;
previous[current.name + '.map'] = current.hashedName + '.map';
return previous;
}, {});
fs.writeFileSync('./public/asset-hashes.json', JSON.stringify(hashes, null, 2), { encoding: 'UTF8' });
}
2 changes: 2 additions & 0 deletions build/app/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const path = require('path');
const shellpromise = require('shellpromise');
const shellpipe = require('./shellpipe');
const downloadAssets = require('./download-assets');
const assetHashes = require('./asset-hashes')

const exit = err => {
logger.error(err);
Expand Down Expand Up @@ -50,6 +51,7 @@ program
devAdvice();

shellpipe(`webpack ${options.production ? '--bail' : '--dev'} --config ${webpackConfPath}`)
.then(assetHashes)
.then(aboutJson)
.then(downloadAssets)
.then(() => {
Expand Down
124 changes: 75 additions & 49 deletions build/app/webpack.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
const path = require('path');
const nWebpack = require('@financial-times/n-webpack');
const fs = require('fs');
const join = require('path').join;
const Wrap = require('../lib/addons/wrap');
const headCss = require('../lib/head-css')

const gitignore = fs.readFileSync(join(process.cwd(), '.gitignore'), 'utf8')
.split('\n');


function noGitignoreWildcard () {
gitignore.forEach(pattern => {
if (/^\/?public\/(.*\/\*|\*|$)/.test(pattern)) {
if (pattern !== '/public/n-ui/') {
throw new Error('Wildcard pattern or entire directories (i.e. /public/) for built public assets not allowed in your .gitignore. Please specify a path for each file');
}
}
});
}

function clone (obj) {
return JSON.parse(JSON.stringify(obj));
Expand All @@ -22,23 +39,39 @@ function filterEntryKeys (obj, rx, negativeMatch) {
}, {})
}

function constructVariants (nWebpackOptions) {

// we no longer build a main.js for the app when generating the standard asset variants
const variants = [
Object.assign({}, nWebpackOptions, {
entry: filterEntryKeys(nWebpackOptions.entry, /main\.js$/, true)
})
]

variants.push(Object.assign(clone(nWebpackOptions), {
language: 'js',
externals: {'n-ui': true},
entry: modifyEntryKeys(nWebpackOptions.entry, /main\.js$/, name => name.replace(/\.js$/,'-without-n-ui.js'))
}))

if (process.env.NEXT_APP_SHELL === 'local') {
const nWebpackWarning = `
const baseConfig = require(path.join(process.cwd(), 'n-ui-build.config.js'));

noGitignoreWildcard();

// we no longer build a main.js for the app when generating the standard asset variants
const variants = [
// all entry points excluding main.js generated as normal
headCss(nWebpack(Object.assign({}, baseConfig, {
entry: filterEntryKeys(baseConfig.entry, /main\.js$/, true)
})))
]

// new entry point for main.js declaring external n-ui
const mainJs = nWebpack(Object.assign(clone(baseConfig), {
language: 'js',
entry: modifyEntryKeys(baseConfig.entry, /main\.js$/, name => name.replace(/\.js$/,'-without-n-ui.js'))
}))

const nUiEntry = path.join(process.cwd(), 'bower_components/n-ui/_entry');
const nUiEntryPoints = require(nUiEntry)(baseConfig.nUiExcludes)
mainJs.externals = Object.assign({}, mainJs.externals, nUiEntryPoints);
mainJs.plugins.push(
new Wrap(
'(function(){function init(){\n',
'\n};window.ftNextnUiLoaded ? init() : document.addEventListener ? document.addEventListener(\'ftNextnUiLoaded\', init) : document.attachEvent(\'onftNextnUiLoaded\', init);})();',
{ match: /\.js$/ }
)
);

variants.push(mainJs);

if (process.env.NEXT_APP_SHELL === 'local') {
const nWebpackWarning = `
/*********** n-webpack warning ************/

You have set the environment variable NEXT_APP_SHELL=local
Expand All @@ -48,43 +81,36 @@ or similar). It will slow down your build A LOT!!!!

If you do not need this behaviour run

unset NEXT_APP_SHELL
unset NEXT_APP_SHELL

/*********** n-webpack warning ************/
`;
console.warn(nWebpackWarning); // eslint-disable-line no-console
console.warn(nWebpackWarning); // eslint-disable-line no-console

const ignoresNUi = fs.readFileSync(path.join(process.cwd(), '.gitignore'), 'utf8')
.split('\n')
.some(line => line === '/public/n-ui/');

if (!ignoresNUi) {
throw 'Add /public/n-ui/ to your .gitignore to start building a local app shell';
}
const ignoresNUi = fs.readFileSync(path.join(process.cwd(), '.gitignore'), 'utf8')
.split('\n')
.some(line => line === '/public/n-ui/');

const appShellBuild = Object.assign(clone(nWebpackOptions), {
language: 'js',
env: 'dev',
withBabelPolyfills: false,
output: {
filename: '[name]',
library: 'ftNextUi',
devtoolModuleFilenameTemplate: 'n-ui//[resource-path]?[loaders]'
},
entry: {},
exclude: [/node_modules/]
});

appShellBuild.entry['./public/n-ui/es5.js'] = './bower_components/n-ui/build/deploy/wrapper.js'
variants.push(appShellBuild);
if (!ignoresNUi) {
throw 'Add /public/n-ui/ to your .gitignore to start building a local app shell';
}
// can't just variants.map(nWebpack) becaue second param truthiness
return variants.map(conf => nWebpack(conf))
}

const baseConfig = Object.assign({}, {
withHeadCss: true,
withHashedAssets: true
}, require(path.join(process.cwd(), 'n-ui-build.config.js')));
const appShellBuild = Object.assign(clone(baseConfig), {
language: 'js',
env: 'dev',
withBabelPolyfills: false,
output: {
filename: '[name]',
library: 'ftNextUi',
devtoolModuleFilenameTemplate: 'n-ui//[resource-path]?[loaders]'
},
entry: {
'./public/n-ui/es5.js': './bower_components/n-ui/build/deploy/wrapper.js'
},
exclude: [/node_modules/]
});

variants.push(nWebpack(appShellBuild));
}

module.exports = constructVariants(baseConfig);
module.exports = variants
36 changes: 22 additions & 14 deletions build/deploy/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';
const nWebpack = require('@financial-times/n-webpack');
const headCss = require('../lib/head-css')

const coreConfig = {
output: {
Expand All @@ -11,9 +12,7 @@ const coreConfig = {
exclude: [/node_modules/]
};

// Build variants of the bundle that work with different combinations of feature flags
// Only build some of them when bower linking in dev to save build time
module.exports = [
const configs = [
{
withBabelPolyfills: false,
env: 'dev',
Expand All @@ -22,23 +21,32 @@ module.exports = [
},
buildInDev: true
},
{
withBabelPolyfills: false,
env: 'prod',
entry: {
'./dist/assets/es5.min.js': './build/deploy/wrapper.js'
}
},
{
withBabelPolyfills: false,
env: 'prod',
entry: {
'./dist/assets/n-ui-core.css': './build/deploy/shared-head.scss'
},
withHeadCss: true,
wrap: undefined,
buildInDev: true
}
]
.filter(conf => conf.buildInDev || !process.env.DEV_BUILD)
.map(conf => nWebpack(Object.assign({}, coreConfig, conf)));
];

if (!process.env.DEV_BUILD) {
configs.push({
withBabelPolyfills: false,
env: 'prod',
entry: {
'./dist/assets/es5.min.js': './build/deploy/wrapper.js'
}
})
}

module.exports = configs
.map(conf => {
const webpackConf = nWebpack(Object.assign({}, coreConfig, conf));
if (conf.withHeadCss) {
return headCss(webpackConf)
}
return webpackConf
})
29 changes: 29 additions & 0 deletions build/lib/addons/wrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Slightly modified version of entry-wrap-webpack-plugin https://github.com/shakyShane/entry-wrap-webpack-plugin
const ConcatSource = require('webpack/lib/ConcatSource');

function Wrap (before, after, options) {
this.options = options || {};
this.before = before;
this.after = after;
}

Wrap.prototype.apply = function (compiler) {
const options = this.options;
const before = this.before;
const after = this.after;

compiler.plugin('compilation', function (compilation) {
compilation.plugin('optimize-chunk-assets', function (chunks, callback) {
chunks.forEach(function (chunk) {
if(!chunk.initial) return;
const files = chunk.files.filter(file => options.match ? options.match.test(file) : true);
files.forEach(function (file) {
compilation.assets[file] = new ConcatSource(before, '\n', compilation.assets[file], '\n', after);
});
});
callback();
});
});
};

module.exports = Wrap;
6 changes: 6 additions & 0 deletions build/lib/head-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const ExtractCssBlockPlugin = require('extract-css-block-webpack-plugin');

module.exports = function (config) {
config.plugins.push(new ExtractCssBlockPlugin());
return config;
}
8 changes: 5 additions & 3 deletions demo/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict';

const nWebpack = require('@financial-times/n-webpack');
const headCss = require('../build/lib/head-css')
const path = require('path');
module.exports = nWebpack({
const webpackConfig = headCss(nWebpack({
withBabelPolyfills: false,
withHeadCss: true,
entry: {
'./public/main-without-n-ui.js': './demo/client/main.js',
'./public/main.css': './demo/client/main.scss'
Expand All @@ -13,4 +13,6 @@ module.exports = nWebpack({
path.join(__dirname, '../')
],
exclude: [/node_modules/]
});
}));

module.exports = webpackConfig;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@
"uglifyjs": "^2.4.10"
},
"dependencies": {
"@financial-times/n-webpack": "^3.0.0",
"@financial-times/n-express": "^19.0.0",
"@financial-times/n-handlebars": "^1.15.0",
"@financial-times/n-polyfill-io": "^1.0.9",
"@financial-times/n-webpack": "^2.0.0",
"@financial-times/next-json-ld": "^0.1.0",
"aws-sdk": "^2.7.21",
"chokidar": "^1.6.1",
Expand Down