├── .gitignore
├── LICENSE
├── README.md
├── assets
├── Github.svg
├── Icon.svg
├── StoreIcon.png
├── StoreIconDark.png
├── StoreScreenshot.png
├── StoreScreenshot1280x800.png
└── StoreScreenshotScaled.png
├── jest.config.js
├── package.json
├── public
├── icon.png
├── manifest.json
├── options.html
└── popup.html
├── src
├── content_script.tsx
├── mount_view.tsx
├── options.tsx
├── popup.css
├── popup.tsx
└── types.d.ts
├── tsconfig.json
└── webpack
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 | dist/
4 | tmp/
5 | .vscode
6 | .github
7 |
8 |
9 | .DS_Store
10 | .DS_Store?
11 | ._*
12 | .Spotlight-V100
13 | .Trashes
14 | ehthumbs.db
15 | Thumbs.db
16 |
17 | dist.zip
18 | dist.pem
19 | dist.crx
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Tomofumi Chiba
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skip the Scroll
2 |
3 |
4 |
5 | Tired of scrolling through hundreds of github issue comments to find that one magical response? Skip the scroll is a simple chrome extension that sorts github issue comments for you and allows you to iterate through them based on their social reactions.
6 |
7 | [Get extension on the Chrome Web store](https://chrome.google.com/webstore/detail/skip-the-scroll/mfehannpjmgfagldoilpngeoecdfgmnd)
8 |
9 | ## Prerequisites
10 |
11 | - [node + npm](https://nodejs.org/) (Current Version)
12 |
13 | ## Setup
14 |
15 | ```
16 | npm install
17 | ```
18 |
19 | ## Build
20 |
21 | ```
22 | npm run build
23 | ```
24 |
25 | ## Build in watch mode
26 |
27 | ### terminal
28 |
29 | ```
30 | npm run watch
31 | ```
32 |
--------------------------------------------------------------------------------
/assets/Github.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
27 |
--------------------------------------------------------------------------------
/assets/Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/assets/StoreIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreIcon.png
--------------------------------------------------------------------------------
/assets/StoreIconDark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreIconDark.png
--------------------------------------------------------------------------------
/assets/StoreScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreScreenshot.png
--------------------------------------------------------------------------------
/assets/StoreScreenshot1280x800.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreScreenshot1280x800.png
--------------------------------------------------------------------------------
/assets/StoreScreenshotScaled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/assets/StoreScreenshotScaled.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "roots": [
3 | "src"
4 | ],
5 | "transform": {
6 | "^.+\\.ts$": "ts-jest"
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "skip-the-scroll",
3 | "version": "1.0.0",
4 | "description": "skip-the-scroll",
5 | "main": "index.js",
6 | "scripts": {
7 | "watch": "webpack --config webpack/webpack.dev.js --watch",
8 | "build": "webpack --config webpack/webpack.prod.js",
9 | "clean": "rimraf dist",
10 | "test": "npx jest",
11 | "style": "prettier --write \"src/**/*.{ts,tsx}\""
12 | },
13 | "author": "",
14 | "license": "MIT",
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/henryjeff/skip-the-scroll"
18 | },
19 | "dependencies": {
20 | "@svgr/webpack": "^6.1.2",
21 | "framer-motion": "^5.5.5",
22 | "react": "^17.0.1",
23 | "react-dom": "^17.0.1",
24 | "svg-url-loader": "^7.1.1"
25 | },
26 | "devDependencies": {
27 | "@types/chrome": "0.0.158",
28 | "@types/jest": "^27.0.2",
29 | "@types/react": "^17.0.0",
30 | "@types/react-dom": "^17.0.0",
31 | "copy-webpack-plugin": "^9.0.1",
32 | "css-loader": "^6.5.1",
33 | "glob": "^7.1.6",
34 | "jest": "^27.2.1",
35 | "prettier": "^2.2.1",
36 | "rimraf": "^3.0.2 ",
37 | "style-loader": "^3.3.1",
38 | "ts-jest": "^27.0.5",
39 | "ts-loader": "^8.0.0",
40 | "typescript": "^4.4.3 ",
41 | "webpack": "^5.0.0",
42 | "webpack-cli": "^4.0.0",
43 | "webpack-merge": "^5.0.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henryjeff/skip-the-scroll/ca76d6701de3df3a62d285024e89f18440793602/public/icon.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 |
4 | "name": "Skip the Scroll",
5 | "description": "Tired of scrolling through hundreds of Github issue comments to find that one magical response? Skip the scroll is here to help!",
6 | "version": "1.0.0",
7 |
8 | "options_ui": {
9 | "page": "options.html"
10 | },
11 |
12 | "action": {
13 | "default_icon": "icon.png",
14 | "default_popup": "popup.html"
15 | },
16 |
17 | "content_scripts": [
18 | {
19 | "matches": ["https://github.com/*"],
20 | "js": ["js/vendor.js", "js/content_script.js"]
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/public/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Skip the Scroll Options
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Skip the Scroll
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/content_script.tsx:
--------------------------------------------------------------------------------
1 | let comments: CommentData[] = [];
2 |
3 | chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
4 | if (msg.type === "loadComments") {
5 | const _comments = parseComments();
6 | comments = _comments;
7 | sendResponse(comments);
8 | return true;
9 | }
10 | if (msg.type === "scrollTo") {
11 | const commentData = comments[Number(msg.index)];
12 | if (commentData) {
13 | const y =
14 | commentData.comment.getBoundingClientRect().top +
15 | window.pageYOffset +
16 | -128;
17 | window.scrollTo({ top: y, behavior: "smooth" });
18 | comments.forEach((comment) => {
19 | // @ts-ignore
20 | comment.comment.style = "box-shadow: 0px 0px 0px 0px";
21 | });
22 | // @ts-ignore
23 | commentData.comment.style = "box-shadow: 0px 0px 0px 1.5px #2BA44E;";
24 | sendResponse(commentData);
25 | return true;
26 | }
27 | }
28 | return true;
29 | });
30 |
31 | type CommentData = {
32 | comment: Element;
33 | upvotes: number;
34 | timestamp: number;
35 | };
36 |
37 | const parseComments = () => {
38 | const comments: CommentData[] = [];
39 |
40 | // Get all posts on the page
41 | const commentElements = document.getElementsByClassName(
42 | "timeline-comment comment"
43 | );
44 |
45 | // All upvote emojis
46 | const upvoteEmojis = ["👍", "😄", "🎉", "🚀", "❤️"];
47 |
48 | Array.from(commentElements).forEach((comment) => {
49 | const reactions = comment.getElementsByClassName(
50 | "social-reaction-summary-item"
51 | );
52 | let totalUpvotes = 0;
53 | Array.from(reactions).forEach((reaction) => {
54 | // @ts-ignore
55 | let splitText: string[] = reaction.innerText.split("\n");
56 |
57 | let emoji = splitText[0];
58 | let numReactions = Number(splitText[1]);
59 |
60 | // Increase totalUpvotes if there are upvote emojis in the reaction
61 | if (upvoteEmojis.includes(emoji)) {
62 | totalUpvotes += numReactions;
63 | }
64 | });
65 | const timestamp = Date.parse(
66 | comment
67 | .getElementsByClassName("js-timestamp")[0]
68 | .children[0].getAttribute("datetime")!
69 | );
70 | comments.push({ comment, upvotes: totalUpvotes, timestamp: timestamp });
71 | });
72 |
73 | sortComments(comments);
74 |
75 | return comments;
76 | };
77 |
78 | const sortComments = (comments: CommentData[]) => {
79 | comments
80 | .sort(function (x: CommentData, y: CommentData) {
81 | var n = x.upvotes - y.upvotes;
82 | if (n !== 0) {
83 | return n;
84 | }
85 |
86 | return x.timestamp - y.timestamp;
87 | })
88 | .reverse();
89 | };
90 |
--------------------------------------------------------------------------------
/src/mount_view.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import { motion, MotionProps } from "framer-motion";
3 |
4 | export type AnimationEasing =
5 | | "sSoft"
6 | | "sMedium"
7 | | "sHard"
8 | | "expIn"
9 | | "expOut"
10 | | "expInOut";
11 |
12 | export const Easing: { [key in AnimationEasing]: number[] } = {
13 | sHard: [0.95, 0, 0.5, 1],
14 | sMedium: [0.8, 0, 0.2, 1],
15 | sSoft: [0.6, 0, 0.4, 1],
16 | expIn: [0.9, 0.05, 1, 0.3],
17 | expOut: [0.05, 0.7, 0.1, 1],
18 | expInOut: [0.9, 0.05, 0.1, 1],
19 | };
20 |
21 | export interface AnimatedMountViewProps {
22 | styles?: React.CSSProperties;
23 | easing?: AnimationEasing;
24 | duration?: number;
25 | motionProps?: MotionProps;
26 | mountDirection?: "x" | "y" | "none";
27 | mountInitialOffset?: number;
28 | delay?: number;
29 | className?: string;
30 | }
31 |
32 | const AnimatedMountView: React.FC = ({
33 | styles,
34 | easing,
35 | duration,
36 | motionProps,
37 | children,
38 | mountDirection,
39 | mountInitialOffset,
40 | delay,
41 | className,
42 | }) => {
43 | const positionFinal = useMemo(() => {
44 | return mountDirection === "x"
45 | ? { x: 0 }
46 | : mountDirection === "none"
47 | ? {}
48 | : { y: 0 };
49 | }, [mountDirection]);
50 |
51 | const initialOffset = useMemo(() => {
52 | return mountInitialOffset || 16;
53 | }, [mountInitialOffset]);
54 |
55 | const positionInitial = useMemo(() => {
56 | return mountDirection === "x"
57 | ? { x: initialOffset }
58 | : mountDirection === "none"
59 | ? {}
60 | : { y: initialOffset };
61 | }, [initialOffset, mountDirection]);
62 |
63 | const divProps = useMemo(() => {
64 | return {
65 | animate: { ...positionFinal, opacity: 1 },
66 | initial: { ...positionInitial, opacity: 0 },
67 | transition: {
68 | ease: Easing[easing || "expOut"],
69 | duration: duration || 0.4,
70 | delay,
71 | },
72 | };
73 | }, [positionFinal, positionInitial, duration, easing, delay]);
74 |
75 | return (
76 |
82 | {children}
83 |
84 | );
85 | };
86 |
87 | const FadeAnimatedMountView: React.FC = (props) => {
88 | return ;
89 | };
90 |
91 | export default Object.assign(AnimatedMountView, {
92 | Fade: FadeAnimatedMountView,
93 | });
94 |
--------------------------------------------------------------------------------
/src/options.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | const Options = () => {
5 | // const [color, setColor] = useState("");
6 | // const [status, setStatus] = useState("");
7 | // const [like, setLike] = useState(false);
8 |
9 | // useEffect(() => {
10 | // // Restores select box and checkbox state using the preferences
11 | // // stored in chrome.storage.
12 | // chrome.storage.sync.get(
13 | // {
14 | // favoriteColor: "red",
15 | // likesColor: true,
16 | // },
17 | // (items) => {
18 | // setColor(items.favoriteColor);
19 | // setLike(items.likesColor);
20 | // }
21 | // );
22 | // }, []);
23 |
24 | // const saveOptions = () => {
25 | // // Saves options to chrome.storage.sync.
26 | // chrome.storage.sync.set(
27 | // {
28 | // favoriteColor: color,
29 | // likesColor: like,
30 | // },
31 | // () => {
32 | // // Update status to let user know options were saved.
33 | // setStatus("Options saved.");
34 | // const id = setTimeout(() => {
35 | // setStatus("");
36 | // }, 1000);
37 | // return () => clearTimeout(id);
38 | // }
39 | // );
40 | // };
41 |
42 | return (
43 | <>
44 | {/*
45 | Favorite color:
54 |
55 |
56 |
64 |
65 | {status}
66 | */}
67 | >
68 | );
69 | };
70 |
71 | ReactDOM.render(
72 |
73 |
74 | ,
75 | document.getElementById("root")
76 | );
77 |
--------------------------------------------------------------------------------
/src/popup.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: flex;
3 | margin: 0px;
4 | padding: 0px;
5 | width: 100%;
6 | background-color: #0d1117;
7 | border-width: 1.5px;
8 | border-style: solid;
9 | box-sizing: border-box;
10 | border-color: #30363d;
11 | }
12 |
13 | .btn {
14 | background-color: #30a14e;
15 | border: none;
16 | width: 100%;
17 | padding: 5px 16px;
18 | color: #fff;
19 | font-size: 14px;
20 | font-family: "system-ui", "Segoe UI";
21 | font-weight: 500;
22 | cursor: pointer;
23 | border-radius: 6px;
24 | box-sizing: border-box;
25 | transition-duration: 0.2s;
26 | transition-property: background-color, border-color, opacity;
27 | }
28 |
29 | .spacer {
30 | width: 16px;
31 | }
32 |
33 | .popup {
34 | width: 256px;
35 | border-radius: 6px;
36 | }
37 |
38 | .btn:hover:enabled {
39 | background-color: #40c463;
40 | }
41 |
42 | .btn:active:enabled {
43 | opacity: 0.9;
44 | }
45 |
46 | .btn-dark {
47 | background-color: #0d1117;
48 | border-width: 1.5px;
49 | border-style: solid;
50 | border-color: #30363d;
51 | }
52 |
53 | .btn-dark:hover:enabled {
54 | background-color: #0d1117;
55 | border-color: #8b949e;
56 | }
57 |
58 | .disabled {
59 | opacity: 0.3;
60 | cursor: default;
61 | }
62 |
63 | .btn-dark:active:enabled {
64 | opacity: 0.8;
65 | }
66 |
67 | .buttons {
68 | display: flex;
69 | justify-content: space-between;
70 | padding: 16px;
71 | padding-bottom: 0px;
72 | }
73 |
74 | .no-parse {
75 | padding: 16px;
76 | }
77 |
78 | .header {
79 | display: flex;
80 | align-items: flex-end;
81 | justify-content: space-between;
82 | /* margin-bottom: 16px; */
83 | /* width: 100%; */
84 | padding: 12px;
85 | border-bottom-color: #30363d;
86 | border-bottom-style: solid;
87 | border-bottom-width: 1px;
88 | background-color: #161b22;
89 | }
90 | .header-left {
91 | display: flex;
92 | align-items: center;
93 | }
94 |
95 | .icon {
96 | margin-top: 2px;
97 | margin-left: 8px;
98 | width: 14px;
99 | height: 14px;
100 | }
101 |
102 | /* a:hover {
103 | cursor: pointer;
104 | opacity: 0.8;
105 | } */
106 |
107 | p {
108 | color: #fff;
109 | font-size: 14px;
110 | font-family: "system-ui", "Segoe UI";
111 | font-weight: 500;
112 | padding: 0px;
113 | margin: 0px;
114 | cursor: default;
115 | }
116 |
117 | .subtext {
118 | color: #8b949e;
119 | font-size: 12px;
120 | }
121 |
122 | .subheader {
123 | color: #8b949e;
124 | }
125 |
126 | .subtext-white {
127 | color: white;
128 | font-size: 12px;
129 | }
130 |
131 | .comment-info-line {
132 | flex-shrink: 1;
133 | width: 2px;
134 | padding-top: 4px;
135 | padding-bottom: 4px;
136 | margin-left: 8px;
137 | margin-right: 8px;
138 | background-color: #30363d;
139 | }
140 |
141 | .comment-info-container {
142 | display: flex;
143 | flex-direction: column;
144 | padding: 12px;
145 | }
146 |
147 | .comment-info-inner {
148 | display: flex;
149 | /* background-color: purple; */
150 | padding-top: 8px;
151 | }
152 |
153 | .comment-info {
154 | flex: 1;
155 | }
156 |
157 | .highest-voted {
158 | color: #8b949e;
159 | font-size: 12px;
160 | }
161 |
162 | .highest-voted-container {
163 | padding-top: 12px;
164 | padding-bottom: 12px;
165 | }
166 |
167 | .comment-info-item {
168 | display: flex;
169 | justify-content: space-between;
170 | padding-top: 6px;
171 | padding-bottom: 6px;
172 | }
173 |
--------------------------------------------------------------------------------
/src/popup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import ReactDOM from "react-dom";
3 | import AnimatedMount from "./mount_view";
4 | import "./popup.css";
5 | import GithubIcon from "../assets/Github.svg";
6 | interface CommentInfoItemProps {
7 | text?: string;
8 | data?: string;
9 | }
10 |
11 | const CommentInfoItem: React.FC = ({ text, data }) => {
12 | return (
13 |
14 |
{text}
15 |
21 | {data}
22 |
23 |
24 | );
25 | };
26 |
27 | const Popup = () => {
28 | const [currentURL, setCurrentURL] = useState();
29 | const [currentComment, setCurrentComment] = useState();
30 | const [comments, setComments] = useState([]);
31 | const [index, setIndex] = useState(-1);
32 | const [hasNext, setHasNext] = useState(true);
33 | const [hasLast, setHasLast] = useState(false);
34 | const [date, setDate] = useState("");
35 |
36 | useEffect(() => {
37 | let d = new Date(currentComment?.timestamp!);
38 | setDate(
39 | d.getUTCHours() +
40 | ":" +
41 | (d.getUTCMinutes() < 10 ? "0" : "") +
42 | d.getUTCMinutes() +
43 | ", " +
44 | (d.getMonth() + 1) +
45 | "/" +
46 | d.getDate() +
47 | "/" +
48 | `${d.getFullYear()}`.slice(-2)
49 | );
50 | }, [currentComment]);
51 |
52 | useEffect(() => {
53 | loadComments();
54 | }, []);
55 |
56 | useEffect(() => {
57 | sendMessage("scrollTo", { index: index }, (currentComment) => {
58 | setCurrentComment(currentComment);
59 | });
60 | console.log(index);
61 | if (comments.length > 0) {
62 | if (index + 1 < comments.length) setHasNext(true);
63 | else setHasNext(false);
64 | if (index - 1 >= 0) setHasLast(true);
65 | else setHasLast(false);
66 | }
67 | }, [index]);
68 |
69 | useEffect(() => {
70 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
71 | setCurrentURL(tabs[0].url);
72 | });
73 | }, []);
74 |
75 | const loadComments = () => {
76 | sendMessage("loadComments", {}, (comments) => {
77 | setComments(comments);
78 | });
79 | };
80 |
81 | const sendMessage = (
82 | type: string,
83 | message: object,
84 | responseCallback?: (res: any) => any
85 | ) => {
86 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
87 | const tab = tabs[0];
88 | if (tab.id) {
89 | chrome.tabs.sendMessage(tab.id, { ...message, type }, responseCallback);
90 | }
91 | });
92 | };
93 |
94 | const nextComment = () => {
95 | if (index + 1 < comments.length) {
96 | setIndex(index + 1);
97 | }
98 | if (index + 1 < comments.length) setHasNext(true);
99 | else setHasNext(false);
100 | };
101 |
102 | const lastComment = () => {
103 | if (index - 1 >= 0) {
104 | setIndex(index - 1);
105 | }
106 | };
107 |
108 | return (
109 |
110 |
122 |
123 | {comments && comments.length > 0 ? (
124 |
125 |
132 |
133 |
140 |
141 | ) : (
142 |
143 |
Unable to parse the current tab
144 |
145 | )}
146 |
147 | {comments && comments.length > 0 ? (
148 |
149 |
Comment Information:
150 |
151 |
152 |
153 | {currentComment ? (
154 | <>
155 |
159 |
160 | {index === 0 ? (
161 |
162 |
163 |
164 | This is the most recent highest rated response
165 |
166 |
167 |
168 | ) : (
169 | <>>
170 | )}
171 | >
172 | ) : (
173 |
174 | )}
175 |
176 |
177 |
178 | ) : (
179 | <>>
180 | )}
181 |
182 | );
183 | };
184 |
185 | ReactDOM.render(
186 |
187 |
188 | ,
189 | document.getElementById("root")
190 | );
191 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | const content: any;
3 | export default content;
4 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "commonjs",
5 | "target": "es6",
6 | "esModuleInterop": true,
7 | "sourceMap": false,
8 | "rootDir": "src",
9 | "outDir": "dist/js",
10 | "noEmitOnError": true,
11 | "jsx": "react",
12 | "typeRoots": [ "node_modules/@types" ]
13 | }
14 | }
--------------------------------------------------------------------------------
/webpack/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 | const CopyPlugin = require("copy-webpack-plugin");
4 | const srcDir = path.join(__dirname, "..", "src");
5 |
6 | module.exports = {
7 | entry: {
8 | popup: path.join(srcDir, 'popup.tsx'),
9 | options: path.join(srcDir, 'options.tsx'),
10 | content_script: path.join(srcDir, 'content_script.tsx'),
11 | },
12 | output: {
13 | path: path.join(__dirname, "../dist/js"),
14 | filename: "[name].js",
15 | },
16 | optimization: {
17 | splitChunks: {
18 | name: "vendor",
19 | chunks(chunk) {
20 | return chunk.name !== 'background';
21 | }
22 | },
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.tsx?$/,
28 | use: "ts-loader",
29 | exclude: /node_modules/,
30 | },
31 | {
32 | test: /\.css$/,
33 | use: [
34 | 'style-loader',
35 | 'css-loader'
36 | ]
37 | },
38 | {
39 | test: /\.svg$/,
40 | use: ['@svgr/webpack'],
41 | },
42 | ],
43 | },
44 | resolve: {
45 | extensions: [".ts", ".tsx", ".js"],
46 | },
47 | plugins: [
48 | new CopyPlugin({
49 | patterns: [{ from: ".", to: "../", context: "public" }],
50 | options: {},
51 | }),
52 | ],
53 | };
54 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | devtool: 'inline-source-map',
6 | mode: 'development'
7 | });
--------------------------------------------------------------------------------
/webpack/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production'
6 | });
--------------------------------------------------------------------------------