Angular-builders: Use Angular to build (and live reload) chrome extension

Created on 12 Jun 2019  路  12Comments  路  Source: just-jeb/angular-builders

Describe the bug
I try to use angular to build chrome extension. To run, extension must have a background script.
I place a background.ts file in the src folder, and add custom build to angular.json serve command:

        "serve": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "./custom-webpack.config.js",
              "replaceDuplicatePlugins": true,
              "mergeStrategies": {
                "externals": "prepend"
              }
            },
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": ["src/favicon.ico", "src/assets", "src/manifest.json"],
            "styles": ["src/styles.styl"],
            "scripts": []
          },

the custom-webpack.config.js file with the new background "entry" to enhance webpack build:

const ExtensionReloader = require('webpack-extension-reloader')

module.exports = {
  watch: true,
  entry: {
    background: './src/background/index.ts'
  },
  output: {
    filename: '[name].bundle.js'
  },
  plugins: [new ExtensionReloader({ reloadPage: true })]
}

the result seems to be good:

chunk {background} background.bundle.js, background.bundle.js.map (background) 41.2 kB [initial] [rendered]
chunk {main} main.bundle.js, main.bundle.js.map (main) 29.9 kB [initial] [rendered]
chunk {polyfills} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 582 kB [initial] [rendered]
chunk {runtime} runtime.bundle.js, runtime.bundle.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.bundle.js, styles.bundle.js.map (styles) 16.8 kB [initial] [rendered]
chunk {vendor} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.92 MB [initial] [rendered]

but the compiled script is not executed when loaded by chrome extension (via manifest.json file)

{
  ...,
  "background": {
    "scripts": ["background.bundle.js"],
    "persistent": true
  },
  "permissions": [
    "tabs",
    "activeTab",
    "background",
    "webNavigation",
    "webRequest",
    "storage"
  ]
}

To Reproduce
Steps to reproduce the behavior:

  1. Generate new ng app (angular 8)
  2. modify angular.json serve cmd to not serve but output files on dist and add chrome reloader webpack module

Expected behavior
Background to be executed

Screenshots
script is well loaded by extension:
Capture d鈥檈虂cran 2019-06-12 a虁 23 44 26
But nothing exectuded:
Capture d鈥檈虂cran 2019-06-12 a虁 23 44 32

Additional context
package.json

{
  "name": "extension-ng8",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "~8.0.0",
    "@angular/common": "~8.0.0",
    "@angular/compiler": "~8.0.0",
    "@angular/core": "~8.0.0",
    "@angular/forms": "~8.0.0",
    "@angular/platform-browser": "~8.0.0",
    "@angular/platform-browser-dynamic": "~8.0.0",
    "@angular/router": "~8.0.0",
    "@types/chrome": "^0.0.86",
    "rxjs": "~6.4.0",
    "tslib": "^1.9.0",
    "webpack-extension-reloader": "^1.1.0",
    "zone.js": "~0.9.1"
  },
  "devDependencies": {
    "@angular-builders/custom-webpack": "^8.0.2",
    "@angular-devkit/build-angular": "~0.800.0",
    "@angular/cli": "~8.0.2",
    "@angular/compiler-cli": "~8.0.0",
    "@angular/language-service": "~8.0.0",
    "@types/jasmine": "~3.3.8",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~8.9.4",
    "codelyzer": "^5.0.0",
    "jasmine-core": "~3.4.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~4.1.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.4.0",
    "protractor": "~5.4.0",
    "ts-node": "~7.0.0",
    "tslint": "~5.15.0",
    "typescript": "~3.4.3",
    "webpack-chrome-extension-reloader": "^1.3.0"
  }
}
not-an-issue

Most helpful comment

Ok, it took me a while but I figured out what your problem is. If you take a look at the resulting bundle (background.bundle.js) you'll see this code at the end of the bundle:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["background"],{

/***/ "./src/background/index.ts":
/*!*********************************!*\
  !*** ./src/background/index.ts ***!
  \*********************************/
/*! no static exports found */
/***/ (function(module, exports) {

console.log('will i be executed? Will I?');


/***/ })

},[["./src/background/index.ts","runtime"]]]);

This code adds your chunk as a webpack module but it doesn't actually run your module. Who does? runtime.bundle.js. This chunk is responsible for setting up webpack environment and installing JSONP callback for chunks loading. This code will eventually execute your chunk once it is loaded.
Thus, just add runtime.bundle.js to your manifest background.scripts array and you're good to go.

Boy I'm gonna write a blog post about this stuff.

