├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── LICENSE.md
├── Makefile
├── README.md
├── assets
├── chrome-store-banner.png
├── chrome-store-banner.sketch
├── demo.gif
└── logo.png
├── package.json
├── src
├── background.html
├── css
│ ├── inject.css
│ └── popup.css
├── img
│ ├── icon-128.png
│ ├── icon-16.png
│ └── icon-48.png
├── js
│ ├── background.js
│ ├── constants.js
│ ├── content.js
│ ├── hot-reload.js
│ ├── message_types.js
│ ├── popup.js
│ ├── popup
│ │ ├── search_bar.js
│ │ └── timeout.js
│ └── types.js
├── manifest.json
└── popup.html
├── utils
├── build.js
├── env.js
└── webserver.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/react"],
3 | "plugins": ["react-hot-loader/babel"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["plugin:prettier/recommended"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 |
5 | build/
6 | dev/
7 |
8 | *.zip
9 | *.crx
10 | *.pem
11 | update.xml
12 |
13 | .vim
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "jsxBracketSameLine": true,
4 | "printWidth": 80,
5 | "singleQuote": true,
6 | "trailingComma": "none",
7 | "tabWidth": 2,
8 | "useTabs": false
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all build
2 |
3 | PACKAGE_VERSION:=$(shell jq -r .version < package.json)
4 |
5 | fmt:
6 | prettier --write .
7 |
8 | check:
9 | prettier --check .
10 |
11 | develop:
12 | yarn run start
13 |
14 | build:
15 | yarn install
16 | NODE_ENV=production yarn run build
17 | mkdir -p dist
18 | cd build && zip -r ../dist/shift-ctrl-f-$(PACKAGE_VERSION).zip .
19 |
20 | all:
21 | build
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shift-Ctrl-F: Semantic Search for the Browser
2 |
3 | 
4 |
5 | [](https://opensource.org/licenses/Apache-2.0)
6 |
7 | [](https://chrome.google.com/webstore/detail/shift-ctrl-f-semantic-sea/alnafbdjkoiljlomnbcknehahbjoiped)
8 |
9 | Search the information available on a webpage using
10 | natural language instead of an exact string match. Uses
11 | [MobileBERT](https://arxiv.org/abs/2004.02984)
12 | fine-tuned on
13 | [SQuAD](https://rajpurkar.github.io/SQuAD-explorer/)
14 | via [TensorFlowJS](https://www.tensorflow.org/js) to
15 | search for answers and mark relevant elements on the web page.
16 |
17 | 
18 |
19 | **This extension is an experiment.** Deep learning models like BERT are powerful
20 | but may return unpredictable and/or biased results that are tough to interpret.
21 | Please apply best judgement when analyzing search results.
22 |
23 | ### Why?
24 |
25 | Ctrl-F uses exact string-matching to find information within a webpage. String
26 | match is inherently a proxy heuristic for the true content -- in most cases it
27 | works very well, but in some cases it can be a bad proxy.
28 |
29 | In our example above we search
30 | [https://stripe.com/docs/testing](https://stripe.com/docs/testing), aiming to
31 | understand the **difference between test mode and live mode**. With string
32 | matching, you might search through some relevant phrases `"live mode"`, `"test
33 | mode"`, and/or `"difference"` and scan through results. With semantic search, you
34 | can directly phrase your question `"What is the difference between live mode
35 | and test mode?"`. We see that the model returns a relevant result, even though
36 | the page does not contain the term "`difference`".
37 |
38 | ### How It Works
39 |
40 | Every time a user executes a search:
41 |
42 | 1. The content script collects all `
`, `
`, and `` elements on the
43 | page and extracts text from each.
44 | 2. The background script executes the question-answering model on every
45 | element, using the query as the question and the element's text as the context.
46 | 3. If a match is returned by the model, it is highlighted within the page along
47 | with the confidence score returned by the model.
48 |
49 | ### Architecture
50 |
51 | There are three main components that interact via [Message
52 | Passing](https://developer.chrome.com/extensions/messaging) to orchestrate the
53 | extension:
54 |
55 | 1. Popup (`popup.js`): React application that renders the search bar, controls
56 | searching and iterating through the results.
57 | 2. Content Script (`content.js`): Runs in the context of the current tab,
58 | responsible for reading from and manipulating the DOM.
59 | 3. Background (`background.js`): Background script that loads and executes the
60 | TensorFlowJS model on question-context pairs.
61 |
62 | `src/js/message_types.js` contains the messages used to interact between these
63 | three components.
64 |
65 | ### Development
66 |
67 | Make sure you have these dependencies installed.
68 |
69 | 1) [Node](https://nodejs.org/en/download/)
70 | 2) [Yarn](https://classic.yarnpkg.com/en/docs/install)
71 | 3) [Prettier](https://prettier.io/docs/en/install.html)
72 |
73 | Then run:
74 |
75 | ```
76 | make develop
77 | ```
78 |
79 | The unpacked extension will be placed inside of `build/`. See [Google Chrome
80 | Extension developer
81 | documentation](https://developer.chrome.com/extensions/getstarted) to load the
82 | unpacked extension into your Chrome browser in development mode.
83 |
84 | ### Publishing
85 |
86 | ```
87 | make build
88 | ```
89 |
90 | A zipped extension file ready for upload will be placed inside of `dist/`.
91 |
--------------------------------------------------------------------------------
/assets/chrome-store-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/assets/chrome-store-banner.png
--------------------------------------------------------------------------------
/assets/chrome-store-banner.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/assets/chrome-store-banner.sketch
--------------------------------------------------------------------------------
/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/assets/demo.gif
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/assets/logo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shift-ctrl-f",
3 | "version": "0.1.0",
4 | "description": "Search the information available on a webpage using natural language.",
5 | "scripts": {
6 | "build": "node utils/build.js",
7 | "start": "node utils/webserver.js"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.11.0",
11 | "@babel/preset-react": "^7.10.4",
12 | "babel-eslint": "^10.1.0",
13 | "babel-loader": "^8.1.0",
14 | "clean-webpack-plugin": "3.0.0",
15 | "copy-webpack-plugin": "5.0.5",
16 | "css-loader": "3.2.0",
17 | "eslint": "^7.6.0",
18 | "eslint-config-prettier": "^6.11.0",
19 | "eslint-loader": "^4.0.2",
20 | "eslint-plugin-prettier": "^3.1.4",
21 | "file-loader": "4.3.0",
22 | "fs-extra": "8.1.0",
23 | "html-loader": "0.5.5",
24 | "html-webpack-plugin": "3.2.0",
25 | "prettier": "^2.0.5",
26 | "react": "^16.13.1",
27 | "react-dom": "^16.13.1",
28 | "react-hot-loader": "^4.12.21",
29 | "style-loader": "1.0.0",
30 | "svg-inline-loader": "^0.8.2",
31 | "webpack": "4.41.2",
32 | "webpack-cli": "3.3.10",
33 | "webpack-dev-server": "3.9.0",
34 | "write-file-webpack-plugin": "4.5.1"
35 | },
36 | "dependencies": {
37 | "@material-ui/core": "^4.11.0",
38 | "@material-ui/icons": "^4.9.1",
39 | "@tensorflow-models/qna": "^1.0.0",
40 | "@tensorflow/tfjs": "1.7.4",
41 | "axios": "^0.19.2",
42 | "jquery": "^3.5.1",
43 | "kind-of": "6.0.3",
44 | "mark.js": "^8.11.1",
45 | "serialize-javascript": "^3.1.0",
46 | "set-value": "^2.0.1",
47 | "websocket-extensions": "0.1.4"
48 | },
49 | "peerDependencies": {}
50 | }
51 |
--------------------------------------------------------------------------------
/src/background.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/css/inject.css:
--------------------------------------------------------------------------------
1 | p[data-ctrlf-selected],
2 | ul[data-ctrlf-selected]
3 | ol[data-ctrlf-selected] {
4 | border-style: dashed;
5 | border-color: black;
6 | border-width: 2px;
7 | }
8 |
9 | .ctrlf-marked {
10 | position: relative;
11 | display: inline;
12 | }
13 |
14 | .ctrlf-marked-score {
15 | position: absolute;
16 | display: inline;
17 | bottom: 100%;
18 | left: 0;
19 |
20 | background-color: yellow;
21 | padding: 2px 4px;
22 | font-size: 12px;
23 | }
24 |
--------------------------------------------------------------------------------
/src/css/popup.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
--------------------------------------------------------------------------------
/src/img/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/src/img/icon-128.png
--------------------------------------------------------------------------------
/src/img/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/src/img/icon-16.png
--------------------------------------------------------------------------------
/src/img/icon-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/model-zoo/shift-ctrl-f/4b742352e61a0a03179ce7c44b2973c662565aef/src/img/icon-48.png
--------------------------------------------------------------------------------
/src/js/background.js:
--------------------------------------------------------------------------------
1 | import './hot-reload';
2 | import '../img/icon-16.png';
3 | import '../img/icon-48.png';
4 | import '../img/icon-128.png';
5 |
6 | import { Component, MessageType } from './message_types';
7 |
8 | const qna = require('@tensorflow-models/qna');
9 |
10 | const sendMessageToContent = (message) => {
11 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
12 | const activeTab = tabs[0];
13 | if (activeTab) {
14 | console.log('Send msg to content:', message);
15 | chrome.tabs.sendMessage(activeTab.id, message);
16 | } else {
17 | console.log('Unable to send msg, no active tab:', message);
18 | }
19 | });
20 | };
21 |
22 | const sendMessageToPopup = (message) => {
23 | console.log('Send msg to popup:', message);
24 | chrome.runtime.sendMessage(message);
25 | };
26 |
27 | qna.load().then((model) => {
28 | window.__qna_model = model;
29 | sendMessageToPopup({
30 | type: MessageType.MODEL_LOADED
31 | });
32 | console.log('Model loaded');
33 | });
34 |
35 | const handleAnswer = (model, msg) => {
36 | model
37 | .findAnswers(msg.question, msg.context)
38 | .then((answers) => {
39 | sendMessageToContent({
40 | type: MessageType.QUESTION_RESULT,
41 | question: msg,
42 | answers: answers
43 | });
44 | })
45 | .catch((error) => {
46 | sendMessageToContent({
47 | type: MessageType.QUESTION_ERROR,
48 | question: msg,
49 | answers: [],
50 | error: error
51 | });
52 | });
53 |
54 | return true;
55 | };
56 |
57 | chrome.runtime.onMessage.addListener((msg, sender, callback) => {
58 | console.log('recieve msg:', msg);
59 | switch (msg.type) {
60 | case MessageType.QUERY:
61 | case MessageType.QUERY_RESULT:
62 | case MessageType.QUERY_ERROR:
63 | case MessageType.QUERY_DONE:
64 | break;
65 |
66 | case MessageType.POPUP_LOADED:
67 | // If model is loaded, respond with a "model loaded"
68 | // message. Otherwise, wait for the model to load.
69 | if (window.__qna_model) {
70 | sendMessageToPopup({
71 | type: MessageType.MODEL_LOADED
72 | });
73 | }
74 | break;
75 |
76 | case MessageType.QUESTION:
77 | if (!window.__qna_model) {
78 | sendMessageToContent({
79 | type: MessageType.QUESTION_ERROR,
80 | question: msg,
81 | answers: [],
82 | error: 'Model not loaded'
83 | });
84 | return true;
85 | } else {
86 | return handleAnswer(window.__qna_model, msg, callback);
87 | }
88 |
89 | default:
90 | console.error('Did not recognize message type: ', msg);
91 | return true;
92 | }
93 | });
94 |
--------------------------------------------------------------------------------
/src/js/constants.js:
--------------------------------------------------------------------------------
1 | // Class names
2 | export const CLASS_NAME_MARKED = 'ctrlf-marked';
3 | export const CLASS_NAME_MARKED_SCORE = 'ctrlf-marked-score';
4 |
5 | // Attribute names
6 | export const DATA_ATTR_ELEMENT_ID = 'data-ctrlf-element-uuid';
7 | export const DATA_ATTR_SUCCESS = 'data-ctrlf-success';
8 | export const DATA_ATTR_SELECTED = 'data-ctrlf-selected';
9 |
10 | export const MIN_TOKENS = 10;
11 |
--------------------------------------------------------------------------------
/src/js/content.js:
--------------------------------------------------------------------------------
1 | import '../css/inject.css';
2 | import Mark from 'mark.js';
3 | import {
4 | DATA_ATTR_ELEMENT_ID,
5 | DATA_ATTR_SELECTED,
6 | DATA_ATTR_SUCCESS,
7 | CLASS_NAME_MARKED,
8 | CLASS_NAME_MARKED_SCORE,
9 | MIN_TOKENS
10 | } from './constants';
11 | import { Component, MessageType } from './message_types';
12 |
13 | import $ from 'jquery';
14 | import { v4 as uuidv4 } from 'uuid';
15 |
16 | const findAllElements = () => {
17 | return $('[' + DATA_ATTR_ELEMENT_ID + ']');
18 | };
19 |
20 | const findElementById = (elementId) => {
21 | const q = $('[' + DATA_ATTR_ELEMENT_ID + '=' + elementId + ']');
22 | return q.length > 0 ? $(q[0]) : null;
23 | };
24 |
25 | const checkIfQueryDone = () => {
26 | const allElements = findAllElements();
27 | const waitingElements = allElements.filter((idx, node) => {
28 | return $(node).attr(DATA_ATTR_SUCCESS) === undefined;
29 | });
30 |
31 | if (waitingElements.length === 0) {
32 | console.log('Query done');
33 | chrome.runtime.sendMessage({
34 | type: MessageType.QUERY_DONE
35 | });
36 | }
37 | };
38 |
39 | // Heuristic to decide whether the element is worth searching.
40 | const searchableElement = (idx, el) => {
41 | const validToken = (token) => {
42 | if (!token) {
43 | return false;
44 | }
45 |
46 | const alphaNum = token.match(/[a-zA-Z0-9]/g);
47 | return alphaNum && alphaNum.length > 0;
48 | };
49 |
50 | // Split by spaces, remove tokens without alphanumeric characters.
51 | const tokens = $(el).text().split(' ').filter(validToken);
52 | return tokens.length > MIN_TOKENS;
53 | };
54 |
55 | const handleQuery = (msg) => {
56 | console.log('Searching query:', msg.query);
57 |
58 | const textElements = $('p,ul,ol');
59 | const searchable = textElements
60 | .filter(searchableElement)
61 | .filter((idx, el) => el.offsetParent !== null);
62 |
63 | console.log('Searching', searchable.length, 'text elements');
64 | if (searchable.length === 0) {
65 | return chrome.runtime.sendMessage({
66 | type: MessageType.QUERY_DONE
67 | });
68 | }
69 |
70 | searchable.each((idx, element) => {
71 | const context = $(element).text().trim();
72 | const elementId = uuidv4();
73 | $(element).attr(DATA_ATTR_ELEMENT_ID, elementId);
74 | chrome.runtime.sendMessage(
75 | {
76 | type: MessageType.QUESTION,
77 | elementId: elementId,
78 | question: msg.query,
79 | context: context
80 | },
81 | handleMsg
82 | );
83 | });
84 | };
85 |
86 | const handleModelSuccess = (msg) => {
87 | // Mark question on dom.
88 | const element = findElementById(msg.question.elementId);
89 | element.attr(DATA_ATTR_SUCCESS, 'true');
90 |
91 | for (const answer of msg.answers) {
92 | chrome.runtime.sendMessage({
93 | type: MessageType.QUERY_RESULT,
94 | answer: answer,
95 | elementId: msg.question.elementId
96 | });
97 | }
98 |
99 | checkIfQueryDone();
100 | };
101 |
102 | const handleModelErr = (msg) => {
103 | console.log('Model error: ', msg);
104 |
105 | // Mark question on dom.
106 | const element = findElementById(msg.question.elementId);
107 | element.attr(DATA_ATTR_SUCCESS, 'false');
108 |
109 | chrome.runtime.sendMessage({
110 | type: MessageType.QUERY_ERROR,
111 | error: msg.error,
112 | elementId: msg.elementId
113 | });
114 |
115 | checkIfQueryDone();
116 | };
117 |
118 | const clearSelection = () => {
119 | // Remove old highlight if it exists.
120 | const oldElement = $('[' + DATA_ATTR_SELECTED + ']');
121 | if (oldElement.length > 0) {
122 | oldElement.removeAttr(DATA_ATTR_SELECTED);
123 | var instance = new Mark(oldElement[0]);
124 | instance.unmark({
125 | done: () => {
126 | $('.' + CLASS_NAME_MARKED_SCORE).remove();
127 | }
128 | });
129 | }
130 | };
131 |
132 | const handleSelection = (msg) => {
133 | clearSelection();
134 |
135 | // Add new highlight;
136 | const element = findElementById(msg.elementId);
137 | element.attr(DATA_ATTR_SELECTED, 'true');
138 | element[0].scrollIntoView({
139 | block: 'end',
140 | inline: 'nearest'
141 | });
142 | var instance = new Mark(element[0]);
143 | instance.mark(msg.answer.text, {
144 | className: CLASS_NAME_MARKED,
145 | acrossElements: true,
146 | separateWordSearch: false,
147 | done: () => {
148 | const scoreEl = $('')
149 | .addClass(CLASS_NAME_MARKED_SCORE)
150 | .text(msg.answer.score.toFixed(4));
151 | $('.' + CLASS_NAME_MARKED)
152 | .first()
153 | .append(scoreEl);
154 | }
155 | });
156 | };
157 |
158 | const handleClear = () => {
159 | clearSelection();
160 | findAllElements()
161 | .removeAttr(DATA_ATTR_SUCCESS)
162 | .removeAttr(DATA_ATTR_ELEMENT_ID);
163 | };
164 |
165 | const handleMsg = (msg, sender, callback) => {
166 | if (!msg) {
167 | return;
168 | }
169 |
170 | console.log('recieved msg:', msg, 'from:', sender);
171 |
172 | switch (msg.type) {
173 | case MessageType.POPUP_LOADED:
174 | case MessageType.MODEL_LOADED:
175 | case MessageType.MODEL_ERROR:
176 | break;
177 |
178 | case MessageType.QUERY:
179 | handleQuery(msg);
180 | break;
181 | case MessageType.QUESTION_RESULT:
182 | handleModelSuccess(msg);
183 | break;
184 | case MessageType.QUESTION_ERROR:
185 | handleModelErr(msg);
186 | break;
187 | case MessageType.SELECT:
188 | handleSelection(msg);
189 | break;
190 | case MessageType.CLEAR:
191 | handleClear();
192 | break;
193 |
194 | default:
195 | console.error('Did not recognize message type:', msg);
196 | }
197 | };
198 |
199 | chrome.runtime.onMessage.addListener(handleMsg);
200 |
--------------------------------------------------------------------------------
/src/js/hot-reload.js:
--------------------------------------------------------------------------------
1 | const filesInDirectory = (dir) =>
2 | new Promise((resolve) =>
3 | dir.createReader().readEntries((entries) =>
4 | Promise.all(
5 | entries
6 | .filter((e) => e.name[0] !== '.')
7 | .map((e) =>
8 | e.isDirectory
9 | ? filesInDirectory(e)
10 | : new Promise((resolve) => e.file(resolve))
11 | )
12 | )
13 | .then((files) => [].concat(...files))
14 | .then(resolve)
15 | )
16 | );
17 |
18 | const timestampForFilesInDirectory = (dir) =>
19 | filesInDirectory(dir).then((files) =>
20 | files.map((f) => f.name + f.lastModifiedDate).join()
21 | );
22 |
23 | const reload = () => {
24 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
25 | // NB: see https://github.com/xpl/crx-hotreload/issues/5
26 | if (tabs[0]) {
27 | chrome.tabs.reload(tabs[0].id);
28 | }
29 | chrome.runtime.reload();
30 | });
31 | };
32 |
33 | const watchChanges = (dir, lastTimestamp) => {
34 | timestampForFilesInDirectory(dir).then((timestamp) => {
35 | if (!lastTimestamp || lastTimestamp === timestamp) {
36 | setTimeout(() => watchChanges(dir, timestamp), 1000); // retry after 1s
37 | } else {
38 | reload();
39 | }
40 | });
41 | };
42 |
43 | chrome.management.getSelf((self) => {
44 | if (self.installType === 'development') {
45 | chrome.runtime.getPackageDirectoryEntry((dir) => watchChanges(dir));
46 | }
47 | });
48 |
--------------------------------------------------------------------------------
/src/js/message_types.js:
--------------------------------------------------------------------------------
1 | export const MessageType = {
2 | // User types in a query.
3 | // From: Popup
4 | // To: Content Script
5 | QUERY: 'QUERY',
6 |
7 | // User selects a query result.
8 | // From: Popup
9 | // To: Content Script
10 | SELECT: 'SELECT',
11 |
12 | // User clears selection.
13 | // From: Popup
14 | // To: Content Script
15 | CLEAR: 'CLEAR',
16 |
17 | // Query has resulted in a match for an element on the page.
18 | // From: Content Script
19 | // To: Popup
20 | QUERY_RESULT: 'QUERY_RESULT',
21 | QUERY_ERROR: 'QUERY_ERROR',
22 | // Indicates that all the elements of the page have been analyzed and the
23 | // query is "done"
24 | // From: Content Script
25 | // To: Popup
26 | QUERY_DONE: 'QUERY_DONE',
27 |
28 | // Model request for a question-answer pair.
29 | // From: Content Script
30 | // To: Background
31 | QUESTION: 'QUESTION',
32 | // Model responds to a question-answer pair with success or error.
33 | // From: Background
34 | // To: Content Script
35 | QUESTION_RESULT: 'QUESTION_RESULT',
36 | QUESTION_ERROR: 'QUESTION_ERROR',
37 |
38 | // Popup sends this message when opened.
39 | // From: Popup
40 | // To: Background
41 | POPUP_LOADED: 'POPUP_LOADED',
42 | // Background sends this message on successful model load or error. Used to
43 | // indicate that the model is ready to the user.
44 | // From: Background
45 | // To: Popup
46 | MODEL_LOADED: 'MODEL_LOADED',
47 | MODEL_ERROR: 'MODEL_ERROR'
48 | };
49 |
--------------------------------------------------------------------------------
/src/js/popup.js:
--------------------------------------------------------------------------------
1 | import '../css/popup.css';
2 |
3 | import React from 'react';
4 | import { render } from 'react-dom';
5 | import SearchBar from './popup/search_bar';
6 |
7 | render(, window.document.getElementById('app-container'));
8 |
--------------------------------------------------------------------------------
/src/js/popup/search_bar.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import {
3 | Box,
4 | Button,
5 | Container,
6 | CircularProgress,
7 | Divider,
8 | IconButton,
9 | Grid,
10 | FormControl,
11 | InputLabel,
12 | Select,
13 | SvgIcon,
14 | TextField
15 | } from '@material-ui/core';
16 | import EmailIcon from '@material-ui/icons/Email';
17 | import CloseIcon from '@material-ui/icons/Close';
18 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
19 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
20 | import SearchIcon from '@material-ui/icons/Search';
21 |
22 | import { Component, MessageType } from '../message_types';
23 | import { useTimeout } from './timeout';
24 | import { hot } from 'react-hot-loader';
25 |
26 | const SearchBarState = {
27 | MODEL_LOADING: 'MODEL_LOADING',
28 | READY: 'READY',
29 | LOADING: 'LOADING',
30 | DONE: 'DONE'
31 | };
32 |
33 | const sendMessageToContent = (message) => {
34 | console.log('send msg to content:', message);
35 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => {
36 | const activeTab = tabs[0];
37 | chrome.tabs.sendMessage(activeTab.id, message);
38 | });
39 | };
40 |
41 | const sendMessageToBackground = (message) => {
42 | console.log('send msg to background:', message);
43 | chrome.runtime.sendMessage(message);
44 | };
45 |
46 | const registerListener = (setState, setAnswers, setErrors) => {
47 | chrome.runtime.onMessage.addListener((msg, sender, callback) => {
48 | console.log('recieved msg:', msg, 'from:', sender);
49 | switch (msg.type) {
50 | // Do nothing, these msgs are handled by the content script.
51 | case MessageType.QUESTION_RESULT:
52 | case MessageType.QUESTION_ERROR:
53 | case MessageType.QUESTION:
54 | break;
55 |
56 | case MessageType.MODEL_LOADED:
57 | setState(SearchBarState.READY);
58 | break;
59 |
60 | case MessageType.QUERY_RESULT:
61 | setAnswers((answers) =>
62 | [...answers, msg].sort((msg1, msg2) => {
63 | if (msg1.answer.score < msg2.answer.score) {
64 | return 1;
65 | } else {
66 | return -1;
67 | }
68 | })
69 | );
70 | break;
71 |
72 | case MessageType.QUERY_ERROR:
73 | setErrors((errors) => [...errors, msg]);
74 | break;
75 |
76 | case MessageType.QUERY_DONE:
77 | setState(SearchBarState.DONE);
78 | break;
79 |
80 | default:
81 | console.error('Did not recognize message type:', msg);
82 | break;
83 | }
84 | });
85 | };
86 |
87 | const SearchBarInput = (props) => {
88 | const inputRef = useRef();
89 |
90 | useEffect(() => {
91 | if (props.state === SearchBarState.READY) {
92 | inputRef.current.focus();
93 | }
94 | }, [props.state]);
95 |
96 | return (
97 | {
102 | props.setInput(e.target.value);
103 | }}
104 | disabled={props.state !== SearchBarState.READY}
105 | onKeyPress={(e) => {
106 | if (e.key === 'Enter') {
107 | props.search();
108 | }
109 | }}
110 | />
111 | );
112 | };
113 |
114 | const SearchBarControl = (props) => {
115 | if (props.state === SearchBarState.MODEL_LOADING) {
116 | return (
117 |
118 |
119 |
120 |
121 |
122 | Model Loading
123 |
124 |
125 | );
126 | }
127 |
128 | return (
129 |
130 |
131 | = props.answers.length - 1}
134 | onClick={() => {
135 | props.setSelectionIdx((idx) => idx + 1);
136 | }}>
137 |
138 |
139 |
140 |
141 | {
145 | props.setSelectionIdx((idx) => idx - 1);
146 | }}>
147 |
148 |
149 |
150 | {props.state === SearchBarState.READY && (
151 |
152 |
153 |
154 |
155 |
156 | )}
157 | {props.state === SearchBarState.LOADING && (
158 |
159 |
160 |
161 |
162 |
163 | )}
164 | {props.state === SearchBarState.DONE && (
165 |
166 |
167 |
168 |
169 |
170 | )}
171 |
172 | );
173 | };
174 |
175 | const SearchIndicator = (props) => {
176 | if (props.state === SearchBarState.DONE && props.answers.length === 0) {
177 | return No Results;
178 | }
179 |
180 | if (props.answers.length === 0) {
181 | return null;
182 | }
183 |
184 | return (
185 |
186 | {props.selectionIdx + 1}/{props.answers.length}
187 |
188 | );
189 | };
190 |
191 | const SearchBar = (props) => {
192 | var [answers, setAnswers] = useState([]);
193 | var [errors, setErrors] = useState([]);
194 | var [state, setState] = useState(SearchBarState.MODEL_LOADING);
195 | var [selectionIdx, setSelectionIdx] = useState(0);
196 | var [input, setInput] = useState('');
197 |
198 | // Register event listeners for recieving answers and errors
199 | // from the content script.
200 | useEffect(() => {
201 | registerListener(setState, setAnswers, setErrors);
202 | }, [setState, setAnswers, setErrors]);
203 |
204 | // Fire a selection event any time answers or selected index
205 | // changes.
206 | useEffect(() => {
207 | if (selectionIdx >= answers.length) {
208 | return;
209 | }
210 |
211 | sendMessageToContent({
212 | type: MessageType.SELECT,
213 | answer: answers[selectionIdx].answer,
214 | elementId: answers[selectionIdx].elementId
215 | });
216 | }, [selectionIdx, answers]);
217 |
218 | useEffect(() => {
219 | sendMessageToBackground({
220 | type: MessageType.POPUP_LOADED
221 | });
222 | }, []);
223 |
224 | const search = () => {
225 | sendMessageToContent({
226 | type: MessageType.QUERY,
227 | query: input
228 | });
229 |
230 | setState(SearchBarState.LOADING);
231 | };
232 |
233 | const reset = () => {
234 | sendMessageToContent({
235 | type: MessageType.CLEAR
236 | });
237 |
238 | setAnswers([]);
239 | setErrors([]);
240 | setSelectionIdx(0);
241 | setState(SearchBarState.READY);
242 | };
243 |
244 | const gridStyle = {
245 | width: '450px',
246 | padding: '10px',
247 | paddingBottom: '5px'
248 | };
249 |
250 | const itemStyle = {
251 | margin: 'auto auto'
252 | };
253 |
254 | return (
255 |
256 |
257 |
263 |
264 | {state === SearchBarState.LOADING || state === SearchBarState.DONE ? (
265 |
266 |
271 |
272 | ) : null}
273 |
274 |
283 |
284 |
285 | );
286 | };
287 |
288 | export default hot(module)(SearchBar);
289 |
--------------------------------------------------------------------------------
/src/js/popup/timeout.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export const useTimeout = (callback, delay, ...deps) => {
4 | const savedCallback = useRef();
5 |
6 | // Remember the latest callback.
7 | useEffect(() => {
8 | savedCallback.current = callback;
9 | }, [callback]);
10 |
11 | // Set up the interval.
12 | useEffect(() => {
13 | function tick() {
14 | savedCallback.current();
15 | }
16 | if (delay !== null) {
17 | const id = setTimeout(tick, delay);
18 | return () => clearTimeout(id);
19 | }
20 | /* eslint-disable */
21 | }, [delay, ...deps]);
22 | };
23 |
--------------------------------------------------------------------------------
/src/js/types.js:
--------------------------------------------------------------------------------
1 | export const MessageType = {
2 | // Query from popup -> content script.
3 | QUERY: 'QUERY',
4 | // Select from popup -> content script.
5 | SELECT: 'SELECT',
6 | // Clear selection and search from popup -> content script.
7 | CLEAR: 'CLEAR',
8 | // Query result from content script -> popup.
9 | QUERY_RESULT: 'QUERY_RESULT',
10 | // Query error from content script -> popup.
11 | QUERY_ERROR: 'QUERY_ERROR',
12 | // Query is done when content script -> popup sends this.
13 | QUERY_DONE: 'QUERY_DONE',
14 |
15 | // Question from content script -> background model.
16 | QUESTION: 'QUESTION',
17 | // Answer from background model -> content.
18 | QUESTION_RESULT: 'QUESTION_RESULT',
19 | // Error from background model -> content.
20 | QUESTION_ERROR: 'QUESTION_ERROR',
21 |
22 | // Popup broadcast message on load.
23 | POPUP_LOADED: 'POPUP_LOADED',
24 | // Model broadcast message when loaded.
25 | MODEL_LOADED: 'MODEL_LOADED',
26 | // Model error
27 | MODEL_ERROR: 'MODEL_ERROR'
28 | };
29 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Shift-Ctrl-F: Semantic Search for the Browser",
4 | "short_name": "Shift-Ctrl-F",
5 | "author": "Yoav Zimmerman ",
6 | "background": {
7 | "page": "background.html"
8 | },
9 | "browser_action": {
10 | "default_popup": "popup.html"
11 | },
12 | "content_scripts": [
13 | {
14 | "matches": ["http://*/*", "https://*/*"],
15 | "js": ["content.bundle.js"],
16 | "run_at": "document_end"
17 | }
18 | ],
19 | "icons": {
20 | "16": "icon-16.png",
21 | "48": "icon-48.png",
22 | "128": "icon-128.png"
23 | },
24 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
25 | "commands": {
26 | "_execute_browser_action": {
27 | "suggested_key": {
28 | "default": "Shift+Ctrl+F",
29 | "mac": "Shift+Ctrl+F"
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/utils/build.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack'),
2 | config = require('../webpack.config');
3 |
4 | delete config.chromeExtensionBoilerplate;
5 |
6 | webpack(config, function (err) {
7 | if (err) throw err;
8 | });
9 |
--------------------------------------------------------------------------------
/utils/env.js:
--------------------------------------------------------------------------------
1 | // tiny wrapper with default env vars
2 | module.exports = {
3 | NODE_ENV: process.env.NODE_ENV || 'development',
4 | PORT: process.env.PORT || 3000
5 | };
6 |
--------------------------------------------------------------------------------
/utils/webserver.js:
--------------------------------------------------------------------------------
1 | var WebpackDevServer = require('webpack-dev-server'),
2 | webpack = require('webpack'),
3 | config = require('../webpack.config'),
4 | env = require('./env'),
5 | path = require('path'),
6 | fs = require('fs');
7 |
8 | var options = config.chromeExtensionBoilerplate || {};
9 | var excludeEntriesToHotReload = options.notHotReload || [];
10 |
11 | for (var entryName in config.entry) {
12 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) {
13 | config.entry[entryName] = [
14 | 'webpack-dev-server/client?http://localhost:' + env.PORT,
15 | 'webpack/hot/dev-server'
16 | ].concat(config.entry[entryName]);
17 | }
18 | }
19 |
20 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat(
21 | config.plugins || []
22 | );
23 |
24 | delete config.chromeExtensionBoilerplate;
25 |
26 | var compiler = webpack(config);
27 |
28 | var server = new WebpackDevServer(compiler, {
29 | hot: true,
30 | contentBase: path.join(__dirname, '../build'),
31 | sockPort: env.PORT,
32 | headers: {
33 | 'Access-Control-Allow-Origin': '*'
34 | },
35 | disableHostCheck: true
36 | });
37 |
38 | server.listen(env.PORT);
39 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack'),
2 | path = require('path'),
3 | fileSystem = require('fs'),
4 | env = require('./utils/env'),
5 | CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin,
6 | CopyWebpackPlugin = require('copy-webpack-plugin'),
7 | HtmlWebpackPlugin = require('html-webpack-plugin'),
8 | WriteFilePlugin = require('write-file-webpack-plugin');
9 |
10 | // load the secrets
11 | var alias = {};
12 |
13 | var secretsPath = path.join(__dirname, 'secrets.' + env.NODE_ENV + '.js');
14 |
15 | var fileExtensions = [
16 | 'jpg',
17 | 'jpeg',
18 | 'png',
19 | 'gif',
20 | 'eot',
21 | 'otf',
22 | 'ttf',
23 | 'woff',
24 | 'woff2'
25 | ];
26 |
27 | if (fileSystem.existsSync(secretsPath)) {
28 | alias['secrets'] = secretsPath;
29 | }
30 |
31 | var options = {
32 | mode: process.env.NODE_ENV || 'development',
33 | entry: {
34 | popup: path.join(__dirname, 'src', 'js', 'popup.js'),
35 | background: path.join(__dirname, 'src', 'js', 'background.js'),
36 | content: path.join(__dirname, 'src', 'js', 'content.js')
37 | },
38 | chromeExtensionBoilerplate: {
39 | notHotReload: ['content', 'background']
40 | },
41 | output: {
42 | path: path.join(__dirname, 'build'),
43 | filename: '[name].bundle.js'
44 | },
45 | module: {
46 | rules: [
47 | { test: /\.svg$/, loader: 'svg-inline-loader', exclude: /node_modules/ },
48 | {
49 | test: /\.css$/,
50 | loader: 'style-loader!css-loader',
51 | exclude: /node_modules/
52 | },
53 | {
54 | test: new RegExp('.(' + fileExtensions.join('|') + ')$'),
55 | loader: 'file-loader?name=[name].[ext]',
56 | exclude: /node_modules/
57 | },
58 | {
59 | test: /\.html$/,
60 | loader: 'html-loader',
61 | exclude: /node_modules/
62 | },
63 | {
64 | test: /\.(js|jsx)$/,
65 | loader: ['babel-loader', 'eslint-loader'],
66 | exclude: /node_modules/
67 | }
68 | ]
69 | },
70 | resolve: {
71 | alias: alias
72 | },
73 | plugins: [
74 | // clean the build folder
75 | new CleanWebpackPlugin({
76 | cleanAfterEveryBuildPatterns: ['!manifest.json']
77 | }),
78 | // expose and write the allowed env vars on the compiled bundle
79 | new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
80 | new CopyWebpackPlugin([
81 | {
82 | from: 'src/manifest.json',
83 | transform: function (content, path) {
84 | // generates the manifest file using the package.json informations
85 | return Buffer.from(
86 | JSON.stringify({
87 | description: process.env.npm_package_description,
88 | version: process.env.npm_package_version,
89 | ...JSON.parse(content.toString())
90 | })
91 | );
92 | }
93 | }
94 | ]),
95 | new HtmlWebpackPlugin({
96 | template: path.join(__dirname, 'src', 'popup.html'),
97 | filename: 'popup.html',
98 | chunks: ['popup']
99 | }),
100 | new HtmlWebpackPlugin({
101 | template: path.join(__dirname, 'src', 'background.html'),
102 | filename: 'background.html',
103 | chunks: ['background']
104 | }),
105 | new WriteFilePlugin()
106 | ]
107 | };
108 |
109 | if (env.NODE_ENV === 'development') {
110 | options.devtool = 'cheap-module-eval-source-map';
111 | }
112 |
113 | module.exports = options;
114 |
--------------------------------------------------------------------------------