angular-quickstart-lib
angular-quickstart-lib copied to clipboard
Gotchas and lessons learned
Below is a list of small issues and gotcha's that cost me a lot of time trying to built a non-trivial angular library using the starter template. The plan was to extract a fully functioning ngrx store from our main application so we could reuse it in future applications. This library has no visual components but does have external library dependencies.
I've written this text as a quick and dirty summary of my experiences and to hopefully save other people some time who are trying to do the same thing.
Everything as peerDependency
Only way to get my external dependencies to work is to add them all as both peerDependencies as devDepencies. This practice is discussed in the readme of the library seed but bears repeating.
Don't forget @types
Modules that don't expose their typings directly through their package.json need to be added.
So for lodash you would do: npm install --save-dev @types/lodash
Rollup index include
The rollup bundler will not understand any implicit include of an index.ts for a folder. So you will need to rewrite the following:
import { reducer } from './reducers';
to the following:
import { reducer } from './reducers/index';
Rollup warnings (externals)
You will be greeted by a wall of warning about globals and externals you use in your code. All the externals need to be defined in the build.js file. I ended up with the following list which will need to be updated again if I add new external code imports (ie. rxjs operators).
globals: {
// The key here is library name, and the value is the the name of the global variable name
// the window object.
// See https://github.com/rollup/rollup/wiki/JavaScript-API#globals for more.
'@angular/common': 'ng.common',
'@angular/core': 'ng.core',
'@angular/forms': 'ng.forms',
'@angular/platform-browser': 'ng.platform.browser',
'@angular/router': 'ng.router',
'@ngrx/core': 'ngrx.core',
'@ngrx/core/index': 'ngrx.core',
'@ngrx/effects': 'ngrx.effects',
'@ngrx/effects/index': 'ngrx.effects',
'@ngrx/router-store': 'ngrx.routerStore',
'@ngrx/router-store/index': 'ngrx.routerStore',
'@ngrx/store': 'ngrx.store',
'@ngrx/store/index': 'ngrx.store',
'my-sdk-js': 'MySDK',
'lodash': '_',
'lodash/index': '_',
'ngx-cookie': 'ngx-cookie',
'ngx-cookie/index': 'ngx-cookie',
'reselect': 'Reselect',
'reselect/index': 'Reselect',
'rxjs': 'Rx',
'rxjs/add/operator/catch': 'Rx.Observable.prototype',
'rxjs/add/operator/combineLatest': 'Rx.Observable.prototype',
'rxjs/add/operator/debounceTime': 'Rx.Observable.prototype',
'rxjs/add/operator/filter': 'Rx.Observable.prototype',
'rxjs/add/operator/first': 'Rx.Observable.prototype',
'rxjs/add/operator/map': 'Rx.Observable.prototype',
'rxjs/add/operator/mergeMap': 'Rx.Observable.prototype',
'rxjs/add/operator/pluck': 'Rx.Observable.prototype',
'rxjs/add/operator/skip': 'Rx.Observable.prototype',
'rxjs/add/operator/switchMap': 'Rx.Observable.prototype',
'rxjs/add/operator/takeUntil': 'Rx.Observable.prototype',
'rxjs/add/operator/throttleTime': 'Rx.Observable.prototype',
'rxjs/add/operator/withLatestFrom': 'Rx.Observable.prototype',
'rxjs/Observable': 'Rx',
'rxjs/observable/empty': 'Rx.Observable',
'rxjs/observable/fromEvent': 'Rx.Observable',
'rxjs/observable/fromPromise': 'Rx.Observable',
'rxjs/observable/merge': 'Rx.Observable',
'rxjs/observable/of': 'Rx.Observable',
'rxjs/observable/of': 'Rx.Observable',
'rxjs/observable/throw': 'Rx.Observable',
'rxjs/ReplaySubject': 'Rx',
'rxjs/Subject': 'Rx',
},
external: [
// List of dependencies
// See https://github.com/rollup/rollup/wiki/JavaScript-API#external for more.
'@angular/common',
'@angular/core',
'@angular/forms',
'@angular/platform-browser',
'@angular/router',
'@ngrx/core',
'@ngrx/core/index',
'@ngrx/effects',
'@ngrx/effects/index',
'@ngrx/router-store',
'@ngrx/router-store/index',
'@ngrx/store',
'@ngrx/store/index',
'my-sdk-js',
'lodash',
'lodash/index',
'ngx-cookie',
'ngx-cookie/index',
'reselect',
'reselect/index',
'rxjs',
'rxjs/add/operator/catch',
'rxjs/add/operator/combineLatest',
'rxjs/add/operator/debounceTime',
'rxjs/add/operator/filter',
'rxjs/add/operator/first',
'rxjs/add/operator/map',
'rxjs/add/operator/mergeMap',
'rxjs/add/operator/pluck',
'rxjs/add/operator/skip',
'rxjs/add/operator/switchMap',
'rxjs/add/operator/takeUntil',
'rxjs/add/operator/throttleTime',
'rxjs/add/operator/withLatestFrom',
'rxjs/Observable',
'rxjs/observable/empty',
'rxjs/observable/fromEvent',
'rxjs/observable/fromPromise',
'rxjs/observable/merge',
'rxjs/observable/of',
'rxjs/observable/of',
'rxjs/observable/throw',
'rxjs/ReplaySubject',
'rxjs/Subject',
],
Also note the additional entries for the xxx/index variants of the same externals. All these occur based on exactly how modules are imported inside your code. Just keep adding them until all the warnings are gone.
I'm still drowning in a sea of warnings which read: The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten
. Which apparently can be safely ignored. I found the following rollupjs issue with a code sample to hide the warnings which feels wrong to me. See Rollup Issue 794. I think mine are caused by the following ngrx effects code: fetch$: Observable<Action> = this.actions$
which I can't get around.
Casing matters
If you look closely you will see an uppercase Observable being referenced. This is not a typo. The observable in Rxjs itself is being exported that way while the operators use the lowercase folder name. It also cost me 30 minutes of my life to determine that reselects exported global has an uppercase R.
SystemJS setup
The included demo App uses systemjs to bundle externals. You will need to also declare externals there. I ended up with the following map:
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'@ngrx/core': 'npm:@ngrx/core/bundles/core.umd.js',
'@ngrx/effects': 'npm:@ngrx/effects/bundles/effects.umd.js',
'@ngrx/router-store': 'npm:@ngrx/router-store/bundles/router-store.umd.js',
'@ngrx/store': 'npm:@ngrx/store/bundles/store.umd.js',
'my-sdk-js': 'npm:my-sdk-js/dist/bundle.js',
'ngx-cookie': 'npm:ngx-cookie/bundles/ngx-cookie.umd.js',
'reselect': 'npm:reselect/dist/reselect.js',
'lodash': 'npm:lodash/lodash.js'
},
You pretty much need to look inside each node_module folder and package.json to figure out the entry points. More hilarity ensued when lodash still wouldn't work. Lodash could not be declared as npm:lodash/index.js
because that in turn includes lodash.js which couldn't be resolved when running the demo app. So problems that arise will be completely dependant on the specific module(s) you require. Note that newer versions of modules may once again break this setup as they reorganise their internal file setup.
npm link
Simply does not work as a development setup. You start getting errors like this:
Type 'Observable<boolean>' is not assignable to type 'Observable<boolean>'. Two different types with this name exist, but they are unrelated
And more of the same for all the angular/xxx stuff because there are effectively two versions of the same code floating around. TypeScript issue 6496
After building you can npm install the dist/ folder which effectively copies the output module. Not a very viable development workflow. We ended up developing the entire module inside the main app and taking out the library code when it was stable enough.
Hidden exports
I ended up with a non compiling library that was complaining about a missing 'Reselect' namespace. Reselect is a module used inside our projects and helps with building selectors. By searching through the output typing file for the library I found that reselect itself was indeed missing but it's types were used on calls returned elsewhere. I had to manually export the following:
export { Selector,
ParametricSelector,
OutputSelector,
OutputParametricSelector } from 'reselect';
So if you end up with missing namespace warnings start searching which one it is, where it's being used and what exactly is being used. Export the required types in your library code to ensure they end up in the final typings bundle.
Library initialisation
Our own code also needed a good once over to switch from the pattern of being embedded in the app code to being standalone. Some code previously done in constructors had to be moved to implicit init calls during the library setup. We also could not rely on the environment configuration from a CLI generated app. This was used to add the ngrx store-devtools which is now no longer possible. We will likely fork (or branch) the library to re-add the development tools and immutable support we dont want in production.
I think it should be possible to use wildcards in SystemJS config.
like 'rxjs/*': `${this.APP_BASE}rxjs/*`,
.
Wonder, if something like this could be done for rollup.
To fix "Rollup index include" you can use https://github.com/rollup/rollup-plugin-node-resolve
'angular/platform-browser': 'ng.platform.browser' should be: 'angular/platform-browser': 'ng.platformBrowser' taken from: Angular2\packages\platform-browser\rollup.config.js -> moduleName: 'ng.platformBrowser',
For the new HttpClient: 'angular/common/http': 'ng.commmon.http',
For Rollup warnings (externals), I use RegEx to group modules with same global variable name as below:
const globals = {
[...]
'rxjs/Observable': 'Rx',
'rxjs/ReplaySubject': 'Rx',
'rxjs/add/operator': 'Rx.Observable.prototype',
'rxjs/add/observable': 'Rx.Observable',
[...]
};
const findKey = id => Object.keys(globals).find(k => (new RegExp(k)).test(id));
export default {
[...]
globals: id => globals[findKey(id) || 'none'],
external: id => !!findKey(id),
[...]
}
Thanks, this really helpful. I wonder @Qwerios, did you have to include js-first libs like lodash, or even just a function from there? This seems to also complain about not exported members (like isEmpty, e.g.)
@axtho for lodash I did the following:
- add as peer dependency for actual use
- add as dev dependency for running demo app
- add typings for lodash for compiling
So at least the following commands:
npm i --save-peer lodash
npm i --save-dev @types/lodash lodash
Then in code you can import { isEmpty } from 'lodash'
and do your thing.
The app that will consume your library will need to install lodash as a dependency for it to work. If you embed lodash into your own library chances are 2 versions will end up in your final app which you do not want.
Hope that clears it up for you
Thanks for the write-up! I wonder if we can add this to an FAQ or otherwise off of the README.md ( @filipesilva maybe a question for you)? This is really good info! It's a fantastic starter for a lib. Similarly, we have run into many of the same gotchas/lessons learned :-)