Universal: Using this with non-typescript Angular components?

Created on 28 Feb 2016  路  18Comments  路  Source: angular/universal

I am basically trying to write something equivalent to the universal-starter repo, but without the typescript dependency. I know that I can write this .d.ts files for my non-typescript modules, but I prefer not to.

Here is a repo to reproduce the issue -- it is basically a copy of the angular-connect branch of universal-starter. I'm running Node v. 5.0.0.
https://github.com/mlent/vanilla-universal-starter

Basically, when I navigate to http://localhost:3000, the server crashes with the error:

Hash: 4cec55caa889f9ccd61c
Version: webpack 1.12.14
Time: 3318ms
    Asset     Size  Chunks             Chunk Names
client.js  5.09 MB       0  [emitted]  client
server.js  3.02 MB       1  [emitted]  server
   [0] multi server 28 bytes {1} [built]
    + 307 hidden modules
Zone already exported on window the object!
Listen on http://localhost:3000
Cannot resolve all parameters for 'ResolvedMetadataCache'(?, ?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'ResolvedMet$dataCache' is decorated with Injectable.
/opt/vanilla-universal-starter/node_modules/angular2/src/core/application_ref.js:224
    var inits = injector.getOptional(application_tokens_1.APP_INITIALIZER);
                        ^

TypeError: Cannot read property 'getOptional' of undefined
    at _runAppInitializers (/opt/vanilla-universal-starter/node_modules/angular2/src/cor$/application_ref.js:224:25)
    at PlatformRef_._initApp (/opt/vanilla-universal-starter/node_modules/angular2/src/c$re/application_ref.js:205:23)
    at PlatformRef_.application (/opt/vanilla-universal-starter/node_modules/angular2/sr$/core/application_ref.js:153:24)
    at Object.bootstrap (/opt/vanilla-universal-starter/node_modules/angular2-universal-$review/dist/server/src/platform/node.js:86:10)
    at renderToString (/opt/vanilla-universal-starter/node_modules/angular2-universal-pr$view/dist/server/src/render.js:74:19)
    at renderToStringWithPreboot (/opt/vanilla-universal-starter/node_modules/angular2-u$iversal-preview/dist/server/src/render.js:85:12)
    at /opt/vanilla-universal-starter/node_modules/angular2-universal-preview/dist/node_$odules/angular2-express-engine/src/engine.js:51:27
    at FSReqWrap.readFileAfterClose [as oncomplete] (fs.js:404:3)

I do see that the client code is able to take over and my component does function.

But, the server crashes and obviously the server-side part of the rendering doesn't happen, and I can see the component in a pre-rendered state.

What appears to be happening

  1. App is trying to initialize providers
  2. It should be getting an injector back
  3. The code to initalize providers uses a utility function that depends on Array.from
  4. Node does not support Array.from, or it's wrongly polyfilled or something, because injector ends up undefined.
  5. Calling function on undefined injector causes crash

1. App is trying to init providers
src/core/application_ref.ts

zone.run(() => {
      providers = ListWrapper.concat(providers, [
        provide(NgZone, {useValue: zone}),
        provide(ApplicationRef, {useFactory: (): ApplicationRef => app, deps: []})
      ]);

      var exceptionHandler;
      try {
        injector = this.injector.resolveAndCreateChild(providers);
        /*
         * Injector already is undefined here, even though the node stack trace indicates a later line?
        */
        exceptionHandler = injector.get(ExceptionHandler);
        zone.overrideOnErrorHandler((e, s) => exceptionHandler.call(e, s));
      } catch (e) {
        if (isPresent(exceptionHandler)) {
          exceptionHandler.call(e, e.stack);
        } else {
          print(e.toString());
        }
      }
    });

src/core/di/injector.ts

  resolveAndCreateChild(providers: Array<Type | Provider | any[]>): Injector {
    var resolvedProviders = Injector.resolve(providers); // <-----
    return this.createChildFromResolved(resolvedProviders);
  }
  // ...
createChildFromResolved(providers: ResolvedProvider[]): Injector {  // <-----
    var bd = providers.map(b => new ProviderWithVisibility(b, Visibility.Public));
    var proto = new ProtoInjector(bd);
    var inj = new Injector(proto);
    inj._parent = this;
    return inj;
  }

src/core/di/injector.ts

static resolve(providers: Array<Type | Provider | any[]>): ResolvedProvider[] { // <------
    return resolveProviders(providers);   // <-----
  }

export function resolveProviders(providers: Array<Type | Provider | any[]>): ResolvedProvider[] {  // <-----
  var normalized = _normalizeProviders(providers, []);
  var resolved = normalized.map(resolveProvider);
  /*
   * Both normalized and resolved appear to be defined at this point.
   */
  return MapWrapper.values(mergeResolvedProviders(resolved, new Map<number, ResolvedProvider>())); 
}

src/facade/collection.ts

// Safari doesn't implement MapIterator.next(), which is used is Traceur's polyfill of Array.from
// TODO(mlaval): remove the work around once we have a working polyfill of Array.from
var _arrayFromMap: {(m: Map<any, any>, getValues: boolean): any[]} = (function() {
  try {
    if ((<any>(new Map()).values()).next) {
      return function createArrayFromMap(m: Map<any, any>, getValues: boolean): any[] {
        return getValues ? (<any>Array).from(m.values()) : (<any>Array).from(m.keys());
      };
    }
  } catch (e) {
  }
  return function createArrayFromMapWithForeach(m: Map<any, any>, getValues: boolean): any[] {
    var res = ListWrapper.createFixedSize(m.size), i = 0;
    m.forEach((v, k) => {
      res[i] = getValues ? v : k;
      i++;
    });
    return res;
  };
})();

Ultimately, what is returned from this function causes node not to have a proper injector and it explodes.

My understanding is that Node doesn't currently support Array.from, but somehow the universal-starter doesn't encounter this problem. I tried to run my script under ts-node but it has the exact same issue. So I'm very curious what the key difference is in the typescript version that dodges this issue.

Funny thing is that on another computer (both are Mac OS), with the same repo and running the same version of node, the server does _not_ crash, but the server-side rendering is also not working. But I am able to reload the page multiple times without disturbing the server.

In sum, my questions are:

  1. Is what I'm trying to do even possible, or are angular2 and universal so tightly coupled and dependent on typescript?
  2. Any ideas on why the typescript version is working, but a plain es6/node version is not?

Thanks a ton in advance for any insight, I appreciate the work you're doing!

Most helpful comment

Heads-up: I got the same error after a code refactor.
It's notable I'm also using ng2 with ES6, though I'm not using Universal, so yeah, this does seem to relate more to ng2 injection than to Universal.
Will try to investigate more on my part as well; before injection all the injectables appear properly resolved though.

Edit: yep, just comment import 'reflect-metadata';.

Does it sound right to conclude that there must be something in the reflect-metadata that works perfectly in the browser, but fails in Node?
It seems that nope, it can fail in the browser just as hard.

All 18 comments

Hi @mlent and thank you for using universal ;)

