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
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
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
andweb-core-router
are not open-source, so you'll need to uninstall those and remove their references fromapp/index.ts
.
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”.
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
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.
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.
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)
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.
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
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.
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:
require
)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.
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:
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();
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).
No opinions here. First person to need them gets to write the docs ;)
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;
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();
We call them smoke tests. They're generally run by CI/CD.
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.
Will cause iqb to examine itself and the build/
directory to print information about which
commands and flags are available.
Can be used to fetch updated project tooling from the upstream repository. This update will overwrite any changes to:
bin/iqb
bin/headless
build/deploy
build/start-server
build/update
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 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.
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.
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
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.
A pretty minimal container that can be used to serve a bedrock application. Intended use case is for DCOS but it should work anywhere.
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.