├── .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 | Click here!
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 |
14 |
15 | [](https://badge.fury.io/js/webpack-chrome-extension-reloader)
16 | [](https://travis-ci.org/rubenspgcavalcante/webpack-chrome-extension-reloader)
17 | [](https://www.npmjs.com/package/webpack-chrome-extension-reloader)
18 | [](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) [](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 | 
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 |
--------------------------------------------------------------------------------