1

Problem

When I am trying to render an Angular module with PlatformRef::bootstrapModule(...) I get an error:

ReferenceError: document is not defined

Obviously, Angular is attempting to use document in Node.js environment where it's not present (since it's not a browser environment).

Question

How do I work around the issue?

Code

Core Code

@Injectable()
export class MyDomRendererFactory2 implements RendererFactory2 {
  private renderer: MyRenderer;
  constructor(eventManager: EventManager) {
    this.renderer = new MyRenderer(eventManager);
  }
  createRenderer(hostElement: any, type: RendererType2): Renderer2 {
    return this.renderer;
  }
  begin?(): void { throw new Error("Method not implemented."); }
  end?(): void { throw new Error("Method not implemented."); }
  whenRenderingDone?(): Promise<any> { throw new Error("Method not implemented."); }
}

async function generate<M>(moduleType: Type<M>) {
  try {
    const extraProviders: StaticProvider[] = [
      { provide: Compiler, useFactory: compilerFactory => compilerFactory.createCompiler(), deps: [CompilerFactory] },
    ];
    const platformRef = platformDynamicServer(extraProviders);
    const moduleRef = await platformRef
      .bootstrapModule(
        moduleType,
        {
          providers: [
            { provide: ResourceLoader, useValue: new ResourceLoaderImpl(`src\\app`), deps: [ ] },

            {
              provide: RendererFactory2,
              useClass: MyDomRendererFactory2,
              deps: [EventManager],
            },
          ],
        });

    const appComponent = moduleRef.injector.get(AppComponent);

    console.info(appComponent.title.toString());
  } catch (error) {
    throw new Error(error.toString());
  }
}

Custom Renderer

At this point, it does nothing, basically.

@Injectable()
export class MyRenderer implements Renderer2 {
  data: { [key: string]: any; };
  destroy(): void { }
  createElement(name: string, namespace?: string) { }
  createComment(value: string) { }
  createText(value: string) { }
  destroyNode: (node: any) => void;
  appendChild(parent: any, newChild: any): void { }
  insertBefore(parent: any, newChild: any, refChild: any): void { }
  removeChild(parent: any, oldChild: any): void { }
  selectRootElement(selectorOrNode: any) { }
  parentNode(node: any) { }
  nextSibling(node: any) { }
  setAttribute(el: any, name: string, value: string, namespace?: string): void { }
  removeAttribute(el: any, name: string, namespace?: string): void { }
  addClass(el: any, name: string): void { }
  removeClass(el: any, name: string): void { }
  setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void { }
  removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void { }
  setProperty(el: any, name: string, value: any): void { }
  setValue(node: any, value: string): void { }
  listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void { return null; }
  constructor(private eventManager: EventManager) {}
}

Custom Resource loader

Guesses the source file by "url" (which is not a URL actually) and reads all the text of the file.

@Injectable()
export class ResourceLoaderImpl extends ResourceLoader {
  constructor(private _cwd: string) {
    super();
  }

  get(resourceFileName: string): Promise<string> {
    return new Promise<string>((resolve, reject) => {
      glob(`**/${resourceFileName}`, { cwd: this._cwd, }, (globError, matches) => {
        if (globError) reject(globError.toString());
        else
          readFile(path.join(this._cwd, matches[0]), (readFileError, data) => {
            if (readFileError) reject(readFileError);
            else resolve(data.toString());
          });
      });
    });
  }
}

Imports

import { ResourceLoader } from '@angular/compiler';
import { Compiler, CompilerFactory, Injectable, Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2, StaticProvider, Type } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { platformDynamicServer } from '@angular/platform-server';
import { readFile } from 'fs';
import * as glob from 'glob';
import * as path from 'path';
import 'zone.js';
import 'zone.js/dist/long-stack-trace-zone.js';
import { AppComponent } from './app/app.component';
import { AppModule } from './app/app.module';

UPDATE 1

I also tried to provide a custom document object like this:

import { JSDOM } from 'jsdom';

const jsdom = new JSDOM(`<html></html>`);
export function document() { return jsdom.window.document; }

by means of registering it with the DI:

{ provide: DOCUMENT, useFactory: document, deps: [] }

Still getting the same ReferenceError: document is not defined error.

UPDATE 2

The root of my problem is that RendererFactory2 does not get resolved to MyDomRendererFactory2. Instead, a standard NgModule's DomRendererFactory2 is being used... This is what I observe in providers during debugging:

enter image description here

Igor Soloydenko
  • 11,067
  • 11
  • 47
  • 90

1 Answers1

0

If you are using server-side than you must have to add check whether the platform is browser or server because keywords like Document, Window are not available in Server side support

you can use isPlatformBrowser from angular. https://angular.io/api/common/isPlatformBrowser

mruanova
  • 6,351
  • 6
  • 37
  • 55
  • Where do I add such check?? Notice my code does not use any of those "keywords" directly. Do you have a code reference? – Igor Soloydenko Aug 30 '18 at 19:37
  • see here https://stackoverflow.com/questions/43812124/angular-isplatformbrowser-checking-against-platform-id-doesnt-prevent-server-si – mruanova Aug 30 '18 at 22:50
  • I am sorry, but I am not following. Without a clear code example the answer and the links are not very helpful. :( In my case I am not going to make any changes to the target module (`AppModule`). I must repair the code shown in the question rather. – Igor Soloydenko Aug 30 '18 at 22:58