Search code examples
angularspring-bootwebpackfreemarkerhtml-webpack-plugin

How to use angular bundles in index.ftl (freemarker template)


I am working on multi-module Gradle project having below structure

    parent-app
     - backend
        - src/main
            - java
            - resources
            - webapp
        - build.gradle
     - angular-ui
        - src
            - app
            - envirnoments
            - index.html
            - index.ftl
            - main.ts
        - angular.json
        - package.json
        - webpack.config.js
        - build.gradle
     - build.gradle
     - settings.gradle

I am using index.ftl (freemarker templates) as my view which uses some macros from in-house library (gradle dependencies) to get headers. But for everything else, I have to use angular components/pages.

I was trying with webpack to dynamically add angular bundles (main.js, polyfill.js etc) in index.ftl file.

The configuration throws minify error at angular build (ng build --prod) but I see js files are added in index.ftl as scripts.

Can anyone please help understand the issue and how to resolve it so that my angular bundle are fully loaded in index.ftl file without any errors.

Below is the error

Html Webpack Plugin:
<pre>
  Error: html-webpack-plugin could not minify the generated output.
  In production mode the html minifcation is enabled by default.
  If you are not generating a valid html output please disable it manually.
  You can do so by adding the following setting to your HtmlWebpackPlugin config:
  |
  |    minify: false
  |
  See https://github.com/jantimon/html-webpack-plugin#options for details.
  For parser dedicated bugs please create an issue here:
  https://danielruf.github.io/html-minifier-terser/
  Parse Error: <#macro main> 
      <app-root></app-root> 
  <#macro> 
  <#maro pagebody> 
  <#macro><script src="angular-ui/runtime.689a94f98876eea3f04c.js"></script><script src="angular-ui/polyfills.94daefd414b8355106ab.js"></script><script src="angular-ui/main.95a9937db670e12d53ac.js"></script>
  
  - htmlparser.js:244 new HTMLParser
    [angular-webpack]/[html-minifier-terser]/src/htmlparser.js:244:13
  
  - htmlminifier.js:993 minify
    [angular-webpack]/[html-minifier-terser]/src/htmlminifier.js:993:3
  
  - htmlminifier.js:1354 Object.exports.minify
    [angular-webpack]/[html-minifier-terser]/src/htmlminifier.js:1354:16
  
  - index.js:1019 HtmlWebpackPlugin.minifyHtml
    [angular-webpack]/[html-webpack-plugin]/index.js:1019:46
  
  - index.js:435 HtmlWebpackPlugin.postProcessHtml
    [angular-webpack]/[html-webpack-plugin]/index.js:435:40
  
  - index.js:260 
    [angular-webpack]/[html-webpack-plugin]/index.js:260:25
  
  - task_queues:96 processTicksAndRejections
    node:internal/process/task_queues:96:5
  
</pre>

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const fs = require('fs');
const path = require('path');

module.exports = {
  output: {
    "publicPath": "angular-ui/"
  },

  plugins: [
    new HtmlWebpackPlugin({
        "template": "./src/index.ftl",
        "filename": "../backend/src/main/webapp/WEB-INF/templates/index.ftl",
        "inject": false,
        "hash": true,
        "xhtml": true,
    }),
    {
      apply: (compile) => {
        compile.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => {
          fs.unlink(path.join(process.cwd(), "../backend/src/main/webapp/angular-ui/index.html"), (error) => {
            if (error) throw error;
          });
        });
      }
    }
  ]
}

index.ftl

<#macro main>
    <app-root></app-root>
<#macro>
<#maro pagebody>
<% for (key in htmlWebpackPlugin.files.chunks) { %>
    <% if (htmlWebpackPlugin.files.chunks[key].entry) { %>
        <script src="<@spring.url '/<%= htmlWebpackPlugin.files.chunks[key].entry %>'/>" type="text/javascript"></script>
    <% } %>
<% } %>
<#macro>
<@header>

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularUI</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

package.json

{
  "name": "angular-ui",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --open",
    "build": "ng build --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~11.2.14",
    "@angular/common": "~11.2.14",
    "@angular/compiler": "~11.2.14",
    "@angular/core": "~11.2.14",
    "@angular/forms": "~11.2.14",
    "@angular/platform-browser": "~11.2.14",
    "@angular/platform-browser-dynamic": "~11.2.14",
    "@angular/router": "~11.2.14",
    "rxjs": "~6.6.0",
    "tslib": "^2.0.0",
    "zone.js": "~0.11.3"
  },
  "devDependencies": {
    "@angular-builders/custom-webpack": "^11.1.1",
    "@angular-devkit/build-angular": "~0.1102.17",
    "@angular/cli": "~11.2.17",
    "@angular/compiler-cli": "~11.2.14",
    "@types/jasmine": "~3.6.0",
    "@types/node": "^12.11.1",
    "codelyzer": "^6.0.0",
    "html-webpack-plugin": "^4.5.2",
    "jasmine-core": "~3.6.0",
    "jasmine-spec-reporter": "~5.0.0",
    "karma": "~6.1.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.0.3",
    "karma-jasmine": "~4.0.0",
    "karma-jasmine-html-reporter": "~1.5.0",
    "protractor": "~7.0.0",
    "ts-node": "~8.3.0",
    "tslint": "~6.1.0",
    "typescript": "~4.1.5"
  }
}

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-ui": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:application": {
          "strict": true
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "./webpack.config.js"
            },
            "outputPath": "../backend/src/main/webapp/angular-ui",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "500kb",
                  "maximumError": "1mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "angular-ui:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "angular-ui:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "angular-ui:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "tsconfig.app.json",
              "tsconfig.spec.json",
              "e2e/tsconfig.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        },
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "angular-ui:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "angular-ui:serve:production"
            }
          }
        }
      }
    }
  },
  "defaultProject": "angular-ui"
}


Solution

  • The problem is the HtmlWebpackPlugin doesn't know how to correctly parse .ftl files. By default the plugin will use an ejs-loader. See https://github.com/jantimon/html-webpack-plugin/blob/main/docs/template-option.md

    Do you need to minify the index.ftl file? I'd argue that you don't. It's not necessary especially when you can just compress it before sending it from the server. You should be able to pass the config property minify with the value of false into the HtmlWebpackPlugin to prevent the minification error.

    i.e.

    new HtmlWebpackPlugin({
      "template": "./src/index.ftl",
      "filename": "../backend/src/main/webapp/WEB-INF/templates/index.ftl",
      "inject": false,
      "hash": true,
      "xhtml": true,
      "minify": false // <-- property to add
    }),
    

    Adding the minify: false entry to the HtmlWebpackPlugin options should fix your immediate error.

    However, I also noticed the index.ftl has a syntax error where you are trying to set the src attribute. There is an extra '/> before closing the src attribute. Specifically, you'll need to modify this line:

    <script src="<@spring.url '/<%= htmlWebpackPlugin.files.chunks[key].entry %>'/>" type="text/javascript"></script>
    

    to be:

    <script src="<@spring.url '/<%= htmlWebpackPlugin.files.chunks[key].entry %>" type="text/javascript"></script>
    

    Additionally, when testing locally, in order for my js files to be written to the file, I needed to change your line htmlWebpackPlugin.files.chunks to htmlWebpackPlugin.files.js because I'm not doing any chunking. You may need to do the same.