Skip to content

Build process

James edited this page Jul 1, 2017 · 5 revisions

build system:

time

problem

  • we want to write code in the latest greatest, but not everyone can haz it
  • we want all the latest greatest, and for it to work with all the old things
  • it has to be performant & lean, yet easy to debug

solution

  • try and make everyone happy especially ourselves, use all the tools!
  • use D8 (debug shell for V8) with irhydra with locally built irhydra using dart -> awesome-deopt to easily find your deopts & quickly optimizable functions, don't be fooled by js
  • make a bunch of small side-effect-free functions that can be easily scoped when you do your build
  • be safe with export naming & sugar syntax for import/export, test your dist files
  • add a lot of debugging functionality wrapped in conditionals that will be dropped out when you build, but kept for development versions
  • export for lots of formats, the more the merrier, make the api painless for people to use

steps

  • this makes it extremely easy to export & run quickly
  • easy understanding & control over exactly how your code will turn out when you export it

1. run the Makefile with shorthand combinations for what needs to be run (e.g., make, or make copy test cov prepublish)

  • all npm scripts are run by default with Makefile
  • all scripting logic is done in Makefile, e.g. yarn run jest -- --coverage
  • none of the npm scripts are in package.json, it looks like this ```js "scripts": { "buble": "buble", "rollup": "rollup", "jest": "jest", "webpack": "webpack", "gzip": "gzip-size", }

2. copy with flow-remove-types (types are no longer in the src but the copying for easy importing is still helpful)

  • src/ -> / copying the files to the root encourages much easier modular imports
  • src/ -> dist/
  • test/ -> test-dist

3. code coverage: run buble on the dist/ & test-dist/ with sourceMaps inlined (output into those same respective dirs)

4. nyc runs ava, which uses babel, which uses the inline sourceMaps from buble... this is an unfortunate & required convoluted sequence of steps because:

  • nyc does not support for es6
  • ava forces the use of babel
  • unfortunately the tap reporter for ava doesn't play nicely with covert (which is old anyway)
  • there are barely any code coverage libraries for js
  • babel has issues (breaks) when running babel-transpiled-code that extends raw es6 classes

UPDATE - migrated to jest!

  • much easier code coverage available with just --coverage
  • no need to transpile and deal with sourceMaps
  • about 10x faster
  • some inconsistent issues on CI that are known jest issues
  • used jest codemods which worked very well this time

5. run rollup - exports

  • rollup: config: {} entry in package.json for some targets that need a little extra config
  • we already have a dist folder that is es3+, and we can use it for each target (vs transpiling all of the source code each time)
  • best practice here is an index/entry file that just conditionally requires & exports the right file for the environment
  • rollup allows multiple targets each with their own format which I'll inline for convenience
    • amd – Asynchronous Module Definition, used with module loaders like RequireJS
    • cjs – CommonJS, suitable for Node and Browserify/Webpack/FuseBox
    • es (default) – Keep the bundle as an ES module file
    • iife – A self-executing function, suitable for inclusion as a <script> tag. (If you want to - create a bundle for your application, you probably want to use this, because it leads to - smaller file sizes.) (preact builds with this)
    • umd – Universal Module Definition, works as amd, cjs and iife all in one ❗ (this is default dev export, used also for dev by inferno)

UPDATE

  • additional exports specifically have been crafted with a custom rollup plugin, rollup-plugin-falafel do some things the rollup replace plugin cannot do,
    • remove the not-needed unwrapModuleExports wrapper (for export default)
    • replace constant variable names before rollup temporarily changes all code to es6 imports and then back to commonjs
  • debugger exports added, which is a set of the best places for the debugger to be used, so just importing from (or aliasing to) /debugger will enable that flow
  • .min (as mangled) exports alongside the more readable dist files for each format that are compressed but not mangled, shaken, not stirred.
  • // @TODO need to document the upcoming documentation generating

6. production finishing touches:

  • replace our environment variables to remove things for production
  • export a development build that keeps these conditions, for easier debugging
  • uglify-js3 is run with uglify-es
  • optimize-js wraps some functions
  • eslint uses babeleslint to check the code, prettier makes it pretty - then is forced back by eslint if the rules are overriden (since it does not respect some of them)
  • codacy checks the lint rules in case any were missed by local tools
  • travis does the same thing all over again so we are sure it didn't "just work for us"
  • coveralls & badgesize gets the result & then you get badges & graphs
  • record build data size-over-time
    • gzip-size of the build gzip-size dist/index.js --raw >> build/size-over-time.txt
    • record date date +%Y:%M:%D:%H:%M:%S >> build/size-over-time.txt
    • format echo --- >> build/size-over-time.txt
    • comment (should do a cli prompt)
  1. experiment with other bundling setups
  • fuse-box has a much easier build process, reports gzip, much faster, can compile with buble, typescript, or babel without any extra plugins, but the size is just a little bigger than rollup so I can't use it for main export yet - but likely soon
  • at one point, creating a rollup bundle of es6 code, transpiling that with typescript (so helpers are not duplicated, though with the latest version they have a helper for that), then re-bundling & uglifying gave a little better size than buble, but it didn't stay that way for long :-/
  • using webpack, even with the new webpack 3 scope hoisting was many many times bigger than everything else so it was just not an option, more for applications than libraries (using webpack is much better with webpack-chain)
  • NOTE these were not used, but are other bundlerishes
    • gulp probably would work, but would need 100 plugins
    • browserify, although the slowest, was the easiest to just with 1 line cli cmd, and is the most stable by far, so when I just wanted to zip an html + css + js file, it just worked no trouble no config, kudos there, but not really for optimizing size
    • brunch is definitely not focused on this job, but similar to yeoman in the way that it can get you started with a skeleton, probably worth putting a getting started repo on it for chainable
    • grunt using grunt nowadays, is like using underscore, jquery, and coffeescript all together nowadays - not for a good reason (which there are), but just because the code wasn't maintained
    • pin.gy nice looking cli, but that's about the same as browserify without being an OG
    • broccoli.js it says node 0.10.x as node --version for "latest"

related

targets

some notes

  • most important & standard ones are main, browser
  • nobody should be using js:next, module replaces it
  • typings is for typescript
{
  "main:es6": "src/index.js",
  "main:dev": "dists/dev/index.js",
  "main:tsc": "dists/tsc/index.js",
  "main:iife": "dists/iife/index.js",
  "main:umd": "dists/umd/index.js",
  "main:cjs": "dists/cjs/index.js",
  "main:es": "dists/es/index.js",
  "js:next": "dists/es/index.js",
  "main": "dists/umd/index.js",
  "module": "dists/umd/index.js",
  "web": "dists/cjs/index.js",
  "browser": "dists/cjs/index.js",
  "alias": "dists/cjs/index.js",
  "amd": "dists/amd/index.js",
  "types": "typings/index.d.ts",
  "typings": "typings/index.d.ts"
}

ReplaceDefine

This is how replace/define plugin works:

Libraries (such as react, inferno, etc) (see react-readme, react-code, inferno-code) have conditionals which look like the following:

if (process.env.NODE_ENV === 'development') { /* do debugging */ }

After it has been run with a config similar to:

define({
  'process.env.NODE_ENV': JSON.stringify('production'),
})

the library code will come out as

if ('production' === 'development') { /* do debugging */ }

when that code is run through uglify or babili with drop-dead-code (default: true), it can do static-analysis, and will remove that block, since 'production' is never 'development'.

since this is a string replacement, it does not mean that a process polyfill is required for the browser, so console.log(process.env) will not exist unless it is auto-polyfilled because of that console.log, or because you've explicitly added a polyfill that handles that.

the origin of define is C define

to see more on deadcode elimination

types & docs

export transpiling

be careful - module.exports & exports.name work perfectly well, test your dist files

next

closure & traceur I haven't tried with chainable yet so