relateiq/bedrock

Name: bedrock

Owner: SalesforceIQ

Description: null

Created: 2015-05-15 17:53:02.0

Updated: 2016-10-28 06:46:24.0

Pushed: 2017-03-08 18:58:01.0

Homepage: null

Size: 168

Language: Shell

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

Welcome to Bedrock

Because I'm tired of typing it, going forward “Angular 1.x” will be referred to as “A1.x” and 2.0 will be “A2.0”. You've been warned.

Disclaimer: bedrock relies on some tooling and dependencies that are private to SalesforceIQ, and therefore this may not be useable “as-is” for non-SalesforceIQ development. Specifically, uiq and web-core-router are not open-source, so you'll need to uninstall those and remove their references from app/index.ts.

What is Bedrock

It's a Typescript/A1.x based shell that provides the same basic environment as the CRM web application, including UIQ, Routing, and familiar build tools. It also provides all the fun stuff you need to get up and running with typescript (including build tools, basic type definitions for node, angular, ES6, etc.).

The goal of bedrock is to provide a “playground” or wrapper that allows you to develop new components in isolation. It's pretty cool.

Bedrock provides the foundation for the development of what we refer to as “web modules”.

Shell App Structure

README.md             You better know what a README.md is
app                   The source code for the shell application (super simple angular app)
??? app.scss          SCSS specific to the app shell (should be minimal)
??? index.html        HTML that powers the app shell (should be minimal)
??? index.spec.ts     Integration tests (can exist in any subdirectory of app/)
??? index.ts          Boostraps the A1.x shell app (should be minimal)
??? tsconfig.json     Typescript project file for the app
assets
bin                   Build tools
??? headless
??? iqb
build                 Build tasks
??? feat
??? start-server
??? update
node_modules          node modules... 
package.json
release               Where all build artifacts end up (this is the WWW root for the local server)
??? assets
??? index.html
src                   Your module's source code.  99% of your work should be done in here
??? index.html
??? index.scss
??? index.ts
??? tsconfig.json
typings               Type definitions for non-typescript modules (angular, node, etc.)
??? browser.d.ts
??? main.d.ts
typings.json          Configuration file that determines which type definitions get installed
Getting Started

UI modules should export an angular module containing a directive. In the case of Modals/Dialogs you'll also probably want to export a service that makes the modal easy to use. My recommendation is to create that service somewhere in src/ and add it to the angularModule that gets exported from src/index.ts, then you can inject it normal-style.

The routing for bedrock is handled in app/index.ts. If you change the name of the root module from SampleModule (and you should), then you'll need to update the default view in app/index.ts to reflect the new root directive.

A few notes about Typescript
Type Definitions

Typescript needs to understand where modules are defined and what their type is to work properly. To this effect, globals like module or require or angular will throw compile errors unless you define them. This is what the typings/ directory is for. To install type definitions for a module that you want to use, but didn't write, you can run:

ngs install modulename --save

The order of arguments here matters. If you put the --save flag before the module name, it won't work. sadface. angry face.

Classes

Use classes, but avoid deep inheritance hierarchies. Hierarchies should be kept as shallow as possible. If you do something like AbstractDataService -> SpecificDataService -> SpecificDataServiceWithFeature -> MoreSpecificDataServiceWithMoreFeatures then I will find you and scold you. Repeatedly. Classes are also useful if you need to add intrinsic logic to your objects (getters, setters, methods).

Do your best to keep Display logic (eg: anything referenced by a template) separate from model/business logic (eg: anything referenced by a service/controller)

Preparing for A2.0

The app shell is designed to follow an “A2.0-style” structure as much as possible. The A2.0 dependency injector provides a shotloaf of awesome new features, including injector hierarchies, custom providers, etc. We should make it easy to move modules over to that structure.

The conventions described below are also designed to help make unit testing as easy as possible. If you follow the rules, then you usually won't need to actually load angular for your tests. This means you don't need a DOM, which means that your tests don't have to be browserified to run, which makes them much faster.

The A1.x Way

Our traditional A1.x approach is to create a module folder, which contains an index file, usually a directive definition, a controller, maybe some services, and some templates:

av
CustomizeNavDialogCtrl.js
TopNav-spec-helper.js
TopNavCnst.js
TopNavCustomizeSrvc.js
TopNavCustomizeSrvc.spec.js
TopNavDrtv.js
TopNavDrtv.spec.js
TopNavListFiltersDrtv.js
TopNavSearchBarDrtv.js
TopNavSearchBarDrtv.spec.js
TopNavSrvc.js
TopNavSrvc.spec.js
index.js

This works really well in A1.x, but in A2.0, controllers, factories, services, constants aren't things anymore. Everything is a class, and you create providers that create instances of classes.

