├── .prettierrc ├── client ├── events.constants.ts ├── args.constant.ts ├── index.ts ├── ExtensionCompiler.ts ├── manual.ts └── args-parser.ts ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── sample-gif.gif ├── ISSUE_TEMPLATE.md └── CONTRIBUTING.MD ├── sample ├── plugin-src │ ├── my-background.js │ ├── some-asset.txt │ ├── style.css │ ├── dependency-sample.js │ ├── my-content-script.js │ ├── popup.html │ └── popup.js ├── icons │ ├── webpack.16.png │ ├── webpack.48.png │ └── webpack.128.png ├── manifest.json ├── webpack.plugin.js └── README.md ├── .prettierignore ├── src ├── hot-reload │ ├── index.ts │ ├── changes-triggerer.ts │ ├── HotReloaderServer.ts │ └── SignEmitter.ts ├── middleware │ ├── index.ts │ ├── middleware-source-builder.ts │ ├── middleware-injector.ts │ └── wcer-middleware.raw.ts ├── constants │ ├── reference-docs.constants.ts │ ├── midleware-config.constants.ts │ ├── log.constants.ts │ ├── options.constants.ts │ └── fast-reloading.constants.ts ├── messages │ ├── errors.ts │ ├── warnings.ts │ └── Message.ts ├── utils │ ├── default-options.ts │ ├── env.js │ ├── logger.ts │ ├── signals.ts │ └── block-protection.ts ├── webpack │ ├── AbstractChromeExtensionReloader.ts │ └── CompilerEventsFacade.ts ├── index.ts └── ChromeExtensionReloader.ts ├── .npmignore ├── .travis.yml ├── .babelrc ├── specs ├── index.ts ├── middleware-source-builder.specs.ts ├── changes-triggerer.specs.ts ├── block-protection.specs.ts ├── ChromeExtensionReloader.specs.ts ├── SignEmitter.specs.ts └── middleware-injector.specs.ts ├── tslint.json ├── typings ├── webpack-chrome-extension-reloader.d.ts └── index.d.ts ├── tsconfig.json ├── .gitignore ├── LICENSE.txt ├── webpack.config.js ├── package.json └── README.MD /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /client/events.constants.ts: -------------------------------------------------------------------------------- 1 | export const SIG_EXIT = "SIG_EXIT"; 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [Write what you PR is]. Closes #[ISSUE_NUMBER] -------------------------------------------------------------------------------- /sample/plugin-src/my-background.js: -------------------------------------------------------------------------------- 1 | console.info("Change anything here"); 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | src/middleware/wcer-middleware.raw.ts -------------------------------------------------------------------------------- /sample/plugin-src/some-asset.txt: -------------------------------------------------------------------------------- 1 | Asset sample to test files emitted by CopyWebpackPlugin -------------------------------------------------------------------------------- /sample/plugin-src/style.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: white; 3 | background: black; 4 | } 5 | -------------------------------------------------------------------------------- /sample/plugin-src/dependency-sample.js: -------------------------------------------------------------------------------- 1 | import assetContent from "./some-asset.txt"; 2 | 3 | export default () => assetContent; -------------------------------------------------------------------------------- /.github/sample-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubenspgcavalcante/webpack-chrome-extension-reloader/HEAD/.github/sample-gif.gif -------------------------------------------------------------------------------- /sample/icons/webpack.16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubenspgcavalcante/webpack-chrome-extension-reloader/HEAD/sample/icons/webpack.16.png -------------------------------------------------------------------------------- /sample/icons/webpack.48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubenspgcavalcante/webpack-chrome-extension-reloader/HEAD/sample/icons/webpack.48.png -------------------------------------------------------------------------------- /src/hot-reload/index.ts: -------------------------------------------------------------------------------- 1 | import _changesTriggerer from "./changes-triggerer"; 2 | 3 | export const changesTriggerer = _changesTriggerer; 4 | -------------------------------------------------------------------------------- /sample/icons/webpack.128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rubenspgcavalcante/webpack-chrome-extension-reloader/HEAD/sample/icons/webpack.128.png -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import _middlewareInjector from "./middleware-injector"; 2 | 3 | export const middlewareInjector = _middlewareInjector; 4 | -------------------------------------------------------------------------------- /sample/plugin-src/my-content-script.js: -------------------------------------------------------------------------------- 1 | import depSample from "./dependency-sample"; 2 | 3 | console.info("Change anything here!"); 4 | console.log(depSample()); -------------------------------------------------------------------------------- /src/constants/reference-docs.constants.ts: -------------------------------------------------------------------------------- 1 | export const REF_URL = 2 | "https://github.com/rubenspgcavalcante/webpack-chrome-extension-reloader/wiki/General-Information"; 3 | -------------------------------------------------------------------------------- /sample/plugin-src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/constants/midleware-config.constants.ts: -------------------------------------------------------------------------------- 1 | export const RECONNECT_INTERVAL = 2000; 2 | export const SOCKET_ERR_CODE_REF = 3 | "https://tools.ietf.org/html/rfc6455#section-7.4.1"; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | sample/ 3 | specs/ 4 | dist/tests.js 5 | dist/tests.js.map 6 | .idea/ 7 | .vscode/ 8 | .babelrc 9 | .gitignore 10 | tsconfig.json 11 | tslint.json 12 | webpack.config.js 13 | yarn.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | 5 | before_script: yarn build 6 | script: yarn test 7 | 8 | cache: yarn 9 | branches: 10 | only: 11 | - master 12 | - /^greenkeeper/.*$/ -------------------------------------------------------------------------------- /src/messages/errors.ts: -------------------------------------------------------------------------------- 1 | import Message from "./Message"; 2 | import { ERROR } from "../constants/log.constants"; 3 | 4 | export const bgScriptRequiredMsg = new Message( 5 | ERROR, 6 | 1, 7 | "Background script entry is required" 8 | ); 9 | -------------------------------------------------------------------------------- /src/constants/log.constants.ts: -------------------------------------------------------------------------------- 1 | export const NONE: LOG_NONE = 0; 2 | export const LOG: LOG_LOG = 1; 3 | export const INFO: LOG_INFO = 2; 4 | export const WARN: LOG_WARN = 3; 5 | export const ERROR: LOG_ERROR = 4; 6 | export const DEBUG: LOG_DEBUG = 5; 7 | -------------------------------------------------------------------------------- /client/args.constant.ts: -------------------------------------------------------------------------------- 1 | export const HELP = "help"; 2 | export const CONFIG = "config"; 3 | export const PORT = "port"; 4 | export const NO_PAGE_RELOAD = "no-page-reload"; 5 | export const BACKGROUND_ENTRY = "background"; 6 | export const CONTENT_SCRIPT_ENTRY = "content-script"; 7 | -------------------------------------------------------------------------------- /src/constants/options.constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PORT = 9090; 2 | export const DEFAULT_CONFIG = "webpack.config.js"; 3 | export const DEFAULT_RELOAD_PAGE = true; 4 | export const DEFAULT_CONTENT_SCRIPT_ENTRY = "content-script"; 5 | export const DEFAULT_BACKGROUND_ENTRY = "background"; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Type:** 2 | - [ ] bug 3 | - [ ] feature 4 | - [ ] enhancement 5 | - [ ] question 6 | 7 | **Environment:** 8 | - OS: 9 | - Library Version: 10 | 11 | **I'm going to open a PR:** 12 | - [ ] yes 13 | - [ ] no 14 | 15 | **Description:** 16 | [ Write the issue description here ] -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ], 10 | "plugins": [ 11 | "@babel/plugin-syntax-dynamic-import", 12 | "@babel/plugin-proposal-class-properties", 13 | "@babel/plugin-proposal-object-rest-spread" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /specs/index.ts: -------------------------------------------------------------------------------- 1 | import { install } from "source-map-support"; 2 | 3 | import "./ChromeExtensionReloader.specs"; 4 | import "./middleware-source-builder.specs"; 5 | import "./middleware-injector.specs"; 6 | import "./changes-triggerer.specs"; 7 | import "./block-protection.specs"; 8 | import "./SignEmitter.specs"; 9 | 10 | install(); 11 | -------------------------------------------------------------------------------- /specs/middleware-source-builder.specs.ts: -------------------------------------------------------------------------------- 1 | import {assert} from "chai"; 2 | import middlewareSourceBuilder from "../src/middleware/middleware-source-builder"; 3 | 4 | describe("middlewareSourceBuilder", () => { 5 | it("Build the middleware from the post-compiled source code", () => { 6 | assert(typeof middlewareSourceBuilder({port: 1234, reloadPage: true}) === 'string'); 7 | }); 8 | }); -------------------------------------------------------------------------------- /src/utils/default-options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_BACKGROUND_ENTRY, 3 | DEFAULT_CONTENT_SCRIPT_ENTRY, 4 | DEFAULT_PORT, 5 | DEFAULT_RELOAD_PAGE 6 | } from "../constants/options.constants"; 7 | 8 | export default { 9 | reloadPage: DEFAULT_RELOAD_PAGE, 10 | port: DEFAULT_PORT, 11 | entries: { 12 | contentScript: DEFAULT_CONTENT_SCRIPT_ENTRY, 13 | background: DEFAULT_BACKGROUND_ENTRY 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /sample/plugin-src/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Neither this file or any dependency (like style.css) should trigger 3 | * the plugin reload. This way the popup will not close clearing it state 4 | */ 5 | import "./style.css"; 6 | 7 | const element = document.createElement("span"); 8 | element.innerText = "You clicked me! :)"; 9 | 10 | document 11 | .getElementById("button") 12 | .addEventListener("click", () => document.body.appendChild(element)); 13 | -------------------------------------------------------------------------------- /src/hot-reload/changes-triggerer.ts: -------------------------------------------------------------------------------- 1 | import HotReloaderServer from "./HotReloaderServer"; 2 | import { info } from "../utils/logger"; 3 | 4 | export default (port: number, reloadPage: boolean) => { 5 | const server = new HotReloaderServer(port); 6 | 7 | info("[ Starting the Chrome Hot Plugin Reload Server... ]"); 8 | server.listen(); 9 | 10 | return (): Promise => { 11 | return server.signChange(reloadPage); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-eval": true, 4 | "arrow-parens": [true, "ban-single-arg-parens"], 5 | "indent": [true, "spaces"], 6 | "no-duplicate-variable": true, 7 | "one-line": [true, "check-open-brace", "check-whitespace"], 8 | "variable-name": [true, "ban-keywords"], 9 | "triple-equals": [true, "allow-null-check"], 10 | "extends": [ 11 | "tslint:latest", 12 | "tslint-config-prettier" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /src/webpack/AbstractChromeExtensionReloader.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "webpack"; 2 | import CompilerEventsFacade from "./CompilerEventsFacade"; 3 | 4 | export default abstract class AbstractChromeExtensionReloader 5 | implements Plugin { 6 | protected _injector: Function; 7 | protected _triggerer: Function; 8 | protected _eventAPI: CompilerEventsFacade; 9 | protected _chunkVersions: Object; 10 | 11 | context: any; 12 | 13 | abstract apply(options?: any); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/env.js: -------------------------------------------------------------------------------- 1 | const { production, development, test } = [ 2 | "production", 3 | "development", 4 | "test" 5 | ].reduce((acc, env) => { 6 | acc[env] = (val) => (process.env["NODE_ENV"] === env ? val : null); 7 | return acc; 8 | }, {}); 9 | 10 | const isProduction = !!production(true); 11 | const isDevelopment = !!development(true); 12 | const isTest = !!test(true); 13 | 14 | module.exports = { 15 | production, 16 | development, 17 | test, 18 | isProduction, 19 | isDevelopment, 20 | isTest 21 | }; 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { install } from "source-map-support"; 2 | import ChromeExtensionReloaderImpl from "./ChromeExtensionReloader"; 3 | import { DEBUG, ERROR, NONE } from "./constants/log.constants"; 4 | import { setLogLevel } from "./utils/logger"; 5 | 6 | install(); 7 | 8 | const logLevel = process.env.NODE_ENV 9 | ? { 10 | production: ERROR, 11 | development: DEBUG, 12 | test: NONE 13 | }[process.env.NODE_ENV] 14 | : ERROR; 15 | 16 | setLogLevel(logLevel); 17 | export = ChromeExtensionReloaderImpl; 18 | -------------------------------------------------------------------------------- /src/messages/warnings.ts: -------------------------------------------------------------------------------- 1 | import Message from "./Message"; 2 | import { WARN } from "../constants/log.constants"; 3 | 4 | export const onlyOnDevelopmentMsg = new Message( 5 | WARN, 6 | 1, 7 | "Warning, Chrome Extension Reloader Plugin was not enabled! It runs only on webpack --mode=development (v4 or more) or with NODE_ENV=development (lower versions)" 8 | ); 9 | 10 | export const browserVerWrongFormatMsg = new Message( 11 | WARN, 12 | 2, 13 | "Warning, Chrome browser with unexpected version format. Expected x.x.x.x, Provided <%= version %>" 14 | ); 15 | -------------------------------------------------------------------------------- /typings/webpack-chrome-extension-reloader.d.ts: -------------------------------------------------------------------------------- 1 | declare module "webpack-chrome-extension-reloader" { 2 | type PluginOptions = { 3 | port: number; 4 | reloadPage: boolean; 5 | entries: EntriesOption; 6 | }; 7 | type EntriesOption = { 8 | background: string; 9 | contentScript: ContentScriptOption; 10 | }; 11 | 12 | type ContentScriptOption = string | Array; 13 | 14 | export default interface ChromeExtensionReloader { 15 | new (options?: PluginOptions): ChromeExtensionReloaderInstance; 16 | } 17 | 18 | export interface ChromeExtensionReloaderInstance { 19 | apply(compiler: Object): void; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Webpack Chrome Extension Reloader Sample", 4 | "version": "0.1", 5 | "background": { 6 | "scripts": [ 7 | "background.js" 8 | ] 9 | }, 10 | "icons": { 11 | "16": "webpack.16.png", 12 | "48": "webpack.48.png", 13 | "128": "webpack.128.png" 14 | }, 15 | "browser_action": { 16 | "default_popup": "popup.html" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "" 22 | ], 23 | "js": [ 24 | "content-script.js" 25 | ], 26 | "css": [ 27 | "style.css" 28 | ] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/middleware/middleware-source-builder.ts: -------------------------------------------------------------------------------- 1 | import { template } from "lodash"; 2 | import { 3 | RECONNECT_INTERVAL, 4 | SOCKET_ERR_CODE_REF 5 | } from "../constants/midleware-config.constants"; 6 | import * as signals from "../utils/signals"; 7 | import rawSource from "raw-loader!./wcer-middleware.raw"; 8 | 9 | export default function middleWareSourceBuilder({ 10 | port, 11 | reloadPage 12 | }: MiddlewareTemplateParams): string { 13 | const tmpl = template(rawSource); 14 | 15 | return tmpl({ 16 | WSHost: `ws://localhost:${port}`, 17 | reloadPage: `${reloadPage}`, 18 | signals: JSON.stringify(signals), 19 | config: JSON.stringify({ RECONNECT_INTERVAL, SOCKET_ERR_CODE_REF }) 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /client/index.ts: -------------------------------------------------------------------------------- 1 | import { install } from "source-map-support"; 2 | import * as minimist from "minimist"; 3 | import argsParser from "./args-parser"; 4 | import { SIG_EXIT } from "./events.constants"; 5 | import ExtensionCompiler from "./ExtensionCompiler"; 6 | import { error } from "util"; 7 | 8 | install(); 9 | const { _, ...args } = minimist(process.argv.slice(2)); 10 | 11 | try { 12 | const { webpackConfig, pluginOptions } = argsParser(args); 13 | const compiler = new ExtensionCompiler(webpackConfig, pluginOptions); 14 | compiler.watch(); 15 | } catch (err) { 16 | if (err.type === SIG_EXIT) { 17 | process.exit(err.payload); 18 | } else { 19 | error(err); 20 | process.exit(err.code); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { DEBUG, ERROR, INFO, LOG, WARN } from "../constants/log.constants"; 2 | import { green, red, white, yellow } from "colors/safe"; 3 | 4 | let logLevel; 5 | export const setLogLevel = (level: LOG_LEVEL) => (logLevel = level); 6 | 7 | export const log = (message: string) => logLevel >= LOG && console.log(message); 8 | export const info = (message: string) => 9 | logLevel >= INFO && console.info(green(message)); 10 | export const warn = (message: string) => 11 | logLevel >= WARN && console.warn(yellow(message)); 12 | export const error = (message: string) => 13 | logLevel >= ERROR && console.error(red(message)); 14 | export const debug = (message: string) => 15 | logLevel >= DEBUG && console.debug(white(debug(message))); 16 | -------------------------------------------------------------------------------- /src/utils/signals.ts: -------------------------------------------------------------------------------- 1 | export const SIGN_CHANGE: ActionType = "SIGN_CHANGE"; 2 | export const SIGN_RELOAD: ActionType = "SIGN_RELOAD"; 3 | export const SIGN_RELOADED: ActionType = "SIGN_RELOADED"; 4 | export const SIGN_LOG: ActionType = "SIGN_LOG"; 5 | export const SIGN_CONNECT: ActionType = "SIGN_CONNECT"; 6 | 7 | export const signChange: ActionFactory = ({ reloadPage = true }) => ({ 8 | type: SIGN_CHANGE, 9 | payload: { reloadPage } 10 | }); 11 | export const signReload: ActionFactory = () => ({ type: SIGN_RELOAD }); 12 | export const signReloaded: ActionFactory = (msg: string) => ({ 13 | type: SIGN_RELOADED, 14 | payload: msg 15 | }); 16 | export const signLog: ActionFactory = (msg: string) => ({ 17 | type: SIGN_LOG, 18 | payload: msg 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "allowSyntheticDefaultImports": true, 5 | "experimentalDecorators": true, 6 | "target": "es2016", 7 | "sourceMap": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "allowJs": true, 11 | "typeRoots": ["typings/index.d.ts"], 12 | "types": ["node", "mocha", "chrome"] 13 | }, 14 | "presets": [ 15 | "env", 16 | { 17 | "modules": false 18 | } 19 | ], 20 | "files": [ 21 | "typings/index.d.ts", 22 | "typings/webpack-chrome-extension-reloader.d.ts" 23 | ], 24 | "include": ["client/**/*", "src/**/*", "specs/**/*"], 25 | "exclude": ["node_modules", "**/*.spec.ts"], 26 | "extensions": { 27 | ".ts": "TS", 28 | ".js": "JS" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /specs/changes-triggerer.specs.ts: -------------------------------------------------------------------------------- 1 | import ws = require("ws"); 2 | import { assert } from "chai"; 3 | import { spy, stub } from "sinon"; 4 | import HotReloaderServer from "../src/hot-reload/HotReloaderServer"; 5 | import changesTriggerer from "../src/hot-reload/changes-triggerer"; 6 | 7 | describe("changesTriggerer", () => { 8 | let listenSpy; 9 | beforeEach(() => { 10 | stub(ws, "Server").callsFake(function() { 11 | this.on = () => null; 12 | this.send = () => null; 13 | }); 14 | listenSpy = spy(HotReloaderServer.prototype, "listen"); 15 | stub(HotReloaderServer.prototype, "signChange").callsFake(() => 16 | Promise.resolve() 17 | ); 18 | }); 19 | 20 | it("Should start the hot reloading server", () => { 21 | changesTriggerer(8080, true); 22 | assert.isOk(listenSpy.calledOnce); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /specs/block-protection.specs.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { useFakeTimers } from "sinon"; 3 | import { FAST_RELOAD_DEBOUNCING_FRAME } from "../src/constants/fast-reloading.constants"; 4 | import { debounceSignal } from "../src/utils/block-protection"; 5 | 6 | const _ = require("lodash"); 7 | 8 | describe("debounce signals to prevent extension block", () => { 9 | let calls; 10 | const clock = useFakeTimers(); 11 | 12 | const test = () => { 13 | calls++; 14 | }; 15 | 16 | beforeEach(() => { 17 | calls = 0; 18 | }); 19 | 20 | afterEach(() => { 21 | clock.restore(); 22 | }); 23 | 24 | it(`It should debounce the method call for ${ 25 | FAST_RELOAD_DEBOUNCING_FRAME 26 | } milli`, () => { 27 | const sample = debounceSignal(FAST_RELOAD_DEBOUNCING_FRAME)(test); 28 | 29 | sample(); 30 | clock.tick(400); 31 | sample(); 32 | clock.tick(FAST_RELOAD_DEBOUNCING_FRAME); 33 | assert.equal(calls, 1); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .bin/ 4 | .tmp/ 5 | .DS_STORE 6 | dist/ 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules 39 | jspm_packages 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # NPM lock file 57 | package-lock.json -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Rubens Pinheiro Gonçalves Cavalcante 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/middleware/middleware-injector.ts: -------------------------------------------------------------------------------- 1 | import { ConcatSource } from "webpack-sources"; 2 | import middleWareSourceBuilder from "./middleware-source-builder"; 3 | import { EntriesOption } from "webpack-chrome-extension-reloader"; 4 | 5 | export default function middlewareInjector( 6 | { background, contentScript }: EntriesOption, 7 | { port, reloadPage }: MiddlewareTemplateParams 8 | ) { 9 | const source = middleWareSourceBuilder({ port, reloadPage }); 10 | const sourceFactory: SourceFactory = (...sources): Source => 11 | new ConcatSource(...sources); 12 | 13 | return (assets: object, chunks: WebpackChunk[]) => 14 | chunks.reduce((prev, { name, files }) => { 15 | if ( 16 | name === background || 17 | name === contentScript || 18 | contentScript.includes(name) 19 | ) { 20 | files.forEach(entryPoint => { 21 | if (/\.js$/.test(entryPoint)) { 22 | prev[entryPoint] = sourceFactory(source, assets[entryPoint]); 23 | } 24 | }); 25 | } 26 | return prev; 27 | }, {}); 28 | } 29 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Important checks 4 | - Before create any pull requests, please open a issue explaining the situation 5 | - Be sure to follow the tslint rules and run the prettier 6 | - Be sure **before open the pull request**, to test the existent code and/or create tests if you made a new feature or 7 | changed a already existing one. 8 | 9 | # Issues 10 | Should be in the format: 11 | 12 | ```text 13 | **Type:** 14 | - [x] bug 15 | - [ ] feature 16 | - [ ] enhancement 17 | - [ ] question 18 | 19 | **Environment:** 20 | - OS: Windows 10 21 | - Version: 0.6.0 22 | 23 | **Going to open a PR:** 24 | - [x] yes 25 | - [ ] no 26 | 27 | **Description:** 28 | The messages aren't showing on the console 29 | ``` 30 | 31 | # Pull Requests 32 | - On the description of the pull request set the issue id for closing it: 33 | ```text 34 | Now the messages are showing. Closes #41 35 | ``` 36 | 37 | ## Following the guideline 38 | Please follow the above rules, so the repo can stay consistent and easy for everyone find questions and 39 | already resolved stuff. Be aware that your PR can be denied if you don't follow then :cry: -------------------------------------------------------------------------------- /src/constants/fast-reloading.constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Chrome lets only a max number of calls in a time frame 3 | * before block the plugin for be reloading itself to much 4 | * @see https://github.com/rubenspgcavalcante/webpack-chrome-extension-reloader/issues/2 5 | */ 6 | export const FAST_RELOAD_DEBOUNCING_FRAME = 2000; 7 | 8 | export const FAST_RELOAD_CALLS = 6; 9 | export const FAST_RELOAD_WAIT = 10 * 1000; 10 | 11 | // ======================================================================================================================== // 12 | 13 | /** 14 | * A new reloading rate was createad after opening a bug ticket on 15 | * Chromium, and the revision was merged to master 16 | * @see https://chromium-review.googlesource.com/c/chromium/src/+/1340272 17 | */ 18 | 19 | /** 20 | * The Chrome/Chromium version number that includes the new rates 21 | * @see https://storage.googleapis.com/chromium-find-releases-static/d3b.html#d3b25e1380984b2f1f23d0e8dc1a337743c6caaf 22 | */ 23 | export const NEW_FAST_RELOAD_CHROME_VERSION = "73.0.3637.0"; 24 | 25 | export const NEW_FAST_RELOAD_DEBOUNCING_FRAME = 1000; 26 | export const NEW_FAST_RELOAD_CALLS = 30; 27 | -------------------------------------------------------------------------------- /client/ExtensionCompiler.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from "webpack"; 2 | import { Configuration } from "webpack"; 3 | import ChromeExtensionReloaderImpl from "../src/ChromeExtensionReloader"; 4 | import { error } from "util"; 5 | import { info } from "../src/utils/logger"; 6 | import { PluginOptions } from "webpack-chrome-extension-reloader"; 7 | 8 | export default class ExtensionCompiler { 9 | private compiler; 10 | 11 | constructor( 12 | config: (env: Object, args: Array) => Configuration | Configuration, 13 | pluginOptions: PluginOptions 14 | ) { 15 | this.compiler = webpack( 16 | typeof config === "function" ? config(process.env, process.argv) : config 17 | ); 18 | new ChromeExtensionReloaderImpl(pluginOptions).apply(this.compiler); 19 | } 20 | 21 | private static treatErrors(err) { 22 | error(err.stack || err); 23 | if (err.details) { 24 | error(err.details); 25 | } 26 | } 27 | 28 | watch() { 29 | this.compiler.watch({}, (err, stats) => { 30 | if (err) { 31 | return ExtensionCompiler.treatErrors(err); 32 | } 33 | info(stats.toString({ colors: true })); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/messages/Message.ts: -------------------------------------------------------------------------------- 1 | import { INFO, WARN, ERROR } from "../constants/log.constants"; 2 | import { REF_URL } from "../constants/reference-docs.constants"; 3 | import { white, bold } from "colors/safe"; 4 | import { template } from "lodash"; 5 | 6 | export default class Message { 7 | private referenceNumber: number; 8 | private type: LOG_INFO | LOG_WARN | LOG_ERROR; 9 | private message: string; 10 | 11 | constructor(type, referenceNumber, message) { 12 | this.type = type; 13 | this.referenceNumber = referenceNumber; 14 | this.message = message; 15 | } 16 | 17 | private getPrefix() { 18 | switch (this.type) { 19 | case INFO: 20 | return "I"; 21 | case WARN: 22 | return "W"; 23 | case ERROR: 24 | return "E"; 25 | } 26 | } 27 | 28 | public get(additionalData: object = {}) { 29 | const code = `WCER-${this.getPrefix()}${this.referenceNumber}`; 30 | const refLink = bold(white(`${REF_URL}#${code}`)); 31 | return `[${code}] ${template(this.message, additionalData)}.\nVisit ${ 32 | refLink 33 | } for complete details\n`; 34 | } 35 | 36 | toString() { 37 | return this.get(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/webpack/CompilerEventsFacade.ts: -------------------------------------------------------------------------------- 1 | export default class CompilerEventsFacade { 2 | static extensionName = "chrome-extension-reloader"; 3 | private _compiler: any; 4 | private _legacyTapable: boolean; 5 | 6 | constructor(compiler) { 7 | this._compiler = compiler; 8 | this._legacyTapable = !compiler.hooks; 9 | } 10 | 11 | afterOptimizeChunkAssets(call: Function) { 12 | return this._legacyTapable 13 | ? this._compiler.plugin("compilation", comp => 14 | comp.plugin("after-optimize-chunk-assets", chunks => 15 | call(comp, chunks) 16 | ) 17 | ) 18 | : this._compiler.hooks.compilation.tap( 19 | CompilerEventsFacade.extensionName, 20 | comp => 21 | comp.hooks.afterOptimizeChunkAssets.tap( 22 | CompilerEventsFacade.extensionName, 23 | chunks => call(comp, chunks) 24 | ) 25 | ); 26 | } 27 | 28 | afterEmit(call: Function) { 29 | return this._legacyTapable 30 | ? this._compiler.plugin("after-emit", call) 31 | : this._compiler.hooks.afterEmit.tap( 32 | CompilerEventsFacade.extensionName, 33 | call 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hot-reload/HotReloaderServer.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "ws"; 2 | import { info } from "../utils/logger"; 3 | import SignEmitter from "./SignEmitter"; 4 | 5 | export default class HotReloaderServer { 6 | private _server: Server; 7 | private _signEmiter: SignEmitter; 8 | 9 | constructor(port: number) { 10 | this._server = new Server({ port }); 11 | } 12 | 13 | listen() { 14 | this._server.on("connection", (ws, msg) => { 15 | const browserVersion = this._getBrowserVersion(msg.headers["user-agent"]); 16 | this._signEmiter = new SignEmitter(this._server, browserVersion); 17 | 18 | ws.on("message", (data: string) => 19 | info(`Message from the client: ${JSON.parse(data).payload}`) 20 | ); 21 | ws.on("error", () => { 22 | // NOOP - swallow socket errors due to http://git.io/vbhSN 23 | }); 24 | }); 25 | } 26 | 27 | signChange(reloadPage: boolean): Promise { 28 | if (this._signEmiter) { 29 | return this._signEmiter.safeSignChange(reloadPage); 30 | } else return Promise.resolve(null); 31 | } 32 | 33 | private _getBrowserVersion(userAgent) { 34 | const ver = userAgent.match(/\ Chrom(e|ium)\/([0-9\.]*)\ /); 35 | if (ver && ver.length === 3) { 36 | return ver[2]; 37 | } 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/block-protection.ts: -------------------------------------------------------------------------------- 1 | import { debounce, runInContext } from "lodash"; 2 | import { warn, info } from "./logger"; 3 | 4 | export const debounceSignal = (deboucingFrame: number, context?: Object) => ( 5 | func: Function 6 | ) => { 7 | if (context) { 8 | runInContext(context); 9 | } 10 | 11 | return debounce((...args) => { 12 | return func.apply(context, args); 13 | }, deboucingFrame); 14 | }; 15 | 16 | export const fastReloadBlocker = (maxCalls: number, wait: number, context) => ( 17 | func: Function 18 | ) => { 19 | let calls = 0; 20 | let inWait = false; 21 | 22 | return (...args) => { 23 | if (inWait) { 24 | return; 25 | } else if (calls === maxCalls) { 26 | calls = 0; 27 | inWait = true; 28 | 29 | let interval = wait / 1000; 30 | warn( 31 | `Please wait ${ 32 | interval 33 | } secs. for next reload to prevent your extension being blocked` 34 | ); 35 | const logInterval = setInterval(() => warn(`${--interval} ...`), 1000); 36 | 37 | setTimeout(() => { 38 | clearInterval(logInterval); 39 | info("Signing for reload now"); 40 | func.apply(context, args); 41 | inWait = false; 42 | }, wait); 43 | } else { 44 | calls++; 45 | return func.apply(context, args); 46 | } 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /client/manual.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_BACKGROUND_ENTRY, 3 | DEFAULT_CONFIG, 4 | DEFAULT_CONTENT_SCRIPT_ENTRY, 5 | DEFAULT_PORT 6 | } from "../src/constants/options.constants"; 7 | 8 | export default () => ` 9 | Usage: 10 | wcer [--config ] [--port ] [--no-page-reload] [--content-script ] [--background ] 11 | 12 | Complete API: 13 | +------------------------------------------------------------------------------------------------------------+ 14 | | name | default | description | 15 | |--------------------|-------------------|-------------------------------------------------------------------| 16 | | --help | | Show this help 17 | | --config | ${ 18 | DEFAULT_CONFIG 19 | } | The webpack configuration file path | 20 | | --port | ${ 21 | DEFAULT_PORT 22 | } | The port to run the server | 23 | | --content-script | ${ 24 | DEFAULT_CONTENT_SCRIPT_ENTRY 25 | } | The **entry/entries** name(s) for the content script(s) | 26 | | --background | ${ 27 | DEFAULT_BACKGROUND_ENTRY 28 | } | The **entry** name for the background script | 29 | | --no-page-reload | | Disable the auto reloading of all **pages** which runs the plugin | 30 | +------------------------------------------------------------------------------------------------------------+ 31 | `; 32 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type ActionType = string; 2 | declare type Action = { type: ActionType; payload?: any }; 3 | declare type ActionFactory = (payload?: any) => Action; 4 | 5 | declare type MiddlewareTemplateParams = { port: number; reloadPage: boolean }; 6 | 7 | declare type VersionPair = [number | undefined, number | undefined]; 8 | 9 | declare type LOG_NONE = 0; 10 | declare type LOG_LOG = 1; 11 | declare type LOG_INFO = 2; 12 | declare type LOG_WARN = 3; 13 | declare type LOG_ERROR = 4; 14 | declare type LOG_DEBUG = 5; 15 | 16 | declare type LOG_LEVEL = 17 | | LOG_NONE 18 | | LOG_LOG 19 | | LOG_INFO 20 | | LOG_WARN 21 | | LOG_ERROR 22 | | LOG_DEBUG; 23 | 24 | declare interface Source { 25 | source(); 26 | 27 | size(): number; 28 | 29 | map(options: object): void; 30 | 31 | sourceAndMap(options: object): object; 32 | 33 | node(); 34 | 35 | listNode(); 36 | 37 | updateHash(hash: string): void; 38 | } 39 | 40 | declare type SourceFactory = ( 41 | concatSource: string, 42 | rootSource: string 43 | ) => Source; 44 | 45 | declare type WebpackChunk = { files: Array; name: string, hash: string }; 46 | 47 | declare type ClientEvent = { type: string; payload: any }; 48 | 49 | declare module "*.json" { 50 | const json: any; 51 | export = json; 52 | } 53 | 54 | declare module "*.txt" { 55 | const text: string; 56 | export = text; 57 | } 58 | 59 | declare module "*.source.ts" { 60 | const sourceCode: string; 61 | export = sourceCode; 62 | } 63 | 64 | declare module "raw-loader*" { 65 | const rawText: string; 66 | export default rawText; 67 | } 68 | -------------------------------------------------------------------------------- /client/args-parser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BACKGROUND_ENTRY, 3 | CONFIG, 4 | CONTENT_SCRIPT_ENTRY, 5 | HELP, 6 | NO_PAGE_RELOAD, 7 | PORT 8 | } from "./args.constant"; 9 | import { resolve } from "path"; 10 | import { 11 | DEFAULT_BACKGROUND_ENTRY, 12 | DEFAULT_CONFIG, 13 | DEFAULT_CONTENT_SCRIPT_ENTRY, 14 | DEFAULT_PORT 15 | } from "../src/constants/options.constants"; 16 | import { cwd } from "process"; 17 | import manual from "./manual"; 18 | import { SIG_EXIT } from "./events.constants"; 19 | import { log, error } from "util"; 20 | import { PluginOptions } from "webpack-chrome-extension-reloader"; 21 | 22 | export default (args: object) => { 23 | if (args[HELP]) { 24 | log(manual()); 25 | throw { type: SIG_EXIT, payload: 0 }; 26 | } 27 | 28 | const config = args[CONFIG] || DEFAULT_CONFIG; 29 | const port = args[PORT] || DEFAULT_PORT; 30 | const contentArg = args[CONTENT_SCRIPT_ENTRY] || DEFAULT_CONTENT_SCRIPT_ENTRY; 31 | const contentScript = contentArg.includes(",") 32 | ? contentArg.split(",") 33 | : contentArg; 34 | 35 | const background = args[BACKGROUND_ENTRY] || DEFAULT_BACKGROUND_ENTRY; 36 | const pluginOptions: PluginOptions = { 37 | port, 38 | reloadPage: !args[NO_PAGE_RELOAD], 39 | entries: { contentScript, background } 40 | }; 41 | 42 | const optPath = resolve(cwd(), config); 43 | 44 | try { 45 | // tslint:disable-next-line:no-eval 46 | const webpackConfig = eval("require")(optPath); 47 | return { webpackConfig, pluginOptions }; 48 | } catch (err) { 49 | error(`[Error] Couldn't require the file: ${optPath}`); 50 | error(err); 51 | throw { type: SIG_EXIT, payload: 1 }; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /sample/webpack.plugin.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const WebpackChromeReloaderPlugin = require("../dist/webpack-chrome-extension-reloader"); 5 | 6 | const mode = process.env.NODE_ENV; 7 | module.exports = { 8 | mode, 9 | devtool: "inline-source-map", 10 | entry: { 11 | "content-script": "./sample/plugin-src/my-content-script.js", 12 | background: "./sample/plugin-src/my-background.js", 13 | 14 | // This is just the popup script, it shouldn't trigger the plugin reload when is changed 15 | popup: "./sample/plugin-src/popup.js" 16 | }, 17 | output: { 18 | publicPath: ".", 19 | path: resolve(__dirname, "dist/"), 20 | filename: "[name].js", 21 | libraryTarget: "umd" 22 | }, 23 | plugins: [ 24 | /***********************************************************************/ 25 | /* By default the plugin will work only when NODE_ENV is "development" */ 26 | /***********************************************************************/ 27 | new WebpackChromeReloaderPlugin(), 28 | 29 | new MiniCssExtractPlugin({ filename: "style.css" }), 30 | new CopyWebpackPlugin([ 31 | { from: "./sample/plugin-src/popup.html"}, 32 | { from: "./sample/manifest.json" }, 33 | { from: "./sample/icons" } 34 | ]) 35 | ], 36 | module: { 37 | rules: [ 38 | { 39 | test: /\.js?$/, 40 | exclude: /node_modules/, 41 | use: { 42 | loader: "babel-loader", 43 | options: { 44 | presets: [require("@babel/preset-env")] 45 | } 46 | } 47 | }, 48 | { 49 | test: /\.css$/, 50 | use: [ 51 | { 52 | loader: MiniCssExtractPlugin.loader 53 | }, 54 | "css-loader" 55 | ] 56 | }, 57 | { 58 | test: /\.txt$/, 59 | use: "raw-loader" 60 | } 61 | ] 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Example of Plugin using WCER 2 | 3 | I this directory you can see a example of plugin project using Webpack and the webpack-chrome-extension-reloader. 4 | 5 | # How it works 6 | First run `yarn sample` on the root of this project, this will trigger the webpack using the configuratuion withim `webpack.plugin.js`. 7 | On chrome extensions, switch to "development mode" and add a "unpacked" extension. The choose the **dist** directory. 8 | Open a new tab on any site (can't be the home page), open the debugger and you're going to see some *log* from the content-script of the plugin plus a message from the extension hot reload server: 9 | ``` 10 | [ WCER: Connected to Chrome Extension Hot Reloader ] 11 | ``` 12 | Change anything inside `plugin-src` and look the page reload it automatically, using the new version of your extension. 13 | Tip: try to change the content of the console log within the `my-content-script`, and see the page reload and show the new result. 14 | 15 | ## Why can't I load plugin-src/ dir? 16 | The source needs to be parsed and bundled by Webpack, then is outputed on the `dist` directory. This means 17 | you can't directly load this directory as a extension. 18 | The source in dist will contain the neccessary data to make the Hot Reloading work properly. 19 | 20 | ## Running Webpack in watch mode 21 | As Chrome is the "server" of our files, we don't need to run any server other than the one created by 22 | the WCER intself. Don't worry, it creates with no, just by using the plugin it will take care for you. 23 | All you need to do is run your webpack with the `--watch` option enabled, and everytime any change happens 24 | Webpack will rebuild all for you, triggering the WCER plugin, signing the extension to be reloaded. 25 | 26 | ## Manifest and Icons 27 | As both manifest.json and icons aren't directly processed on the extension source, it needs to be 28 | copied to the final output directory `dist/`. So, if you check the `webpack.plugin.js` configuration you can 29 | see the [CopyWebpackPlugin](https://github.com/webpack-contrib/copy-webpack-plugin) being used to move both 30 | manifest and icons to the output directory. -------------------------------------------------------------------------------- /specs/ChromeExtensionReloader.specs.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { spy, stub, SinonStub } from "sinon"; 3 | import ChromeExtensionReloaderImpl from "../src/ChromeExtensionReloader"; 4 | import * as webpack from "webpack"; 5 | import { ChromeExtensionReloaderInstance } from "webpack-chrome-extension-reloader"; 6 | 7 | describe("ChromeExtensionReloader", () => { 8 | const envCopy = { ...process.env }; 9 | 10 | const registerStub = stub( 11 | ChromeExtensionReloaderImpl.prototype, 12 | "_registerPlugin" 13 | ).returns(); 14 | const versionCheckSpy = spy( 15 | ChromeExtensionReloaderImpl.prototype._isWebpackGToEV4 16 | ); 17 | 18 | function pluginFactory( 19 | version: string 20 | ): [ChromeExtensionReloaderInstance, SinonStub] { 21 | const webpackStub = stub(webpack, "version").value(version); 22 | const plugin = new ChromeExtensionReloaderImpl(); 23 | return [plugin, webpackStub]; 24 | } 25 | 26 | beforeEach(() => { 27 | registerStub.reset(); 28 | versionCheckSpy.resetHistory(); 29 | process.env = { ...envCopy }; 30 | }); 31 | 32 | describe("When applying plugin, should check if is in development mode", () => { 33 | it("Should check for --mode flag on versions >= 4", () => { 34 | const [plugin, stub] = pluginFactory("4.2.21"); 35 | const mockedCompiler = { options: {} }; 36 | 37 | plugin.apply(mockedCompiler); 38 | assert(registerStub.notCalled); 39 | 40 | mockedCompiler.options.mode = "development"; 41 | plugin.apply(mockedCompiler); 42 | assert(registerStub.calledOnce); 43 | 44 | stub.restore(); 45 | }); 46 | 47 | it("Should check for NODE_ENV variable on versions < 4", () => { 48 | delete process.env.NODE_ENV; 49 | const [plugin, stub] = pluginFactory("3.1.0"); 50 | const mockedCompiler = { options: {} }; 51 | plugin.apply(mockedCompiler); 52 | 53 | assert(registerStub.notCalled); 54 | 55 | process.env.NODE_ENV = "development"; 56 | 57 | plugin.apply(mockedCompiler); 58 | assert(registerStub.calledOnce); 59 | 60 | stub.restore(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { BannerPlugin } = require("webpack"); 3 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 4 | const pack = require("./package.json"); 5 | const { isDevelopment, isProduction, test } = require("./src/utils/env"); 6 | 7 | const mode = isDevelopment ? "development" : "production"; 8 | 9 | module.exports = (env = { analyze: false }) => ({ 10 | mode, 11 | target: "node", 12 | entry: test({ tests: "./specs/index.ts" }) || { 13 | "webpack-chrome-extension-reloader": "./src/index.ts", 14 | wcer: "./client/index.ts" 15 | }, 16 | devtool: "inline-source-map", 17 | output: { 18 | publicPath: ".", 19 | path: path.resolve(__dirname, "./dist"), 20 | filename: "[name].js", 21 | libraryTarget: "umd" 22 | }, 23 | plugins: [ 24 | env.analyze && isProduction(new BundleAnalyzerPlugin({ sourceMap: true })), 25 | new BannerPlugin({ 26 | banner: "#!/usr/bin/env node", 27 | raw: true, 28 | entryOnly: true, 29 | include: "wcer" 30 | }), 31 | new BannerPlugin({ 32 | banner: '/// ', 33 | raw: true, 34 | entryOnly: true, 35 | include: 'webpack-chrome-extension-reloader' 36 | }) 37 | ].filter((plugin) => !!plugin), 38 | externals: [ 39 | ...Object.keys(pack.dependencies), 40 | "webpack", 41 | "webpack-chrome-extension-reloader" 42 | ], 43 | resolve: { 44 | modules: [path.resolve(__dirname, "src"), "node_modules"], 45 | mainFiles: ["index"], 46 | extensions: [".ts", ".tsx", ".js"] 47 | }, 48 | optimization: { 49 | minimize: false, 50 | nodeEnv: false 51 | }, 52 | module: { 53 | rules: [ 54 | { 55 | test: /\.ts$/, 56 | enforce: "pre", 57 | use: [ 58 | { 59 | loader: "tslint-loader", 60 | options: { 61 | configFile: "./tslint.json" 62 | } 63 | } 64 | ] 65 | }, 66 | { 67 | test: /\.jsx?$/, 68 | exclude: /node_modules/, 69 | loaders: ["babel-loader"] 70 | }, 71 | { 72 | test: /\.tsx?$/, 73 | exclude: /node_modules/, 74 | loaders: ["babel-loader", "ts-loader"] 75 | }, 76 | { 77 | test: /\.json$/, 78 | exclude: /node_modules/, 79 | loaders: ["json-loader"] 80 | }, 81 | { 82 | test: /\.txt$/, 83 | exclude: /node_modules/, 84 | loaders: ["raw-loader"] 85 | } 86 | ] 87 | } 88 | }); 89 | -------------------------------------------------------------------------------- /specs/SignEmitter.specs.ts: -------------------------------------------------------------------------------- 1 | import SignEmitter from "../src/hot-reload/SignEmitter"; 2 | import { assert } from "chai"; 3 | import { spy, SinonSpy } from "sinon"; 4 | import * as blockProtection from "../src/utils/block-protection"; 5 | import * as logger from "../src/utils/logger"; 6 | 7 | import { 8 | FAST_RELOAD_DEBOUNCING_FRAME, 9 | FAST_RELOAD_CALLS, 10 | FAST_RELOAD_WAIT, 11 | NEW_FAST_RELOAD_CHROME_VERSION, 12 | NEW_FAST_RELOAD_DEBOUNCING_FRAME, 13 | NEW_FAST_RELOAD_CALLS 14 | } from "../src/constants/fast-reloading.constants"; 15 | import { browserVerWrongFormatMsg } from "../src/messages/warnings"; 16 | 17 | describe("SignEmitter", () => { 18 | let mockedServer: any; 19 | let debouncerSpy: SinonSpy; 20 | let warnSpy: SinonSpy; 21 | let fastReloadBlockerSpy: SinonSpy; 22 | 23 | beforeEach(() => { 24 | mockedServer = { 25 | clients: [] 26 | }; 27 | debouncerSpy = spy(blockProtection, "debounceSignal"); 28 | warnSpy = spy(logger, "warn"); 29 | fastReloadBlockerSpy = spy(blockProtection, "fastReloadBlocker"); 30 | }); 31 | afterEach(() => { 32 | debouncerSpy.restore(); 33 | fastReloadBlockerSpy.restore(); 34 | warnSpy.restore(); 35 | }); 36 | 37 | it("Should setup signal debouncer as fast reload blocker to avoid extension blocking", () => { 38 | const emitter = new SignEmitter(mockedServer, "0.0.0.0"); 39 | 40 | assert(debouncerSpy.calledWith(FAST_RELOAD_DEBOUNCING_FRAME)); 41 | assert( 42 | fastReloadBlockerSpy.calledWith(FAST_RELOAD_CALLS, FAST_RELOAD_WAIT) 43 | ); 44 | }); 45 | 46 | it(`Should assign new rules if the Chrome/Chromium version is >= ${ 47 | NEW_FAST_RELOAD_CHROME_VERSION 48 | }`, () => { 49 | const emitter = new SignEmitter( 50 | mockedServer, 51 | NEW_FAST_RELOAD_CHROME_VERSION 52 | ); 53 | 54 | assert(debouncerSpy.calledWith(NEW_FAST_RELOAD_DEBOUNCING_FRAME)); 55 | assert( 56 | fastReloadBlockerSpy.calledWith(NEW_FAST_RELOAD_CALLS, FAST_RELOAD_WAIT) 57 | ); 58 | }); 59 | 60 | it("Should fallback into debounce mode and warn user when isn't possible to identify the browser version", () => { 61 | const browserVer = ""; 62 | const emitter = new SignEmitter(mockedServer, browserVer); 63 | 64 | assert(debouncerSpy.calledWith(FAST_RELOAD_DEBOUNCING_FRAME)); 65 | assert( 66 | fastReloadBlockerSpy.calledWith(FAST_RELOAD_CALLS, FAST_RELOAD_WAIT) 67 | ); 68 | 69 | assert( 70 | warnSpy.calledWith(browserVerWrongFormatMsg.get({ version: browserVer })) 71 | ); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/hot-reload/SignEmitter.ts: -------------------------------------------------------------------------------- 1 | import { Server, OPEN } from "ws"; 2 | import { zip } from "lodash"; 3 | 4 | import { 5 | FAST_RELOAD_DEBOUNCING_FRAME, 6 | FAST_RELOAD_CALLS, 7 | FAST_RELOAD_WAIT, 8 | NEW_FAST_RELOAD_CHROME_VERSION, 9 | NEW_FAST_RELOAD_DEBOUNCING_FRAME, 10 | NEW_FAST_RELOAD_CALLS 11 | } from "../constants/fast-reloading.constants"; 12 | import { signChange } from "../utils/signals"; 13 | import { debounceSignal, fastReloadBlocker } from "../utils/block-protection"; 14 | import { warn } from "../utils/logger"; 15 | import { browserVerWrongFormatMsg } from "../messages/warnings"; 16 | 17 | export default class SignEmitter { 18 | private _safeSignChange: Function; 19 | private _server: Server; 20 | 21 | constructor(server: Server, browserVersion: string = "0.0.0.0") { 22 | this._server = server; 23 | const [reloadCalls, reloadDeboucingFrame] = this._satisfies( 24 | browserVersion, 25 | NEW_FAST_RELOAD_CHROME_VERSION 26 | ) 27 | ? [NEW_FAST_RELOAD_CALLS, NEW_FAST_RELOAD_DEBOUNCING_FRAME] 28 | : [FAST_RELOAD_CALLS, FAST_RELOAD_DEBOUNCING_FRAME]; 29 | 30 | const debouncer = debounceSignal(reloadDeboucingFrame, this); 31 | const blocker = fastReloadBlocker(reloadCalls, FAST_RELOAD_WAIT, this); 32 | this._safeSignChange = debouncer(blocker(this._setupSafeSignChange())); 33 | } 34 | 35 | safeSignChange(reloadPage: boolean): Promise { 36 | return new Promise((res, rej) => { 37 | this._safeSignChange(reloadPage, res, rej); 38 | }); 39 | } 40 | 41 | private _setupSafeSignChange() { 42 | return (reloadPage: boolean, onSuccess: Function, onError: Function) => { 43 | try { 44 | this._sendMsg(signChange({ reloadPage })); 45 | onSuccess(); 46 | } catch (err) { 47 | onError(err); 48 | } 49 | }; 50 | } 51 | 52 | private _sendMsg(msg: any) { 53 | this._server.clients.forEach(client => { 54 | if (client.readyState === OPEN) { 55 | client.send(JSON.stringify(msg)); 56 | } 57 | }); 58 | } 59 | 60 | private _satisfies(browserVersion: string, targetVersion: string) { 61 | if (!/\d+\.\d+\.\d+\.\d+/.test(browserVersion)) { 62 | warn(browserVerWrongFormatMsg.get({ version: browserVersion })); 63 | return false; 64 | } 65 | 66 | const versionPairs: Array = zip( 67 | browserVersion.split(".").map(n => parseInt(n)), 68 | targetVersion.split(".").map(n => parseInt(n)) 69 | ); 70 | 71 | for (let [version = 0, target = 0] of versionPairs) { 72 | if (version !== target) return version > target; 73 | } 74 | return true; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ChromeExtensionReloader.ts: -------------------------------------------------------------------------------- 1 | import { merge } from "lodash"; 2 | import AbstractChromePluginReloader from "./webpack/AbstractChromeExtensionReloader"; 3 | import { middlewareInjector } from "./middleware"; 4 | import { changesTriggerer } from "./hot-reload"; 5 | import defaultOptions from "./utils/default-options"; 6 | import CompilerEventsFacade from "./webpack/CompilerEventsFacade"; 7 | import { onlyOnDevelopmentMsg } from "./messages/warnings"; 8 | import { bgScriptRequiredMsg } from "./messages/errors"; 9 | import { warn } from "./utils/logger"; 10 | import { Compiler, version } from "webpack"; 11 | 12 | import { 13 | ChromeExtensionReloaderInstance, 14 | PluginOptions, 15 | EntriesOption 16 | } from "webpack-chrome-extension-reloader"; 17 | 18 | export default class ChromeExtensionReloaderImpl extends AbstractChromePluginReloader 19 | implements ChromeExtensionReloaderInstance { 20 | private _opts?: PluginOptions; 21 | constructor(options?: PluginOptions) { 22 | super(); 23 | this._opts = options; 24 | this._chunkVersions = {}; 25 | } 26 | 27 | _isWebpackGToEV4() { 28 | if (version) { 29 | const [major] = version.split("."); 30 | if (parseInt(major) >= 4) return true; 31 | } 32 | return false; 33 | } 34 | 35 | _contentOrBgChanged( 36 | chunks: WebpackChunk[], 37 | { background, contentScript }: EntriesOption 38 | ) { 39 | return chunks 40 | .filter(({ name, hash }) => { 41 | const oldVersion = this._chunkVersions[name]; 42 | this._chunkVersions[name] = hash; 43 | return hash !== oldVersion; 44 | }) 45 | .some(({ name }) => name === background || name === contentScript); 46 | } 47 | 48 | _registerPlugin(compiler: Compiler) { 49 | const { reloadPage, port, entries } = merge(defaultOptions, this._opts); 50 | 51 | this._eventAPI = new CompilerEventsFacade(compiler); 52 | this._injector = middlewareInjector(entries, { port, reloadPage }); 53 | this._triggerer = changesTriggerer(port, reloadPage); 54 | this._eventAPI.afterOptimizeChunkAssets((comp, chunks) => { 55 | if (!compiler.options.entry || !compiler.options.entry["background"]) { 56 | throw new TypeError(bgScriptRequiredMsg.get()); 57 | } 58 | comp.assets = { 59 | ...comp.assets, 60 | ...this._injector(comp.assets, chunks) 61 | }; 62 | }); 63 | this._eventAPI.afterEmit((comp, done) => { 64 | if (this._contentOrBgChanged(comp.chunks, entries)) { 65 | this._triggerer() 66 | .then(done) 67 | .catch(done); 68 | } 69 | }); 70 | } 71 | 72 | apply(compiler: Compiler) { 73 | if ( 74 | (this._isWebpackGToEV4() 75 | ? compiler.options.mode 76 | : process.env.NODE_ENV) === "development" 77 | ) { 78 | this._registerPlugin(compiler); 79 | } else { 80 | warn(onlyOnDevelopmentMsg.get()); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-chrome-extension-reloader", 3 | "version": "1.3.0", 4 | "private": false, 5 | "description": "Watch for changes and force the reload of the chrome extension", 6 | "main": "dist/webpack-chrome-extension-reloader.js", 7 | "bin": { 8 | "wcer": "./dist/wcer.js" 9 | }, 10 | "types": "typings/webpack-chrome-extension-reloader.d.ts", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/rubenspgcavalcante/webpack-chrome-extension-reloader.git" 14 | }, 15 | "husky": { 16 | "hooks": { 17 | "pre-commit": "yarn format", 18 | "pre-push": "yarn test" 19 | } 20 | }, 21 | "scripts": { 22 | "build": "NODE_ENV=production webpack", 23 | "test": "NODE_ENV=test webpack && mocha dist/tests.js", 24 | "watch": "NODE_ENV=development webpack --watch", 25 | "analyze": "NODE_ENV=production webpack --env.analyze", 26 | "sample": "NODE_ENV=development webpack --config sample/webpack.plugin.js --watch", 27 | "prepublish": "yarn build", 28 | "format": "prettier --write \"{src,client}/**/*.ts\"" 29 | }, 30 | "author": "Rubens Pinheiro Gonçalves Cavalcante", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/rubenspgcavalcante/webpack-chrome-extension-reloader/issues" 34 | }, 35 | "homepage": "https://github.com/rubenspgcavalcante/webpack-chrome-extension-reloader#readme", 36 | "keywords": [ 37 | "webpack", 38 | "plugin", 39 | "chrome", 40 | "extension", 41 | "hot-reload" 42 | ], 43 | "dependencies": { 44 | "colors": "^1.1.2", 45 | "lodash": "^4.17.4", 46 | "minimist": "^1.2.0", 47 | "webpack-sources": "^1.0.1", 48 | "ws": "^7.0.0" 49 | }, 50 | "peerDependencies": { 51 | "webpack": ">=2 <5" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.0.0", 55 | "@babel/plugin-proposal-class-properties": "^7.0.0", 56 | "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", 57 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 58 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 59 | "@babel/plugin-transform-regenerator": "^7.0.0", 60 | "@babel/polyfill": "^7.0.0", 61 | "@babel/preset-env": "^7.0.0", 62 | "@types/chai": "^4.0.0", 63 | "@types/chrome": "^0.0.83", 64 | "@types/colors": "^1.1.3", 65 | "@types/lodash": "^4.14.120", 66 | "@types/minimist": "^1.2.0", 67 | "@types/mocha": "^5.2.5", 68 | "@types/sinon": "^7.0.0", 69 | "@types/webpack": "^4.4.27", 70 | "@types/ws": "^6.0.1", 71 | "autoprefixer": "^9.3.1", 72 | "babel-loader": "^8.0.0", 73 | "chai": "^4.0.2", 74 | "copy-webpack-plugin": "^5.0.0", 75 | "css-loader": "^2.0.0", 76 | "husky": "^2.0.0", 77 | "json-loader": "^0.5.4", 78 | "memory-fs": "^0.4.1", 79 | "mini-css-extract-plugin": "^0.6.0", 80 | "mocha": "^6.1.1", 81 | "prettier": "^1.8.2", 82 | "raw-loader": "^2.0.0", 83 | "sinon": "^7.1.1", 84 | "source-map-support": "^0.5.9", 85 | "style-loader": "^0.23.1", 86 | "ts-loader": "^5.3.1", 87 | "tslint": "^5.11.0", 88 | "tslint-config-prettier": "^1.17.0", 89 | "tslint-loader": "^3.6.0", 90 | "typescript": "^3.2.1", 91 | "webpack": "^4.26.1", 92 | "webpack-bundle-analyzer": "^3.0.3", 93 | "webpack-cli": "^3.1.2" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/middleware/wcer-middleware.raw.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------- */ 2 | /* Start of Webpack Chrome Hot Extension Middleware */ 3 | /* ================================================== */ 4 | /* This will be converted into a lodash templ., any */ 5 | /* external argument must be provided using it */ 6 | /* -------------------------------------------------- */ 7 | (function(chrome, window) { 8 | const signals: any = JSON.parse('<%= signals %>'); 9 | const config: any = JSON.parse('<%= config %>'); 10 | 11 | const reloadPage: boolean = <"true" | "false">"<%= reloadPage %>" === "true"; 12 | const wsHost = "<%= WSHost %>"; 13 | const { 14 | SIGN_CHANGE, 15 | SIGN_RELOAD, 16 | SIGN_RELOADED, 17 | SIGN_LOG, 18 | SIGN_CONNECT 19 | } = signals; 20 | const { RECONNECT_INTERVAL, SOCKET_ERR_CODE_REF } = config; 21 | 22 | const { runtime, tabs } = chrome; 23 | const manifest = runtime.getManifest(); 24 | 25 | // =============================== Helper functions ======================================= // 26 | const formatter = (msg: string) => `[ WCER: ${msg} ]`; 27 | const logger = (msg, level = "info") => console[level](formatter(msg)); 28 | const timeFormatter = (date: Date) => 29 | date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1"); 30 | 31 | // ========================== Called only on content scripts ============================== // 32 | function contentScriptWorker() { 33 | runtime.sendMessage({ type: SIGN_CONNECT }, msg => console.info(msg)); 34 | 35 | runtime.onMessage.addListener(({ type, payload }: Action) => { 36 | switch (type) { 37 | case SIGN_RELOAD: 38 | logger("Detected Changes. Reloading ..."); 39 | reloadPage && window.location.reload(); 40 | break; 41 | 42 | case SIGN_LOG: 43 | console.info(payload); 44 | break; 45 | } 46 | }); 47 | } 48 | 49 | // ======================== Called only on background scripts ============================= // 50 | function backgroundWorker(socket: WebSocket) { 51 | runtime.onMessage.addListener((action: Action, sender, sendResponse) => { 52 | if (action.type === SIGN_CONNECT) { 53 | sendResponse(formatter("Connected to Chrome Extension Hot Reloader")); 54 | } 55 | }); 56 | 57 | socket.addEventListener("message", ({ data }: MessageEvent) => { 58 | const { type, payload } = JSON.parse(data); 59 | 60 | if (type === SIGN_CHANGE) { 61 | tabs.query({ status: "complete" }, loadedTabs => { 62 | loadedTabs.forEach( 63 | tab => tab.id && tabs.sendMessage(tab.id, { type: SIGN_RELOAD }) 64 | ); 65 | socket.send( 66 | JSON.stringify({ 67 | type: SIGN_RELOADED, 68 | payload: formatter( 69 | `${timeFormatter(new Date())} - ${ 70 | manifest.name 71 | } successfully reloaded` 72 | ) 73 | }) 74 | ); 75 | runtime.reload(); 76 | }); 77 | } else { 78 | runtime.sendMessage({ type, payload }); 79 | } 80 | }); 81 | 82 | socket.addEventListener("close", ({ code }: CloseEvent) => { 83 | logger( 84 | `Socket connection closed. Code ${code}. See more in ${ 85 | SOCKET_ERR_CODE_REF 86 | }`, 87 | "warn" 88 | ); 89 | 90 | const intId = setInterval(() => { 91 | logger("Attempting to reconnect (tip: Check if Webpack is running)"); 92 | const ws = new WebSocket(wsHost); 93 | ws.addEventListener("open", () => { 94 | clearInterval(intId); 95 | logger("Reconnected. Reloading plugin"); 96 | runtime.reload(); 97 | }); 98 | }, RECONNECT_INTERVAL); 99 | }); 100 | } 101 | 102 | // ======================= Bootstraps the middleware =========================== // 103 | runtime.reload 104 | ? backgroundWorker(new WebSocket(wsHost)) 105 | : contentScriptWorker(); 106 | })(chrome, window); 107 | 108 | /* ----------------------------------------------- */ 109 | /* End of Webpack Chrome Hot Extension Middleware */ 110 | /* ----------------------------------------------- */ 111 | -------------------------------------------------------------------------------- /specs/middleware-injector.specs.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { stub } from "sinon"; 3 | 4 | import middlewareInjector from "../src/middleware/middleware-injector"; 5 | import * as middlewareSourceBuilder from "../src/middleware/middleware-source-builder"; 6 | import { EntriesOption } from "webpack-chrome-extension-reloader"; 7 | 8 | describe("middleware-injector", () => { 9 | let assetsBuilder, singleContentChunks, multipleContentsChunks; 10 | const sourceCode = "console.log('I am a middleware!!!');"; 11 | 12 | stub(middlewareSourceBuilder, "default").callsFake( 13 | opts => sourceCode 14 | ); 15 | 16 | const sourceFactory = stub().callsFake((toConcat: string, file) => ({ 17 | source: () => toConcat + file.source() 18 | })); 19 | 20 | const entriesInfo = { 21 | background: { name: "bgChunkName", path: "./path/to/bg-script.js" }, 22 | contentScript: { 23 | name: "contentChunkName", 24 | path: "./path/to/content-script.js" 25 | }, 26 | extraContentScript: { 27 | name: "extraContentChunkName", 28 | path: "./path/to/extra-content-script.js" 29 | } 30 | }; 31 | 32 | const templateOpts = { port: 1234, reloadPage: true }; 33 | 34 | const options: EntriesOption = { 35 | background: entriesInfo.background.name, 36 | contentScript: entriesInfo.contentScript.name 37 | }; 38 | 39 | const options2: EntriesOption = { 40 | background: entriesInfo.background.name, 41 | contentScript: [ 42 | entriesInfo.contentScript.name, 43 | entriesInfo.extraContentScript.name 44 | ] 45 | }; 46 | 47 | const fakeCssPath = "./path/to/some.css"; 48 | const fakeImgPath = "./path/to/a/random-image.png"; 49 | 50 | const assets = { 51 | [entriesInfo.background.path]: { source: () => "const bg = true;" }, 52 | [entriesInfo.contentScript.path]: { source: () => "const cs = true;" }, 53 | [entriesInfo.extraContentScript.path]: { 54 | source: () => "const extraCs = true;" 55 | }, 56 | [fakeCssPath]: { source: () => "some-css-source" }, 57 | [fakeImgPath]: { source: () => "some-base64-source" } 58 | }; 59 | 60 | beforeEach(() => { 61 | singleContentChunks = [ 62 | { name: options.background, files: [entriesInfo.background.path] }, 63 | { 64 | name: options.contentScript, 65 | files: [entriesInfo.contentScript.path, fakeCssPath] 66 | }, 67 | { name: "someOtherAsset", files: [fakeImgPath] } 68 | ]; 69 | 70 | multipleContentsChunks = [ 71 | { name: options2.background, files: [entriesInfo.background.path] }, 72 | { 73 | name: options2.contentScript, 74 | files: [entriesInfo.contentScript.path, fakeCssPath] 75 | }, 76 | { 77 | name: options2.contentScript[0], 78 | files: [entriesInfo.contentScript.path, fakeCssPath] 79 | }, 80 | { 81 | name: options2.contentScript[1], 82 | files: [entriesInfo.extraContentScript.path] 83 | }, 84 | { name: "someOtherAsset", files: [fakeImgPath] } 85 | ]; 86 | }); 87 | 88 | describe("Injecting middleware into background and content script entries", () => { 89 | let assetsSingleContent, assetsMultiContent; 90 | beforeEach(() => { 91 | assetsBuilder = middlewareInjector(options, templateOpts); 92 | assetsSingleContent = assetsBuilder(assets, singleContentChunks); 93 | 94 | assetsBuilder = middlewareInjector(options2, templateOpts); 95 | assetsMultiContent = assetsBuilder(assets, multipleContentsChunks); 96 | }); 97 | 98 | it("Should inject into the background script", () => { 99 | const newBgSource = assetsSingleContent[ 100 | entriesInfo.background.path 101 | ].source(); 102 | const oldBgSource = assets[entriesInfo.background.path].source(); 103 | assert.equal(newBgSource, sourceCode + oldBgSource); 104 | }); 105 | 106 | it("Should inject into the a single contentScript", () => { 107 | const newContentSource = assetsSingleContent[ 108 | entriesInfo.contentScript.path 109 | ].source(); 110 | const oldContentSource = assets[entriesInfo.contentScript.path].source(); 111 | assert.equal(newContentSource, sourceCode + oldContentSource); 112 | }); 113 | 114 | it("Should inject into the multiple contentScripts", () => { 115 | const newFirstContentSource = assetsMultiContent[ 116 | entriesInfo.contentScript.path 117 | ].source(); 118 | const oldFirstContentSource = assets[ 119 | entriesInfo.contentScript.path 120 | ].source(); 121 | assert.equal(newFirstContentSource, sourceCode + oldFirstContentSource); 122 | 123 | const newSecondContentSource = assetsMultiContent[ 124 | entriesInfo.extraContentScript.path 125 | ].source(); 126 | const oldSecondContentSource = assets[ 127 | entriesInfo.extraContentScript.path 128 | ].source(); 129 | assert.equal(newSecondContentSource, sourceCode + oldSecondContentSource); 130 | }); 131 | }); 132 | 133 | it("Should return only changed assets", () => { 134 | assetsBuilder = middlewareInjector(options, templateOpts); 135 | const newAssets = assetsBuilder(assets, singleContentChunks, sourceFactory); 136 | 137 | assert.notOk(newAssets.hasOwnProperty(fakeCssPath)); 138 | assert.notOk(newAssets.hasOwnProperty(fakeImgPath)); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Disclaimer 2 | This repository and package are archived, for now on it's recommended to use [webpack-extension-reloader](https://github.com/rubenspgcavalcante/webpack-extension-reloader) 3 | 4 | # Webpack Chrome Extension Reloader 5 | A Webpack plugin to enable hot reloading while developing Chrome extensions. 6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | [![npm version](https://badge.fury.io/js/webpack-chrome-extension-reloader.svg)](https://badge.fury.io/js/webpack-chrome-extension-reloader) 16 | [![Build Status](https://travis-ci.org/rubenspgcavalcante/webpack-chrome-extension-reloader.svg?branch=master)](https://travis-ci.org/rubenspgcavalcante/webpack-chrome-extension-reloader) 17 | [![NPM Downloads](https://img.shields.io/npm/dt/webpack-chrome-extension-reloader.svg)](https://www.npmjs.com/package/webpack-chrome-extension-reloader) 18 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b93aa8303bfb44a2a621cac57639ca26)](https://www.codacy.com/app/rubenspgcavalcante/webpack-chrome-extension-reloader?utm_source=github.com&utm_medium=referral&utm_content=rubenspgcavalcante/webpack-chrome-extension-reloader&utm_campaign=Badge_Grade) [![Greenkeeper badge](https://badges.greenkeeper.io/rubenspgcavalcante/webpack-chrome-extension-reloader.svg)](https://greenkeeper.io/) 19 | 20 | ## Installing 21 | 22 | npm 23 | ``` 24 | npm install webpack-chrome-extension-reloader --save-dev 25 | ``` 26 | 27 | yarn 28 | ``` 29 | yarn add webpack-chrome-extension-reloader --dev 30 | ``` 31 | 32 | ## Solution for ... 33 | Have your ever being annoyed while developing a Google Chrome extension, and being unable to use 34 | webpack-hot-server because it's not a web app but a browser extension? 35 | 36 | Well, now you can do hot reloading! 37 | 38 | ![](.github/sample-gif.gif) 39 | 40 | ## What it does? 41 | Basically something similar to what the webpack hot reload middleware does. When you change the code and the webpack 42 | trigger and finish the compilation, your extension is notified and then reloaded using the chrome.runtime API. Check out 43 | [Hot reloading Chrome extensions using Webpack](https://medium.com/front-end-hacking/hot-reloading-extensions-using-webpack-cdfa0e4d5a08) for more background. 44 | 45 | ## How to use 46 | ### Using as a plugin 47 | Add `webpack-chrome-extension-reloader` to the plugins section of your webpack configuration file. 48 | ```js 49 | const ChromeExtensionReloader = require('webpack-chrome-extension-reloader'); 50 | 51 | plugins: [ 52 | new ChromeExtensionReloader() 53 | ] 54 | ``` 55 | 56 | You can also set some options (the following are the default ones): 57 | ```js 58 | // webpack.dev.js 59 | module.exports = { 60 | mode: "development", // The plugin is activated only if mode is set to development 61 | watch: true, 62 | entry: { 63 | 'content-script': './my-content-script.js', 64 | background: './my-background-script.js' 65 | }, 66 | //... 67 | plugins: [ 68 | new ChromeExtensionReloader({ 69 | port: 9090, // Which port use to create the server 70 | reloadPage: true, // Force the reload of the page also 71 | entries: { // The entries used for the content/background scripts 72 | contentScript: 'content-script', // Use the entry names, not the file name or the path 73 | background: 'background' // *REQUIRED 74 | } 75 | }) 76 | ] 77 | } 78 | ``` 79 | 80 | And then just run your application with Webpack in watch mode: 81 | ```bash 82 | NODE_ENV=development webpack --config myconfig.js --mode=development --watch 83 | ``` 84 | 85 | **Important**: You need to set `--mode=development` to activate the plugin (only if you didn't set on the webpack.config.js already) then you need to run with `--watch`, as the plugin will be able to sign the extension only if webpack triggers the rebuild (again, only if you didn't set on webpack.config). 86 | 87 | ### Multiple Content Script support 88 | If you use more than one content script in your extension, like: 89 | ```js 90 | entry: { 91 | 'my-first-content-script': './my-first-content-script.js', 92 | 'my-second-content-script': './my-second-content-script.js', 93 | // and so on ... 94 | background: './my-background-script.js' 95 | } 96 | ``` 97 | 98 | You can use the `entries.contentScript` options as an array: 99 | ```js 100 | plugins: [ 101 | new ChromeExtensionReloader({ 102 | entries: { 103 | contentScript: ['my-first-content-script', 'my-second-content-script', /* and so on ... */], 104 | background: 'background' 105 | } 106 | }) 107 | ] 108 | ``` 109 | 110 | ### CLI 111 | If you don't want all the plugin setup, you can just use the client that comes with the package. 112 | You can use by intalling the package globably, or directly using `npx` after installing locally the plugin: 113 | 114 | ```bash 115 | npx wcer 116 | ``` 117 | If you run directly, it will use the default configurations, but if you want to customize 118 | you can call it with the following options: 119 | ```bash 120 | npx wcer --config wb.config.js --port 9080 --no-page-reload --content-script my-content.js --background bg.js 121 | ``` 122 | If you have **multiple** content scripts, just use comma (with no spaces) while passing the option 123 | ```bash 124 | npx wcer --content-script my-first-content.js,my-second-content.js,my-third-content.js 125 | ``` 126 | 127 | ### Client options 128 | 129 | | name | default | description | 130 | | ---------------- | ----------------- | ----------------------------------------------------------------- | 131 | | --help | | Shows this help | 132 | | --config | webpack.config.js | The webpack configuration file path | 133 | | --port | 9090 | The port to run the server | 134 | | --content-script | content-script | The **entry/entries** name(s) for the content script(s) | 135 | | --background | background | The **entry** name for the background script | 136 | | --no-page-reload | | Disable the auto reloading of all **pages** which runs the plugin | 137 | 138 | Every time webpack triggers a compilation, the extension reloader are going to do the hot reload :) 139 | **Note:** the plugin only works on **development** mode, so don't forget to set the NODE_ENV before run the command above 140 | 141 | ### Contributing 142 | Please before opening any **issue** or **pull request** check the [contribution guide](/.github/CONTRIBUTING.MD). 143 | 144 | ### Building and Testing 145 | Inside this repository have an example plugin, so you can test and see it working 146 | After clone the repo, run: 147 | ``` 148 | yarn build 149 | ``` 150 | 151 | And then run: 152 | ``` 153 | yarn sample 154 | ``` 155 | 156 | This will make the webpack run in watch mode for the sample plugin source and output the built files on the "dist" 157 | directory. 158 | Load the extension **(the files in "sample/dist" directory)** in Chrome using the "load unpacked extension", open a 159 | new tab in any site and open the developer panel on it. Watch the dev. tools console tab, and do some changes on 160 | the background or content script. Voila! 161 | 162 | **Note:** 163 | You must have both background and content scripts for this plugin to work, and they must be specified in separate `entry` chunks 164 | in your webpack config. 165 | 166 | The reloading script will be injected only on the main entries chunks (in background and content script). Any other 167 | chunks will be ignored. 168 | 169 | ### License 170 | This project is under the [MIT LICENSE](http://opensource.org/licenses/MIT) 171 | --------------------------------------------------------------------------------