├── .circleci └── config.yml ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── assets ├── chrome.png ├── firefox.png ├── icon.svg └── screenshot.png ├── package-lock.json ├── package.json ├── src ├── background.htm ├── background.js ├── fonts.js ├── img │ ├── tabclip_128.png │ ├── tabclip_16.png │ ├── tabclip_32.png │ └── tabclip_48.png ├── manifest.json ├── popup.htm ├── popup.js └── shared.js └── webpack.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.15.1 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests 37 | - run: npm test 38 | 39 | # build extension archive 40 | - run: npx webpack 41 | 42 | - store_artifacts: 43 | path: dist/tabclip.zip 44 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "webextensions": true 7 | }, 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "ecmaVersion": 2017 11 | }, 12 | "extends": "eslint:recommended", 13 | "rules": { 14 | "no-console": [ 15 | "off" 16 | ], 17 | "indent": [ 18 | "error", 19 | "tab" 20 | ], 21 | "linebreak-style": [ 22 | "error", 23 | "unix" 24 | ], 25 | "quotes": [ 26 | "error", 27 | "single" 28 | ], 29 | "semi": [ 30 | "error", 31 | "never" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.swp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Joshua Dick 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tabclip 2 | 3 | Tabclip screenshot 4 | 5 | Copy browser tabs to (or create them from) your clipboard. 6 | 7 | Get the extension: 8 | 9 | 10 | Firefox logo 11 | 12 | 13 | Chrome logo 14 | 15 | 16 | ## About 17 | 18 | Tabclip is a web browser extension for Mozilla Firefox and Google Chrome that allows you to copy browser tabs to (or create them from) your clipboard. 19 | 20 | The "Copy" button, or keyboard shortcut CTRL+SHIFT+C by default, copies tab URLs to your clipboard. 21 | 22 | The "Paste" button, or keyboard shortcut CTRL+SHIFT+V by default, attempts to find all URLs that appear in your clipboard, then opens each URL in a new browser tab. 23 | 24 | That's it! 25 | 26 | ## Feedback 27 | 28 | If you have suggestions or bug reports for tabclip, I am much more likely to see your feedback if you leave it at [tabclip's GitHub Issues page](https://github.com/joshdick/tabclip/issues) rather than on tabclip's Firefox Add-Ons or Chrome Web Store pages. 29 | 30 | I'd like to keep this extension as simple and minimal as possible, so most feature requests are not likely to be honored. 31 | 32 | ## Development 33 | 34 | To build tabclip for both Firefox and Chrome, install the latest versions of NodeJS and `npm`, then do the following: 35 | 36 | ```bash 37 | git clone https://github.com/joshdick/tabclip.git 38 | cd tabclip 39 | npm install 40 | npx webpack 41 | ``` 42 | 43 | The build will create a `tabclip.zip` extension archive in the `dist/` directory. 44 | 45 | ## Attribution 46 | 47 | Tabclip is heavily inspired by Vincent Paré's ["Copy All Urls" Chrome extension](https://chrome.google.com/webstore/detail/copy-all-urls/djdmadneanknadilpjiknlnanaolmbfk). I created tabclip because I wanted a similar extension that looked and worked the same in both Chrome and Firefox. Tabclip was written from scratch and shares no code with the "Copy All Urls" Chrome extension. 48 | 49 | Tabclip's [icon](https://www.flaticon.com/free-icon/design-tab_68369) was made by [Freepik](https://www.flaticon.com/authors/freepik) from [flaticon.com](https://www.flaticon.com/). 50 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /assets/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/assets/chrome.png -------------------------------------------------------------------------------- /assets/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/assets/firefox.png -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/assets/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabclip", 3 | "version": "1.0.0", 4 | "description": "Copy browser tabs to (or create them from) your clipboard.", 5 | "private": true, 6 | "scripts": { 7 | "test": "eslint src/**/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/joshdick/tabclip.git" 12 | }, 13 | "keywords": [ 14 | "browser", 15 | "extension", 16 | "firefox", 17 | "chrome", 18 | "tabs", 19 | "tab", 20 | "clipboard", 21 | "copy", 22 | "paste" 23 | ], 24 | "author": "Josh Dick ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/joshdick/tabclip/issues" 28 | }, 29 | "homepage": "https://joshdick.github.io/tabclip", 30 | "dependencies": { 31 | "@fortawesome/fontawesome": "^1.1.8", 32 | "@fortawesome/fontawesome-free-solid": "^5.0.13", 33 | "bootstrap-css-only": "^4.4.1", 34 | "url-regex-safe": "^2.0.2", 35 | "webextension-polyfill": "^0.7.0" 36 | }, 37 | "devDependencies": { 38 | "clean-webpack-plugin": "^3.0.0", 39 | "copy-webpack-plugin": "^8.1.0", 40 | "css-loader": "^5.1.3", 41 | "eslint": "^7.22.0", 42 | "style-loader": "^2.0.0", 43 | "webpack": "^5.27.2", 44 | "webpack-cli": "^4.5.0", 45 | "zip-webpack-plugin": "^4.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/background.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | tabclip - background 9 | 10 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | const browser = require('webextension-polyfill') 2 | const shared = require('./shared') 3 | 4 | const showNotification = async (quantity, operation) => { 5 | const idSuffix = operation === shared.ALERT_OPERATIONS.COPY ? 'copy' : 'paste' 6 | const titleVerb = operation === shared.ALERT_OPERATIONS.COPY ? 'Copy' : 'Paste' 7 | const messageVerb = operation === shared.ALERT_OPERATIONS.COPY ? 'Copied' : 'Pasted' 8 | await browser.notifications.create(`tabclip-${idSuffix}`, { 9 | type: 'basic', 10 | iconUrl: browser.extension.getURL('img/tabclip_128.png'), 11 | title: `Tabclip ${titleVerb}`, 12 | message: `${messageVerb} ${quantity} URL${quantity === 1 ? '' : 's'}.`, 13 | }) 14 | } 15 | 16 | const commandListener = async (command) => { 17 | if (command === 'copy-tabs') { 18 | const { 19 | [shared.PREFERENCE_NAMES.COPY_SCOPE]: copyScope, 20 | [shared.PREFERENCE_NAMES.INCLDUE_TITLES]: includeTitles, 21 | } = await shared.getPrefs() 22 | const tabCount = await shared.copyTabs(copyScope !== 'allWindows', !!includeTitles) 23 | await showNotification(tabCount, shared.ALERT_OPERATIONS.COPY) 24 | } else if (command === 'paste-tabs') { 25 | const { 26 | [shared.PREFERENCE_NAMES.BACKGROUND_PASTE]: inBackground, 27 | } = await shared.getPrefs() 28 | const tabCount = await shared.pasteTabs(!!inBackground) 29 | await showNotification(tabCount, shared.ALERT_OPERATIONS.PASTE) 30 | } 31 | } 32 | 33 | if (!browser.commands.onCommand.hasListener(commandListener)) { 34 | browser.commands.onCommand.addListener(commandListener) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/fonts.js: -------------------------------------------------------------------------------- 1 | /* eslint-env module */ 2 | import faCopy from '@fortawesome/fontawesome-free-solid/faCopy' 3 | import faPaste from '@fortawesome/fontawesome-free-solid/faPaste' 4 | import fontawesome from '@fortawesome/fontawesome' 5 | fontawesome.library.add(faCopy, faPaste) 6 | -------------------------------------------------------------------------------- /src/img/tabclip_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/src/img/tabclip_128.png -------------------------------------------------------------------------------- /src/img/tabclip_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/src/img/tabclip_16.png -------------------------------------------------------------------------------- /src/img/tabclip_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/src/img/tabclip_32.png -------------------------------------------------------------------------------- /src/img/tabclip_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshdick/tabclip/b22e5f7b313b22922fae34641f50e8fcba187348/src/img/tabclip_48.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "tabclip", 4 | "description": "Copy browser tabs to (or create them from) your clipboard.", 5 | "version": "1.4", 6 | "author": "Josh Dick", 7 | "icons": { 8 | "16": "img/tabclip_16.png", 9 | "32": "img/tabclip_32.png", 10 | "48": "img/tabclip_48.png", 11 | "128": "img/tabclip_128.png" 12 | }, 13 | "homepage_url": "https://joshdick.github.io/tabclip", 14 | "browser_action": { 15 | "default_icon": "img/tabclip_128.png", 16 | "default_popup": "popup.htm", 17 | "default_title": "tabclip" 18 | }, 19 | "background": { 20 | "page": "background.htm" 21 | }, 22 | "commands": { 23 | "copy-tabs": { 24 | "suggested_key": { 25 | "default": "Ctrl+Shift+C", 26 | "mac": "MacCtrl+Shift+C" 27 | }, 28 | "description": "Copy tab(s) to the clipboard" 29 | }, 30 | "paste-tabs": { 31 | "suggested_key": { 32 | "default": "Ctrl+Shift+V", 33 | "mac": "MacCtrl+Shift+V" 34 | }, 35 | "description": "Paste tab(s) from the clipboard" 36 | } 37 | }, 38 | "permissions": [ 39 | "clipboardRead", "clipboardWrite", "notifications", "storage", "tabs" 40 | ], 41 | "content_security_policy": "script-src 'self'; object-src 'self'" 42 | } 43 | -------------------------------------------------------------------------------- /src/popup.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | tabclip 9 | 10 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | Window(s):  47 |
48 | 49 | 50 |
51 |
52 | 53 | 54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | require('bootstrap-css-only/css/bootstrap.min.css') 2 | const shared = require('./shared') 3 | 4 | const alert = document.querySelector('#alert') 5 | const copyButton = document.querySelector('#copyButton') 6 | const pasteButton = document.querySelector('#pasteButton') 7 | const includeTitlesCheckbox = document.querySelector('#includeTitles') 8 | const backgroundPasteCheckbox = document.querySelector('#backgroundPaste') 9 | 10 | // Waiting for DOMContentLoaded allows webextension-polyfill to load 11 | document.addEventListener('DOMContentLoaded', async () => { 12 | const { 13 | [shared.PREFERENCE_NAMES.COPY_SCOPE]: copyScope, 14 | [shared.PREFERENCE_NAMES.INCLUDE_TITLES]: includeTitles, 15 | [shared.PREFERENCE_NAMES.BACKGROUND_PASTE]: backgroundPaste 16 | } = await shared.getPrefs() 17 | if (copyScope) document.querySelector(`#${copyScope}`).checked = true 18 | includeTitlesCheckbox.checked = !!includeTitles 19 | backgroundPasteCheckbox.checked = !!backgroundPaste 20 | }) 21 | 22 | const showAlert = async (quantity, operation) => { 23 | let verb 24 | switch (operation) { 25 | case shared.ALERT_OPERATIONS.COPY: 26 | verb = 'Copied' 27 | break 28 | case shared.ALERT_OPERATIONS.PASTE: 29 | verb = 'Pasted' 30 | break 31 | } 32 | alert.innerText = `${verb} ${quantity} URL${quantity === 1 ? '' : 's'}.` 33 | const className = 'show' 34 | alert.classList.add(className) 35 | await new Promise(resolve => setTimeout(resolve, 3000)) 36 | .then(() => { 37 | alert.classList.remove(className) 38 | alert.innerText = '' 39 | }) 40 | } 41 | 42 | copyButton.onclick = async () => { 43 | const currentWindow = document.querySelector('input[name="copyScope"]:checked').value === 'current' 44 | const includeTitles = includeTitlesCheckbox.checked 45 | const tabCount = await shared.copyTabs(currentWindow, includeTitles) 46 | await showAlert(tabCount, shared.ALERT_OPERATIONS.COPY) 47 | } 48 | 49 | pasteButton.onclick = async () => { 50 | const inBackground = backgroundPasteCheckbox.checked 51 | const tabCount = await shared.pasteTabs(inBackground) 52 | await showAlert(tabCount, shared.ALERT_OPERATIONS.PASTE) 53 | } 54 | 55 | backgroundPasteCheckbox.onchange = async () => { 56 | await shared.savePref(shared.PREFERENCE_NAMES.BACKGROUND_PASTE, backgroundPasteCheckbox.checked) 57 | } 58 | 59 | includeTitlesCheckbox.onchange = async () => { 60 | await shared.savePref(shared.PREFERENCE_NAMES.INCLUDE_TITLES, includeTitlesCheckbox.checked) 61 | } 62 | 63 | document.querySelectorAll('input[name="copyScope"]') 64 | .forEach(radioButton => radioButton.onchange = async (event) => { 65 | await shared.savePref(shared.PREFERENCE_NAMES.COPY_SCOPE, event.target.id) 66 | }) 67 | -------------------------------------------------------------------------------- /src/shared.js: -------------------------------------------------------------------------------- 1 | const browser = require('webextension-polyfill') 2 | const urlRegex = require('url-regex-safe') 3 | 4 | const clipboardBridge = document.querySelector('#clipboardBridge') 5 | clipboardBridge.contentEditable = true 6 | 7 | const ALERT_OPERATIONS = Object.freeze({ 8 | COPY: Symbol('copy'), 9 | PASTE: Symbol('paste') 10 | }) 11 | 12 | const readFromClipboard = async () => { 13 | let result = '' 14 | 15 | clipboardBridge.focus() 16 | document.execCommand('selectAll') 17 | document.execCommand('paste') 18 | result = clipboardBridge.innerText 19 | clipboardBridge.innerText = '' 20 | 21 | if (!result && navigator.clipboard) { 22 | try { 23 | // Can cause Chrome to block without throwing an error, 24 | // so try it only after attempting the method above 25 | result = await navigator.clipboard.readText() 26 | } catch (error) { 27 | // Disregard any error 28 | } 29 | } 30 | 31 | return result 32 | } 33 | 34 | const writeToClipboard = async (text) => { 35 | if (navigator.clipboard) { 36 | try { 37 | await navigator.clipboard.writeText(text) 38 | return 39 | } catch (error) { 40 | // Disregard any error; try alternate method below 41 | } 42 | } 43 | 44 | clipboardBridge.innerText = text 45 | clipboardBridge.focus() 46 | document.execCommand('selectAll') 47 | document.execCommand('copy') 48 | clipboardBridge.innerText = '' 49 | } 50 | 51 | const copyTabs = async (currentWindow, includeTitles) => { 52 | // Return an array where each element represents a window, 53 | // where a window is itself an array where each element is a tab. 54 | const getTabsByWindow = async () => { 55 | if (currentWindow) { 56 | const currentWindowTabs = await browser.tabs.query({ currentWindow }) 57 | return [currentWindowTabs] 58 | } else { 59 | const tabsByWindow = [] 60 | const windows = await browser.windows.getAll({ populate: true }) 61 | for (const window of windows) { 62 | const tabs = [] 63 | for (const tab of window.tabs) { 64 | tabs.push(tab) 65 | } 66 | tabsByWindow.push(tabs) 67 | } 68 | return tabsByWindow 69 | } 70 | } 71 | 72 | const tabsByWindow = await getTabsByWindow() 73 | let tabCount = 0 74 | const output = 75 | tabsByWindow.map( 76 | tabs => tabs.map(tab => { 77 | tabCount += 1 78 | const title = includeTitles ? ` | ${tab.title}` : '' 79 | return `${tab.url}${title}` 80 | }).join('\n') // Combine all tabs for one window into a string, one URL per line 81 | ).join('\n\n') // Combine each window's URL list, separating each list with an empty line 82 | await writeToClipboard(output) 83 | return tabCount 84 | } 85 | 86 | const pasteTabs = async (inBackground = false) => { 87 | const input = await readFromClipboard() 88 | const urls = input.match(urlRegex()) || [] 89 | for (const url of urls) { 90 | browser.tabs.create({ url, active: !inBackground }) 91 | } 92 | return urls.length 93 | } 94 | 95 | // User preferences 96 | 97 | const PREFERENCE_NAMES = Object.freeze({ 98 | BACKGROUND_PASTE: 'backgroundPaste', 99 | COPY_SCOPE: 'copyScope', 100 | INCLUDE_TITLES: 'includeTitles', 101 | }) 102 | 103 | const storage = browser.storage.local 104 | 105 | const savePref = async (name, value) => { 106 | await storage.set({ 107 | [name]: value 108 | }) 109 | } 110 | 111 | const getPrefs = async () => { 112 | const result = await storage.get([ 113 | PREFERENCE_NAMES.BACKGROUND_PASTE, 114 | PREFERENCE_NAMES.COPY_SCOPE, 115 | PREFERENCE_NAMES.INCLUDE_TITLES 116 | ]) 117 | return result 118 | } 119 | 120 | module.exports = { 121 | copyTabs, 122 | pasteTabs, 123 | getPrefs, 124 | savePref, 125 | PREFERENCE_NAMES, 126 | ALERT_OPERATIONS, 127 | } 128 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require('path') 4 | 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 6 | const CopyWebpackPlugin = require('copy-webpack-plugin') 7 | const ZipPlugin = require('zip-webpack-plugin') 8 | 9 | const zipPluginConfig = { 10 | exclude: ['.DS_Store'], 11 | filename: 'tabclip.zip', 12 | // yazl Options 13 | // OPTIONAL: see https://github.com/thejoshwolfe/yazl#addfilerealpath-metadatapath-options 14 | fileOptions: { 15 | mtime: new Date(), 16 | mode: 0o100664, 17 | } 18 | } 19 | 20 | module.exports = { 21 | /* 22 | mode: 'development', 23 | devtool: 'cheap-module-source-map', 24 | */ 25 | mode: 'production', 26 | entry: { 27 | background: './src/background.js', 28 | fonts: './src/fonts.js', 29 | popup: './src/popup.js', 30 | }, 31 | output: { 32 | filename: '[name].bundle.js', 33 | path: path.resolve(__dirname, 'dist') 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.css$/, 39 | use: ['style-loader', 'css-loader'] 40 | } 41 | ] 42 | }, 43 | plugins: [ 44 | new CleanWebpackPlugin(), 45 | new CopyWebpackPlugin({ 46 | patterns: [ 47 | { from: 'manifest*.json', context: 'src' }, 48 | { from: '*.htm', context: 'src' }, 49 | { from: 'img/*png', context: 'src' } 50 | ] 51 | }), 52 | new ZipPlugin(zipPluginConfig) 53 | ] 54 | } 55 | --------------------------------------------------------------------------------