There's an amazing(ly long) write-up about the A2.0 injector at https://angular.io/docs/ts/latest/guide/dependency-injection.html

The A2.0 Way

Disclaimer: this structure is still open for discussion and is subject to change

Within the src/ folder, the only files that should contain the word angular should be in src/ui/**/index.ts. You can still use angular services like $http or $q in your classes, and you'll need to provide mocks in your unit tests for those services.

                                  the TopNav module should be it's own repo based on bedrock
interfaces                        public contracts that consuming modules need to implement or know about
??? ITopNav.ts
lib                               lib contains business logic. nothing UI-related should live here
??? constants.ts
??? top-nav-customize
    ??? TopNavCustomize.spec.ts   test: sidecar files next to what they're testing
    ??? TopNavCustomize.ts        the data model / business logic for TopNavCustomization
ui                                ALL UI Components live here. There should be minimal/no business logic
??? top-nav
|   ??? index.ts                  This is the only file that has anything to do with angular.
|   ??? TopNav.spec.ts            Tests the TopNav.ts file without bootstraping an angular-app
|   ??? TopNav.ts                 The 'controller'; should have nothing angular-specific in it
??? top-nav-list-filters
|   ??? index.ts
|   ??? TopNavListFilters.spec.ts
|   ??? TopNavListFilters.ts
??? top-nav-search-bar
    ??? index.ts
    ??? TopNavSearchBar.spec.ts
    ??? TopNavSearchBar.ts

This basic structure tries to make as much of the code as possible framework-agnostic. The entirety of a module's business logic (models, contracts, validators, etc.) should live in the lib/ folder. It should be possible to get 100% unit test coverage of this folder without involving angular.

Rule of Thumb: when deciding if something belongs in lib/, think to yourself “if I decided to write a CLI tool that did the same thing as the GUI, would I be able to do that using only the objects in lib?“. If the answer is no, then you should probably think about refactoring. If you're building a UI component (like popgun, grid, or something like that, then this rule probably doesn't apply).

The UI folder contains angular components. Those components have associated controllers. You can usually test the entirety of the controller logic without involving angular. Additionally, no logic should be performed in views. This means (nearly) never accessing model values directly. If you're tempted to do something like ng-if="$scope.viewModel.prop === 'Something'", or god forbid ng-style="{'z-index': 1040 + (index && 1 || 0) + index*10}", then stop immediately and do this instead: ng-if="ctrl.currentPropIs('Something')" or ng-style="{'z-index': $ctrl.getZIndex(index)}". This makes the views much simpler and the logic easier to test.

Testing

I have pretty strong opinions regarding testing. This shell app greatly reflects my opinions. Take it or leave it, but I think it's a pretty good idea. If you don't like it, pull requests are welcome.

Tests are written using tape as the testing framework. I picked this over Jasmine or Mocha/Chai for a number of reasons:

You should test enough things to feel comfortable that the system works, and if you start spending a lot of time discussing the testing frameworks/strategies/whatever then it distracts you from building the thing you're trying to test.

Both unit tests and integration tests are written with tape. Unit tests are executed as-is using raw node. Integration tests are browserified and executed with Karma + Headless Firefox.

But How Do I Debug???

node-inspector is the bee's knees. npm install -g node-inspector and then node-debug .src/SomeModule.js to debug it.

Note that you have to run node-debug on the compiled JS file, but node-debug understands sourcemaps, so you'll actually be debugging TS. :yey:

What about beforeEach/afterEach? Those are pretty nice.

They are pretty nice, but having globally defined functions (like describe, it, beforeEach, afterEach, etc.) is not pretty nice. To accomplish the same thing in tape, follow this pattern:

rt tape = require('tape');

