diff --git a/.gitignore b/.gitignore index 37464bc9dce..f92c9998ade 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ css/**/*.css yarn.lock cypress/videos/* /public/styles/octicons/octicons.html +/precompiled/ diff --git a/lib/i18n.js b/lib/i18n.js index 5e0f061f00d..486db75a886 100644 --- a/lib/i18n.js +++ b/lib/i18n.js @@ -2,7 +2,8 @@ require('require-yaml') const i18n = require('electron-i18n') const flat = require('flat') -const { get, set } = require('lodash') +const get = require('lodash/get') +const set = require('lodash/set') const locales = Object.keys(i18n.locales) const websiteStrings = require('../data/locale.yml') const websiteKeys = Object.keys(flat(websiteStrings)) diff --git a/middleware/browserify-opts.js b/middleware/browserify-opts.js new file mode 100644 index 00000000000..fcaa09d3ecc --- /dev/null +++ b/middleware/browserify-opts.js @@ -0,0 +1,25 @@ +const nodeModulesToAvoidBabelifying = [ + 'lodash', + 'lunr', + 'prettydate' +] + +const excludeRegex = new RegExp(`/node_modules/(${nodeModulesToAvoidBabelifying.join('|')})`) + +module.exports = function doBrowserify (browserify) { + return function (entry) { + return browserify(entry, { + transform: [ + ['babelify', { + global: true, + exclude: excludeRegex, + presets: [ + ['@babel/preset-env', { targets: '> 0.25%, not dead' }] + ] + }], + 'brfs' + ] + }) + } +} + diff --git a/middleware/browserify.js b/middleware/browserify.js index 69904ba1b56..f2b04f476b6 100644 --- a/middleware/browserify.js +++ b/middleware/browserify.js @@ -1,26 +1,4 @@ const browserify = require('browserify-middleware') +const browserifyOptions = require('./browserify-opts') -const nodeModulesToAvoidBabelifying = [ - 'lodash', - 'lunr', - 'prettydate' -] - -const excludeRegex = new RegExp(`/node_modules/(${nodeModulesToAvoidBabelifying.join('|')})`) - -function babelifyMiddleware (entry) { - return browserify(entry, { - transform: [ - ['babelify', { - global: true, - exclude: excludeRegex, - presets: [ - ['@babel/preset-env', { targets: '> 0.25%, not dead' }] - ] - }], - 'brfs' - ] - }) -} - -module.exports = babelifyMiddleware +module.exports = browserifyOptions(browserify) diff --git a/package-lock.json b/package-lock.json index 03a0cf73e5d..853046131a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13590,7 +13590,6 @@ "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "optional": true, "requires": { "commander": "~2.17.1", "source-map": "~0.6.1" @@ -13599,14 +13598,12 @@ "commander": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "optional": true + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, diff --git a/package.json b/package.json index 98b37e2acfd..eceb69a7425 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,12 @@ "release": "node script/release", "dev": "cross-env NODE_PATH=. NODE_ENV=development nodemon server.js", "prepack": "check-for-leaks", + "precompile-assets": "cross-env NODE_ENV=production node script/precompile-assets.js", "linkschecker": "NODE_PATH=. NODE_ENV=test node scripts/links-checker.js", "cypress": "cypress run", "lint": "standard --fix", - "generate-octicons": "node ./script/generate-octicons.js" + "generate-octicons": "node ./script/generate-octicons.js", + "heroku-postbuild": "npm run precompile-assets" }, "husky": { "hooks": { @@ -99,6 +101,7 @@ "standard": "^12.0.1", "supertest": "^3.4.2", "supertest-session": "^3.3.0", + "uglify-js": "^3.4.9", "wait-on": "^3.1.0", "walk-sync": "^1.1.3" }, diff --git a/script/integration.sh b/script/integration.sh index 158c68e5150..7e3c62a2feb 100755 --- a/script/integration.sh +++ b/script/integration.sh @@ -1,3 +1,4 @@ +npm run precompile-assets npm start & wait-on http://localhost:5000 cypress run --record --key a0cba5c6-0650-4abe-8d41-d990bb7a0a66 diff --git a/script/precompile-assets.js b/script/precompile-assets.js new file mode 100644 index 00000000000..ea719a10f5e --- /dev/null +++ b/script/precompile-assets.js @@ -0,0 +1,90 @@ +const path = require('path') +const stream = require('stream') +const fs = require('fs-extra') +const browserify = require('browserify') +const browserifyOptions = require('../middleware/browserify-opts') +const sass = require('node-sass') +const uglify = require('uglify-js') + +function dir (...parts) { + return path.join(__dirname, '..', ...parts) +} + +function uglifyStream () { + const buffers = [] + return new stream.Transform({ + transform (chunk, _encoding, callback) { + buffers.push(chunk) + callback() + }, + + flush (callback) { + const code = Buffer.concat(buffers).toString() + const compiled = uglify.minify(code) + this.push(compiled.code) + callback() + } + }) +} + +const PATHS = { + precompiled: dir('precompiled'), + scripts: dir('precompiled', 'scripts'), + styles: dir('precompiled', 'styles'), + nodeModules: dir('node_modules'), + + jsEntry: dir('scripts', 'index.js'), + jsDestination: dir('precompiled', 'scripts', 'index.js'), + + cssEntry: dir('public', 'styles', 'index.scss'), + cssDestination: dir('precompiled', 'styles', 'index.css') +} + +async function precompileAssets () { + try { + console.log('Creating directories...') + await fs.remove(PATHS.precompiled) + await fs.ensureDir(PATHS.scripts) + await fs.ensureDir(PATHS.styles) + console.log('Precompiling JS...') + await precompileJavaScript() + console.log('Precompiling CSS...') + await precompileCss() + } catch (err) { + console.error(err) + process.exit(1) + } +} + +function precompileJavaScript () { + return new Promise((resolve, reject) => { + const b = browserifyOptions(browserify)(PATHS.jsEntry) + const pipe = b.bundle() + .pipe(uglifyStream()) + .pipe(fs.createWriteStream(PATHS.jsDestination)) + pipe.on('error', reject) + pipe.on('finish', resolve) + }) +} + +function precompileCss () { + return new Promise((resolve, reject) => { + sass.render({ + file: PATHS.cssEntry, + includePaths: [ + PATHS.nodeModules + ] + }, async function onSassCompiled (err, result) { + if (err) { + return reject(err) + } + + await fs.writeFile(PATHS.cssDestination, result.css) + resolve() + }) + }) +} + +if (require.main === module) { + precompileAssets() +} diff --git a/scripts/apply-active-class-to-active-links.js b/scripts/apply-active-class-to-active-links.js index 72bc40a09f0..ee3c54e97a1 100644 --- a/scripts/apply-active-class-to-active-links.js +++ b/scripts/apply-active-class-to-active-links.js @@ -1,4 +1,4 @@ -const { escapeRegExp } = require('lodash') +const escapeRegExp = require('lodash/escapeRegExp') module.exports = function applyActiveClassToActiveLinks () { const topPath = `/${location.pathname.split('/')[1]}` diff --git a/scripts/create-filter-list.js b/scripts/create-filter-list.js index 21a7889e555..19c4389d1b9 100644 --- a/scripts/create-filter-list.js +++ b/scripts/create-filter-list.js @@ -1,4 +1,4 @@ -const { debounce } = require('lodash') +const debounce = require('lodash/debounce') const lunr = require('lunr') const queryString = require('query-string') const setQueryString = require('set-query-string') diff --git a/scripts/lazy-load-images.js b/scripts/lazy-load-images.js index 9b2515d20ee..d23d0dd1cde 100644 --- a/scripts/lazy-load-images.js +++ b/scripts/lazy-load-images.js @@ -1,4 +1,4 @@ -const { throttle } = require('lodash') +const throttle = require('lodash/throttle') function inViewport (element) { const { top, right, bottom, left } = element.getBoundingClientRect() diff --git a/server.js b/server.js index c7109c5351b..3973971462c 100644 --- a/server.js +++ b/server.js @@ -45,8 +45,14 @@ app.set('views', path.join(__dirname, '/views')) app.use(nakedRedirect(true, 'www', 302)) app.use(compression()) app.use(helmet()) -app.use(sass()) -app.use('/scripts/index.js', browserify('scripts/index.js')) +if (process.env.NODE_ENV === 'production') { + console.log('Production app detected; serving JS and CSS from disk') + app.use(express.static(path.join(__dirname, 'precompiled'), { redirect: false })) +} else { + console.log('Dev app detected; compiling JS and CSS in memory') + app.use(sass()) + app.use('/scripts/index.js', browserify('scripts/index.js')) +} app.get('/service-worker.js', (req, res) => res.sendFile(path.resolve(__dirname, 'scripts', 'service-worker.js'))) app.use(cookieParser()) app.use(requestLanguage({ diff --git a/test/localization.js b/test/localization.js index 95a066d4f92..bb0924e89ac 100644 --- a/test/localization.js +++ b/test/localization.js @@ -8,7 +8,7 @@ const fs = require('fs') const path = require('path') const walk = require('walk-sync') const flat = require('flat') -const getProp = require('lodash').get +const getProp = require('lodash/get') const locale = require(path.join(__dirname, '../data/locale.yml')) const views = walk.entries(path.join(__dirname, '../views'))