Search code examples
angularwebpackecmascript-6prerender

Prerender es6 Errors with Angular 2/Typescript/Webpack


I'm trying to use prerender-node and a private prerender server to prerender an Angular2/Express app. If I try to target es5 in my tsconfig.json, the prerender server throws this error:

ReferenceError: Can't find variable: Map

undefined:1521 in KeyRegistry
:1540
:7

If I try to target es6 (including node_modules/typescript/lib/lib.es6.d.ts in the files array), the prerender server throws this error:

SyntaxError: Unexpected token 'const'

http://localhost:3000/app.js:50 in eval
http://localhost:3000/app.js:50
http://localhost:3000/app.js:20 in __webpack_require__
http://localhost:3000/app.js:40

I'm guessing I need to include some sort of polyfill in order for this to work, but I don't have the first idea what to include or where to include it.

Here's my webpack config in case that helps:

var webpack = require('webpack');
var path = require('path');
var rootDir = path.resolve();

module.exports =
{
    target: 'node',
    entry: rootDir + "/dist/client/app/bootstrap.js",
    output: {
        path: rootDir + "/dist/client", publicPath: '/', filename: "app.js",
        pathinfo: true
    },
    devtool: 'eval',
    resolve: {
        root: rootDir + "/dist/client/app",
        extensions: ['', '.js']
    },
    module: {
        loaders: [
            {
                test: /\.ts/, loaders: ['ts-loader'], exclude: /node_modules/
            }
        ]
    }
};

And my client tsconfig in case that helps:

{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "moduleResolution": "node",
        "sourceMap": false,
        "outDir": "../../dist/client",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": true
    },
    "files": ["app/bootstrap.ts", "app/vendor.ts", "app/Window.ts", "tests/index.ts", "../../node_modules/typescript/lib/lib.es6.d.ts"]
}

Update

If I change my webpack config to target web instead of node, and comment out server.use(prerender.removeScriptTags()); in the prerender server, the request hits the prerender server every time and no errors are thrown, but nothing is prerendered, either. Seems closer than before, though, so I thought I'd update.

Update

Prerender doesn't seem to execute any Angular code. If I set window.prerenderReady = false in a script tag in the head in my index.html, and then try to set it to true again when my root component is instantiated, the prerender server times out:

import { Component, OnInit } from '@angular/core'

@Component({
    selector: 'my-app',
    template: `
        <div id="main"> all the things </div>
    `
})
export class AppComponent implements OnInit
{
    constructor(){}

    ngOnInit()
    {
        // Prerender never executes this code before it times out
        window.prerenderReady = true;
        console.info('Prerender Ready');
    }
}

Solution

  • I managed to get this working with the following changes:

    Use target: 'web' in Webpack config.

    Use target: 'es5' and add node_modules/typescript/lib/lib.es6.d.ts to files array in tsconfig.json. This prevents compile-time errors on es6 stuff.

    Apparently, despite targeting web and es5, Webpack still leaves es6 styntax around that PhantomJs can't deal with. I don't love the idea of including babel in a Typescript project, but this is the only thing I've found that works: require('babel-polyfill'); BEFORE any application code or vendor code that may use any es6 syntax.

    Set window.prerenderReady = false; in the head of the document and then set it to true when you instantiate the root application component:

    export class ApplicationComponent implements OnInit
    {
        ngOnInit()
        {
            window.prerenderReady = true;
        }
    }
    

    NOTE: for the above to work in Typescript you'll need to make sure the property prerenderReady exists on type Window. E.g.:

    interface Window
    {
        prerenderReady:boolean;
    }
    

    On the server side, make sure you configure prerender-node BEFORE any routes are configured. E.g.:

    export function routeConfig(app:express.Application):void
    {
        // Prerender
        app.use(require('prerender-node').set('prerenderServiceUrl', CONFIG.prerenderServiceUrl));
    
        // Angular routes should return index.html
        app.route(CONFIG.angularRoutes)
            .get((req:Request, res:Response, next:NextFunction) =>
            {
                return res.sendFile(`${app.get('appPath')}/client/index.html`);
            });
    }
    

    Hope this helps someone :)