tion test(description, unitTestFunc) {
 do your beforeEach stuff here

pe(description, unitTestFunc);

 do your afterEach stuff here


('this is my unit test', function(t) {
equal(1, 1);
end();

Spies

I don't like 'em. I think they're a code smell. If you think you need a spy, you might need to refactor your code into smaller units. Or you might be trying to write an integration test, in which case it follows some slightly different rules (see below).

Mocks

No opinions here. First person to need them gets to write the docs ;)

Unit Tests

Unit tests should do what they say on the box. They should test a single unit of the application. If your unit test has a lot of requires/imports, you're probably not writing a unit test.

Generally, unit tests will test everything in lib/ and (ideally) all of the angular controllers in the ui/ folder. If the code is structured properly, then it should be possible to isolate the logic and test it without using angular, browserify, or any other heavy dependencies.

In bedrock, the src/ui/**/index.ts files are not unit tested. Their only purpose is to provide the glue that binds your raw classes to angular. Therefore, there's no logic to test, and their behavior should be verified with an integration test.

The Wrong Way: put it all in one file

lar.module('MyModule', [])
irective('sampleModule', function() {
return {
  restrict: 'E',
  scope: {},
  controller: function($q, $http) {
    this.isHardToTest = function() {
      /*
       * this is hard to test because you have to bootstrap angular just to access this method,
       * and because of the HTML require, you also have to browserify everything... which is lame
       * you clearly don't need angular to test that this returns 4
       */
      return 2 + 2;
    }
  },
  controllerAs: 'ctrl',
  template: require('./index.html')
}
;

The Right Way: split it up

rt SampleModuleCtrl from './SampleModule';

ow this file has no logic that needs testing.  its behavior can be tested in an
ntegration test instead of a unit test.
lar.module('MyModule', [])
irective('sampleModule', function() {
return {
  restrict: 'E',
  scope: {},
  controller: SampleModuleCtrl,
  controllerAs: 'ctrl',
  template: require('./index.html')
}
;
ypescript
rt class SampleModuleCtrl {
nstructor($q, $http) {
// for testing purposes, you can provide your own implementations
// of $q and $http without involving angular.


blic isNoLongerHardToTest() {
// super easy to test because you have 0 dependencies on angular here
return 2 + 2;


Integration Tests

Integrations tests are written in tape and executed in a headless firefox browser using Karma.

Integration tests can make sure that when all of the individual units are put together (or integrated… get it?), the entire system works as expected. You should have far fewer integration tests than unit tests. They will also run less frequently (only on build, not during normal development) and take longer than unit tests. Browserifying the code, starting karma, and starting a headless browser all takes time (usually 5-10 seconds), and so an integration test watcher will NOT automatically start with the start command. You'll need to explicitly pass -i if you want integration tests to run.

Integration tests should live somewhere in the app/ folder, as they'll likely require things like a DOM, angular, UIQ, and other peer dependencies. Since bedrock also provides a similar environment to the webapp, it's an ideal place to make sure all the pieces fit together.

Writing integration tests is as simple as just creating a *.spec.ts file inside of the app/ folder. The difference between a unit test and an integration test is just that integration tests have access to the DOM; the actual code looks pretty much identical. Integration tests are executed after the shell-app has bootstrapped, and (unless you manually change the view) will execute against the default view specified in app/index.ts.

Here's an example integration test:

reference path="../../typings/browser.d.ts" />
t test = require('tape');

efine a test using tape (same exact way as writing a unit test)
('first available option is checked', t => {
 this test has access to the same global variables as app/index.ts
t elems = jQuery("create-list-view [dts='riq_pick_radio'] li"); 

 run a few assertions to make sure the expected DOM structure is there
ok(elems.length > 1, 'the create list modal should have at least 1 option available');
ok(elems[0].classList.contains('riq-pick-option-selected'), 'and the first should be selected');

 woohoo!
end();

Functional Tests

We call them smoke tests. They're generally run by CI/CD.

IQB

IQB is the IQ Build tool, and it provides the actually build logic needed to develop with bedrock. It's a shell script that provides foundational tools to build, run, and develop in bedrock.

IQB tasks can be overridden by creating a build/taskname file and making it executable.

iqb help

Will cause iqb to examine itself and the build/ directory to print information about which commands and flags are available.

iqb update

Can be used to fetch updated project tooling from the upstream repository. This update will overwrite any changes to:

You should not make changes to any of these files. Those changes will get clobbered the next time you run iqb update.

iqb update will also update the NPM packages used by bedrock. You can get a full list of these packages by looking at the update script. Changing versions of those is also not recommended for the same reason.

llexec

llexec is the result of me getting pretty fed up with GNU parallel. More details are available in that project's repo: http://github.com/relateiq/llexec

llexec is used to execute IQB tasks in parallel.

SalesforceIQ Employees Only
iqproj

You can initialize a new web-module by running iqproj new webmod <name>. This command is defined in our development environment and will clone this repo, squash the commits, and do some renaming in the package.json file.

CI/CD

Once you've created a new web-module, you should set up CI/CD for the module. Contact a team lead to set this up. You can see a list of all web modules currently under CI/CD at https://teamcity.amz.relateiq.com/project.html?projectId=RIQ2014_WebModules

iqb deploy

The deploy script will build a webapp docker container that serves assets from release/. Generally, you shouldn't run this locally, but instead should set up CI/CD in TeamCity.

This script requires either docker-machine, the docker for mac beta, or a linux machine to work properly.

docker-bedrock-webserver

A pretty minimal container that can be used to serve a bedrock application. Intended use case is for DCOS but it should work anywhere.

docker-bedrock-build

A container that's used by CI/CD to actually build bedrock. This keeps all the dependencies in the container so the production engineering team doesn't need to update all our build agents just because someone's requirements changed.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.