Search code examples
angularfirebasegoogle-cloud-functionsangular-universalangular-google-maps

Angular Universal does not work with Angular Google Maps


Tech: Angular Cli, Angular version 7, Angular Google Maps, Firebase Functions.

Issue: I'm trying to serve my angular universal app but getting an error for angular google maps when building.

Error I get:

/Users/test/Public/Leisure/leisure-app/functions/node_modules/@agm/core/services/managers/info-window-manager.js:1
(function (exports, require, module, __filename, __dirname) { import {
Observable } from 'rxjs';
                                                              ^^^^^^

SyntaxError: Unexpected token import
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)

Seems that Angular Universal doesnt like third party libraries.

The resource I followed: https://hackernoon.com/deploying-angular-universal-v6-with-firebase-c86381ddd445

My App Module:

import { AgmCoreModule } from '@agm/core';
import { AngularFireAuthModule } from '@angular/fire/auth';
import { AngularFireDatabase } from '@angular/fire/database';
import { AngularFireModule } from '@angular/fire';
import { ServiceWorkerModule } from '@angular/service-worker';
import { AngularFireStorageModule } from '@angular/fire/storage';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { GooglePlaceModule } from 'ngx-google-places-autocomplete';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule, NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { NgxErrorsModule } from '@hackages/ngxerrors';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { TruncateModule } from 'ng2-truncate';
import { MetaModule } from '@ngx-meta/core';
import { PreventDoubleSubmitModule } from 'ngx-prevent-double-submission';

// Core App
import { AppComponent } from './app.component';
import { CoreInterceptor } from './interceptor';
import { environment } from '../environments/environment';

// Modules
import { SharedModule } from './shared/shared.module';
import { CoreModule } from './services/core.module';
import { LayoutModule } from './layouts/layout.module';

// Sections
import { COMPONENTS } from './components/index';
import { ROUTES } from './app.routes';

// Guards
import { AuthGuard } from './guards/auth.guard';
import { CreditGuard } from './guards/credit.guard';

@NgModule({
  declarations: [
    AppComponent,
    COMPONENTS
  ],
  imports: [
    CommonModule,
    FormsModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAuthModule,
    AngularFirestoreModule,
    AngularFireStorageModule,

    // This is the Angular google maps module causing issue on build
    AgmCoreModule.forRoot({
      apiKey: environment.googleApiKey,
      libraries: ['places']
    }),

    GooglePlaceModule,
    LayoutModule,
    BrowserModule.withServerTransition({ appId: 'test123' }),
    PreventDoubleSubmitModule.forRoot(),
    TruncateModule,
    MetaModule.forRoot(),
    HttpClientModule,
    NgxErrorsModule,
    ReactiveFormsModule,
    RouterModule.forRoot(ROUTES),
    CoreModule,
    SharedModule,
    ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production })
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true },
    AuthGuard,
    CreditGuard,
    AngularFireDatabase
  ],
  exports: [ RouterModule ],
  schemas: [ NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Command I run: serve: npm run build && firebase serve --only functions

Latest Error (for npm run serve:ssr):

enter image description here

Error code from serve:ssr:

e.Lb=function(a,b,c){a=this.ka.J[String(a)];if(!a)return!0;a=a.concat();for(var d=!0,f=0;f<a.length;++f){var g=a[f];if(g&&!g.Sa&&g.capture==b){var k=g.listener,p=g.Ob||g.src;g.Eb&&this.Le(g);d=!1!==k.call(p,c)&&d;}}return d&&0!=c.Be};e.jb=function(a,b,c,d){return this.ka.jb(String(a),b,c,d)};var ub=h.JSON.stringify;function vb(a,b){this.Sf=100;this.ef=a;this.ug=b;this.Zb=0;this.Pb=null;}vb.prototype.get=function(){if(0<this.Zb){this.Zb--;var a=this.Pb;this.Pb=a.next;a.next=null;}else a=this.ef();return a};vb.prototype.put=function(a){this.ug(a);this.Zb<this.Sf&&(this.Zb++, a.next=this.Pb, this.Pb=a);};function I(){this.lc=this.Va=null;}var xb=new vb(function(){return new wb},function(a){a.reset();});I.prototype.add=function(a,b){var c=this.Af();c.set(a,b);this.lc?this.lc.next=c:this.Va=c;this.lc=c;};I.prototype.remove=function(){var a=null;this.Va&&(a=this.Va, this.Va=this.Va.next, this.Va||(this.lc=null), a.next=null);return a};I.prototype.wg=function(a){xb.put(a);};I.prototype.Af=function(){return xb.get()};function wb(){this.next=this.scope=this.Gc=null;}
wb.prototype.set=function(a,b){this.Gc=a;this.scope=b;this.next=null;};wb.prototype.reset=function(){this.next=this.scope=this.Gc=null;};function yb(a){h.setTimeout(function(){throw a;},0);}var zb;

Solution

  • TL;DR:

    Source code and DEMO

    enter image description here


    The issue here is that @agm/core package is compiled with es2015 module. As a result, it contains import and export in js code.

    To remedy this you have two main options:

    1. Compile @agm/core package to commonjs format.

    You can use either babel or typescript to compile that package. Then you need to make sure you provided compiled version in your functions dependencies

    functions/package.json

    "dependencies": {
      ...
      "@agm/core": "file:./@agm/core"
    },
    

    Here I use local dependency but you can also use your own published version.

    Another method would be compile directly in node_modules and publish the whole node_modules(I would avoid this):

    firebase.json

    {
      "functions": {
        "ignore": []
      }
    }
    

    How to compile?

    Babel

    Install dependencies in root directory.

    npm i -D @babel/cli @babel/core @babel/preset-env
    

    Use the following script to compile:

    package.json

    "postbuild": "babel node_modules/@agm/core -d functions/@agm/core --presets @babel/preset-env && node ./copy-package-json.js"
    

    where

    copy-package-json.js

    const fs = require('fs-extra');
    const { join } = require('path');
    
    (async() => {
      await fs.copy(join(process.cwd(), 'node_modules/@agm/core/package.json'),
                    join(process.cwd(), 'functions/@agm/core/package.json'));
    })();
    

    Typescript

    tsconfig.agm.json

    {
      "extends": "./tsconfig.json",
      "compilerOptions": {
        "outDir": "./functions/@agm/core",
        "types": [],
        "module": "commonjs"
      },
      "include": [
        "node_modules/@agm/core"
      ]
    }
    

    package.json

    "postbuild": "tsc -p tsconfig.agm.json --allowJs && node ./copy-package-json.js"
    

    2. Generate server bundle

    This is what Angular universal tutorial uses so I prefer this solution.

    Also follow this quide

    Simple steps:


    1. Install global dependencies

    I have installed:

    2. Create a new Angular project

    ng new angular-agm
    

    3. Add Angular universal

    ng add @nguniversal/express-engine --clientProject angular-agm
    

    4. Update server.ts

    Export the express app, then remove the call to listen and change index to index2.

    import 'zone.js/dist/zone-node';
    import {enableProdMode} from '@angular/core';
    // Express Engine
    import {ngExpressEngine} from '@nguniversal/express-engine';
    // Import module map for lazy loading
    import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
    
    import * as express from 'express';
    import {join} from 'path';
    import * as path from 'path';
    
    // Faster server renders w/ Prod mode (dev mode never needed)
    enableProdMode();
    
    // Express server
    export const app = express();
    
    // const PORT = process.env.PORT || 4000;
    const DIST_FOLDER = join(process.cwd(), 'dist/browser');
    
    const index = require('fs')
      .readFileSync(path.resolve(DIST_FOLDER, 'index2.html'), 'utf8')
      .toString();
    
    const domino = require('domino');
    const win = domino.createWindow(index);
    global['window'] = win;
    global['document'] = win.document;
    
    // * NOTE :: leave this as require() since this file is built Dynamically from webpack
    const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');
    
    // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
    app.engine('html', ngExpressEngine({
      bootstrap: AppServerModuleNgFactory,
      providers: [
        provideModuleMap(LAZY_MODULE_MAP)
      ]
    }));
    
    app.set('view engine', 'html');
    app.set('views', DIST_FOLDER);
    
    // Example Express Rest API endpoints
    // app.get('/api/**', (req, res) => { });
    // Serve static files from /browser
    app.get('*.*', express.static(DIST_FOLDER, {
      maxAge: '1y'
    }));
    
    // All regular routes use the Universal engine
    app.get('*', (req, res) => {
      res.render('index2', { req });
    });
    
    // Start up the Node server
    /*app.listen(PORT, () => {
      console.log(`Node Express server listening on http://localhost:${PORT}`);
    });*/
    

    5. Build

    npm run build:ssr
    

    where build:ssr

    "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server && node ./tools/copy-artifacts.js",
    

    copy-artifacts.js

    const fs = require('fs-extra');
    const { join } = require('path');
    
    (async() => {
      const src = join(process.cwd(), 'dist');
      const copy = join(process.cwd(), 'functions/dist');
    
      await fs.rename(join(src, 'browser/index.html'), join(src, 'browser/index2.html'));
      await fs.remove(copy);
      await fs.copy(src, copy);
    })();
    

    6. Update functions/index.js to use built version of express app

    const functions = require('firebase-functions');
    
    const { app } = require('./dist/server');
    
    exports.ssr = functions.https.onRequest(app);
    

    7. Configure firebase.json

    {
      "hosting": {
        "public": "dist/browser",
        "ignore": [
          "firebase.json",
          "**/.*",
          "**/node_modules/**"
        ],
        "rewrites": [
          {
            "source": "**",
            "function": "ssr"
          }
        ]
      }
    }
    

    The source code can be found on Github

    See also demo https://angular-agm.firebaseapp.com/