Another insight - don't abuse ng serve. You use custom-webpack:browser builder for serve target, which is technically fine but very confusing. Instead, use build target for that and run it with ng build --watch. This way you keep your serve clean and also can build your extension with or w/o watching. Just don't forget to remove watch: true from your webpack config.

I've tested it, it works, the background page reloads when you change the file.
Best of luck!

All 12 comments

I'm not sure if I can help you with that. It seems that the builder performs OK. The script is generated, it is attached to the index.html, it is fetched by your Chrome extension (and therefore should be executed).
What exactly is not working? The extension is working? The background script? The main script? What actually defines the main script as an entry point of your Chrome extension?

Hi! thanks @meltedspark
The background script is not working
In fact, it's compiled but as an "exported" script, but therefore it is not executed (like if it has to be imported by another "main" script):

/***/ "./src/background/index.ts":
/*!*********************************!*\
  !*** ./src/background/index.ts ***!
  \*********************************/
/*! no static exports found */
/***/ (function(module, exports) {

console.log('will i be executed?');


/***/ })

},[["./src/background/index.ts","runtime"]]]);
//# sourceMappingURL=background.bundle.js.map

The extension itself (angular build) is ok (index.html + main.bundle.js + polyfills + runtime + styles + vendor).

The background.bundle.js is compiled but not executed.

I don't know how to define another "main" for background script to run itself without compromising angular main

Can you point me out to the concrete place in Chrome Extensions documentation where the background script described? I don't quite understand what actually connects this script to the extension.

OK, it seems pretty straight forward but I still don't have the full picture. I'm gonna need more information. Any chance that you can share a minimal reproduction repo?

Yes: https://github.com/Passiverecords/ChromeExtensionNg8
Just run yarn install && ng serve
a dist folder will be created and you can add it to chrome as an "unpacked" extension, and then inspect the background view (in the chrome extension manager)

Ok, it took me a while but I figured out what your problem is. If you take a look at the resulting bundle (background.bundle.js) you'll see this code at the end of the bundle:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["background"],{

/***/ "./src/background/index.ts":
/*!*********************************!*\
  !*** ./src/background/index.ts ***!
  \*********************************/
/*! no static exports found */
/***/ (function(module, exports) {

console.log('will i be executed? Will I?');


/***/ })

},[["./src/background/index.ts","runtime"]]]);

This code adds your chunk as a webpack module but it doesn't actually run your module. Who does? runtime.bundle.js. This chunk is responsible for setting up webpack environment and installing JSONP callback for chunks loading. This code will eventually execute your chunk once it is loaded.
Thus, just add runtime.bundle.js to your manifest background.scripts array and you're good to go.

Boy I'm gonna write a blog post about this stuff.

Another insight - don't abuse ng serve. You use custom-webpack:browser builder for serve target, which is technically fine but very confusing. Instead, use build target for that and run it with ng build --watch. This way you keep your serve clean and also can build your extension with or w/o watching. Just don't forget to remove watch: true from your webpack config.

I've tested it, it works, the background page reloads when you change the file.
Best of luck!

@meltedspark thanks! it works!

manifest.json is now:

  "background": {
    "scripts": ["background.bundle.js", "runtime.bundle.js"],
    "persistent": true
  },

I was using serve has the concept of the "developing phase" but I understand that it could be confusing...

Thanks one more time!

@meltedspark, thinking about dealing between serve or build, the problem is that the custom webpack config include the ExtensionReloader plugin, which has to be ignored during real "build" phase... If I could link --watch option of the build to the ExtensionReloader, it would be great :)

Well, there are actually two options:

  1. Use configurations.
    You can have two Webpack configs: first that just adds an entry for background bundle (this will be used for the regular build) and second extending it (in which you place all the live reload stuff, watch etc.). Then you can add development configuration under build target and specify a different (the second one) webpack config in there. At last, run ng build --development to use this configuration.
  2. You can use Custom webpack config function which receives Webpack config built by Angular CLI and check the watch property in it. If it's there push ExtensionReloader into plugins array.

adding runtime.js to the manifest.json works for getting background running. @just-jeb did you get the content-script to copy over without the webpackJsonp wrapper? Including runtime.js as part of the content_scripts array doesn't seem to work in the same manner.

Hi, @JBorgia. I'm not quite sure what you mean but I think you might be interested in this article. It doesn't talk much about content scripts but it works the same same way it works for background scripts.
Also, this comment might help.

Was this page helpful?
0 / 5 - 0 ratings