From what I see here is that the injector is undefined because of this error (see message from your console):

Cannot resolve all parameters for 'ResolvedMetadataCache'(?, ?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'ResolvedMet$dataCache' is decorated with Injectable.

So may be this has something to do with babel-plugin-angular2-annotations or the other babel-plugin-transform-* transformers.

I'll investigate this and I let you know...

After a quick investigation, it seems that this issue is caused by the reflect-metadata polyfill.
To verify this, you can comment this line https://github.com/mlent/vanilla-universal-starter/blob/master/client/universal-app.js#L1.

@gdi2290 any idea?

Ah, so the error message was accurate. Meaning...annotation/metadata support isn't being properly polyfilled, causing the annotations within Angular itself to fail -- but only if you're trying to run it without typescript and on the server (perhaps ts-node handles the annotations properly while reflect-metadata does not do so sufficiently for regular node).

Does it sound right to conclude that there must be something in the reflect-metadata that works perfectly in the browser, but fails in Node?

@mlent, I don't have a ton of experience with Babel, but if it works in the browser, it should work in node since it is translating the same thing. @jeffbcross have you heard of anyone trying to do this in the team that we can ping?

@mlent I removed the polyfills can you try again with the updated universal-starter

@gdi2290 Thanks! I'll look at it this weekend. But since my project isn't really _directly_ from the universal-starter, do I just need to use the latest universal code? Or is there a specific change in the universal-starter I should apply to my ES6 version?

@mlent you don't have to use the starter, but do your dependencies match up with the latest from the starter?

https://github.com/angular/universal-starter/blob/master/package.json

Btw, I would love to see ES6 code for this. Everything I have been working with has been TypeScript. Is your project public?

@jeffwhelpley I will update all my dependencies to match the universal starter. I had a hard time getting all the dependencies and peer dependencies to work to even run the thing originally. Might be helpful to forego the ^ notation in the package.json until these dependencies are a bit more stable.

But yes, the code for kind of an ES6 starter is here:
https://github.com/mlent/vanilla-universal-starter

Not really a 'project' perhaps but I tried to apply it to a pre-existing Angular 1.5 codebase and had an impossible time getting it to run, so I figured I'd start simpler :smile:

@mlent I looked at your repo but it seems you've abandoned the no TypeScript approach? I've gotten a little farther, the server side seems to render fine for me, but the client app crashes; which probably has nothing to do with universal. One of the provider Tokens is undefined during resolveProviders.

Code is here https://github.com/sullivanpt/ng2-universal-seed

@sullivanpt Nope, I just tried to run my code under ts-node to see if something about this was handling the error, but running the same code on ts-node and plain node result in crashes as soon as the server tries to render. Was also thinking that this might just be a problem in Angular itself. So I guess you have the same problem as me, then?

@mlent I got server and client side rendering working (yay!) using SystemJS. Check out the systemjs branch in my repo. Caveat is I had to disable preboot; for reasons I haven't worked out yet client side angular never came to life when preboot was enabled.

@gdi2290 for SystemJS we're going to need some pre-bundling of angular2-universal/dist/. I'm not sure if it is too early to start looking at putting this in the build pipeline; I'm thinking probably yes?

ya, we can create a systemjs bundle

@gdi2290 That should just be at the angular-universal-preview level, right? I don't think I need to modify the preboot package for this, but let me know if you think differently.

@mlent I have it all working now, I think you can close this ticket. The master branch of my ng2-univeral-seed repo demonstrates vanilla JS using webpack/babel; whereas the systemjs branch demonstrates vanilla JS with no bundling or babel transpiling.

I collected some thoughts about the difficulties I encountered in the README.md and in TODO statements in the code. Briefly:

  • angular2 seems too tightly coupled to some dependencies, even minor updates can break things.
  • the version of es6-shim that angular2 beta 9 & 10 is pinned to is broken on Chrome.
  • prebootComplete MUST be called if preboot is enabled in the server render or the client app will not start (obvious in retrospect but I lost a few hours learning this).
  • es6-module-loader debugging is NOT worth it; stick to require until node supports import natively (really)

Heads-up: I got the same error after a code refactor.
It's notable I'm also using ng2 with ES6, though I'm not using Universal, so yeah, this does seem to relate more to ng2 injection than to Universal.
Will try to investigate more on my part as well; before injection all the injectables appear properly resolved though.

Edit: yep, just comment import 'reflect-metadata';.

Does it sound right to conclude that there must be something in the reflect-metadata that works perfectly in the browser, but fails in Node?
It seems that nope, it can fail in the browser just as hard.

@tycho01 Agreed. This thread fixed my problem, but it does not seem to be related to Universal or Babel.

This is my package.json

{
  "devDependencies": {
    "css-loader": "^0.23.1",
    "html-loader": "^0.4.3",
    "jasmine-core": "^2.4.1",
    "karma": "^0.13.21",
    "karma-chrome-launcher": "^0.2.2",
    "karma-firefox-launcher": "^0.1.7",
    "karma-jasmine": "^0.3.7",
    "karma-phantomjs-launcher": "^1.0.0",
    "karma-webpack": "^1.7.0",
    "lite-server": "^2.1.0",
    "node-sass": "^3.4.2",
    "phantomjs-prebuilt": "^2.1.4",
    "sass-loader": "^3.1.2",
    "style-loader": "^0.13.0",
    "ts-loader": "^0.8.1",
    "typescript": "^1.8.2",
    "webpack": "^1.12.13",
    "webpack-dev-server": "^1.14.1"
  },
  "dependencies": {
    "@ngrx/store": "^1.2.1",
    "angular2": "^2.0.0-beta.12",
    "es6-promise": "^3.0.2",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.2",
    "zone.js": "0.6.6"
  }
}

The app worked perfectly in the browser, but the test failed with:

    Failed: Cannot resolve all parameters for 'ResolvedMetadataCache'(?, ?). Make sure that all the parameters are decorated with Inject or have valid type annotations and that 'ResolvedMetadataCache' is decorated with Injectable.
    BaseException@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:14985:24
    NoAnnotationError@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:15892:10
    _dependenciesFor@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:14908:1
    resolveFactory@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:14802:25
    resolveProvider@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:14826:67
    Call@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:389:15
    map@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:1385:15
    resolveProviders@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:14834:21
    Injector</Injector.resolve@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:13560:17
    Injector</Injector.prototype.resolveAndCreateChild@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:13745:34
    TestInjector</TestInjector.prototype.createInjector@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:10937:27
    TestInjector</TestInjector.prototype.execute@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:10947:14
    _it/<@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:10301:38
    _it/<@/Users/charlie/Code/proveit/src/people/components/people_list.spec.ts:10304:18

The fix was to remove import 'angular2/bundles/angular2-polyfills'; from the top of my component spec.

import 'es6-shim';
-import 'angular2/bundles/angular2-polyfills';

import {
    describe,
    beforeEach,
    it,
    expect,
    injectAsync,
    inject,
    TestComponentBuilder,
    ComponentFixture,
    setBaseTestProviders} from 'angular2/testing';
import {
    TEST_BROWSER_PLATFORM_PROVIDERS,
    TEST_BROWSER_APPLICATION_PROVIDERS
} from 'angular2/platform/testing/browser';

setBaseTestProviders(TEST_BROWSER_PLATFORM_PROVIDERS, TEST_BROWSER_APPLICATION_PROVIDERS);

import {PersonForm} from "./person_form";
import {Person} from "../models/person";

describe('PersonForm', () => {
  //  Normal testing stuff
});

Instantiation of my PersonForm child component no longer throws the dependency errors with angular2/bundles/angular2-polyfills removed.

@gdi2290 @manekinekko Do you know who might be best to tag here so the team involved with this code could have a look at the issue?

Closing this as issue being tracked on JSPM repo.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings