├── .gitignore ├── icon ├── icon-128.png ├── icon-16.png ├── icon-256.png ├── icon-32.png ├── icon-48.png └── icon-96.png ├── resources ├── icon.xcf ├── main.gif └── popup.png ├── .gitmodules ├── .prettierrc.json ├── tsconfig.webpack.json ├── CONTRIBUTING.md ├── background.html ├── bootstrap.ts ├── content.ts ├── popup.css ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── lib.d.ts ├── manifest.json ├── CHANGELOG.md ├── storage.ts ├── LICENSE ├── webpack.config.ts ├── .eslintrc.json ├── package.json ├── popup.html ├── background.ts ├── README.md └── popup.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /icon/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/icon/icon-128.png -------------------------------------------------------------------------------- /icon/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/icon/icon-16.png -------------------------------------------------------------------------------- /icon/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/icon/icon-256.png -------------------------------------------------------------------------------- /icon/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/icon/icon-32.png -------------------------------------------------------------------------------- /icon/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/icon/icon-48.png -------------------------------------------------------------------------------- /icon/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/icon/icon-96.png -------------------------------------------------------------------------------- /resources/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/resources/icon.xcf -------------------------------------------------------------------------------- /resources/main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/resources/main.gif -------------------------------------------------------------------------------- /resources/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rhysd/monolith-of-web/HEAD/resources/popup.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "monolith"] 2 | path = monolith 3 | url = https://github.com/rhysd/monolith.git 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thank you for contributing this repository! 2 | 3 | Before contributing, please read 'Contributing' section of [README.md](./README.md). 4 | 5 | https://github.com/rhysd/monolith-of-web/blob/master/README.md#contributing 6 | -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Background page for Monolith 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /bootstrap.ts: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains any wasm must all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import('./background').catch(e => console.error('Error importing `background.js`:', e)); 5 | -------------------------------------------------------------------------------- /content.ts: -------------------------------------------------------------------------------- 1 | const html = '' + document.documentElement.outerHTML; 2 | const url = location.href; 3 | const title = document.title; 4 | const msg: MessageToPopup = { 5 | type: 'popup:content', 6 | html, 7 | title, 8 | url, 9 | }; 10 | chrome.runtime.sendMessage(msg); 11 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | main { 2 | display: flex; 3 | flex-direction: column; 4 | margin: 32px; 5 | } 6 | #error-message { 7 | margin-top: 32px; 8 | display: none; 9 | } 10 | #get-monolith-msg { 11 | margin-left: 0.3em; 12 | } 13 | .config-panel { 14 | width: 100%; 15 | display: flex; 16 | justify-content: space-around; 17 | } 18 | .config-btn { 19 | cursor: pointer; 20 | } 21 | .tooltip-bg-normal { 22 | --balloon-color: hsl(204, 86%, 53%); 23 | } 24 | .tooltip-bg-danger { 25 | --balloon-color: rgb(205, 0, 0); 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | check: 6 | name: Try build and apply eslint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Checkout submodules 11 | shell: bash 12 | run: | 13 | auth_header="$(git config --local --get http.https://github.com/.extraheader)" 14 | git submodule sync --recursive 15 | git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 16 | - uses: actions/setup-node@v1 17 | - run: npm ci 18 | - run: npm run build 19 | - run: npm run lint 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "module": "esNext", 5 | "moduleResolution": "node", 6 | "preserveConstEnums": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noEmitOnError": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "strict": true, 15 | "target": "es2019", 16 | "sourceMap": true, 17 | "esModuleInterop": true 18 | }, 19 | "files": [ 20 | "popup.ts", 21 | "bootstrap.ts", 22 | "background.ts", 23 | "content.ts", 24 | "storage.ts", 25 | "lib.d.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /lib.d.ts: -------------------------------------------------------------------------------- 1 | interface Config { 2 | noJs: boolean; 3 | noCss: boolean; 4 | noIFrames: boolean; 5 | noImages: boolean; 6 | } 7 | 8 | type MessageMonolithContent = { 9 | type: 'popup:content'; 10 | html: string; 11 | title: string; 12 | url: string; 13 | }; 14 | type MessageDownloadComplete = { 15 | type: 'popup:complete'; 16 | }; 17 | type MessageDownloadError = { 18 | type: 'popup:error'; 19 | name: string; 20 | message: string; 21 | }; 22 | type MessageToPopup = MessageMonolithContent | MessageDownloadComplete | MessageDownloadError; 23 | 24 | type MessageCreateMonolith = { 25 | type: 'bg:start'; 26 | html: string; 27 | title: string; 28 | url: string; 29 | cors: boolean; 30 | config: Config; 31 | }; 32 | type MessageToBackground = MessageCreateMonolith; 33 | 34 | type Message = MessageToPopup | MessageToBackground; 35 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Monolith", 3 | "version": "0.1.3", 4 | "description": "Get a monolith (single static HTML file) of the web page", 5 | "manifest_version": 2, 6 | "permissions": [ 7 | "activeTab", 8 | "storage" 9 | ], 10 | "optional_permissions": [ 11 | "http://*/*", 12 | "https://*/*" 13 | ], 14 | "icons": { 15 | "16": "icon/icon-16.png", 16 | "32": "icon/icon-32.png", 17 | "48": "icon/icon-48.png", 18 | "96": "icon/icon-96.png", 19 | "128": "icon/icon-128.png", 20 | "256": "icon/icon-256.png" 21 | }, 22 | "browser_action": { 23 | "default_icon": { 24 | "16": "icon/icon-16.png", 25 | "32": "icon/icon-32.png" 26 | }, 27 | "default_title": "Monolith", 28 | "default_popup": "popup.html" 29 | }, 30 | "background": { 31 | "page": "background.html", 32 | "persistent": false 33 | }, 34 | "content_security_policy": "script-src 'self' 'wasm-eval'; object-src 'self'" 35 | } 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [v0.1.2](https://github.com/rhysd/monolith-of-web/releases/tag/v0.1.2) - 12 Jan 2020 3 | 4 | - **Fix:** Handle CORS more properly 5 | - **Improve:** Update upstream monolith repo 6 | 7 | [Changes][v0.1.2] 8 | 9 | 10 | 11 | # [v0.1.1](https://github.com/rhysd/monolith-of-web/releases/tag/v0.1.1) - 03 Jan 2020 12 | 13 | - **Improve:** Insecure permissions are no longer necessary. They were removed from permissions list and now only `activeTab` is required 14 | 15 | [Changes][v0.1.1] 16 | 17 | 18 | 19 | # [v0.1.0](https://github.com/rhysd/monolith-of-web/releases/tag/v0.1.0) - 02 Jan 2020 20 | 21 | First release :tada: 22 | 23 | [Changes][v0.1.0] 24 | 25 | 26 | [v0.1.2]: https://github.com/rhysd/monolith-of-web/compare/v0.1.1...v0.1.2 27 | [v0.1.1]: https://github.com/rhysd/monolith-of-web/compare/v0.1.0...v0.1.1 28 | [v0.1.0]: https://github.com/rhysd/monolith-of-web/tree/v0.1.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /storage.ts: -------------------------------------------------------------------------------- 1 | export interface Storage { 2 | config: Config; 3 | cors: boolean; 4 | } 5 | 6 | const DEFAULT_CONFIG: Config = { 7 | noJs: false, 8 | noCss: false, 9 | noIFrames: false, 10 | noImages: false, 11 | }; 12 | const DEFAULT_CORS = false; 13 | export const DEFAULT_STORAGE: Storage = { 14 | config: DEFAULT_CONFIG, 15 | cors: DEFAULT_CORS, 16 | }; 17 | 18 | export async function loadFromStorage() { 19 | return new Promise(resolve => { 20 | chrome.storage.local.get(['config', 'cors'], items => { 21 | console.log('load!', items); 22 | resolve({ 23 | ...DEFAULT_STORAGE, 24 | ...items, 25 | }); 26 | }); 27 | }); 28 | } 29 | 30 | export async function storeToStorage(config: Config, cors: boolean) { 31 | console.log('store!', config, cors); 32 | return new Promise(resolve => { 33 | const s: Storage = { config, cors }; 34 | chrome.storage.local.set(s, resolve); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 rhysd 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from 'webpack'; 2 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 3 | import * as path from 'path'; 4 | 5 | const config: webpack.Configuration = { 6 | mode: 'development', 7 | entry: { 8 | popup: './popup.ts', 9 | bootstrap: './bootstrap.ts', 10 | content: './content.ts', 11 | }, 12 | devtool: 'inline-source-map', 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: '[name].js', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.ts$/, 21 | use: 'ts-loader', 22 | exclude: /node_modules/, 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['.ts', '.js', '.wasm'], 28 | }, 29 | plugins: [ 30 | new CopyWebpackPlugin([ 31 | 'background.html', 32 | 'node_modules/bulma/css/bulma.min.css', 33 | 'node_modules/@mdi/font/css/materialdesignicons.min.css', 34 | { 35 | from: 'node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2', 36 | to: 'fonts/', 37 | }, 38 | 'node_modules/balloon-css/balloon.min.css', 39 | { from: 'icon', to: 'icon' }, 40 | 'manifest.json', 41 | 'popup.html', 42 | 'popup.css', 43 | ]), 44 | ], 45 | devServer: { 46 | headers: { 47 | 'Access-Control-Allow-Origin': '*', 48 | }, 49 | disableHostCheck: true, 50 | writeToDisk: true, // Useful for Chrome extension 51 | }, 52 | }; 53 | 54 | export default config; 55 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "prettier/@typescript-eslint", 7 | "plugin:prettier/recommended" 8 | ], 9 | "ignorePatterns": [ 10 | "webpack.config.ts" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "project": "./tsconfig.json" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "prettier" 19 | ], 20 | "env": { 21 | "es6": true, 22 | "browser": true, 23 | "node": true 24 | }, 25 | "globals": { 26 | "chrome": "readonly" 27 | }, 28 | "rules": { 29 | "prefer-spread": "off", 30 | "@typescript-eslint/explicit-function-return-type": "off", 31 | "@typescript-eslint/explicit-member-accessibility": "off", 32 | "eqeqeq": "error", 33 | "@typescript-eslint/no-floating-promises": "error", 34 | "@typescript-eslint/no-unnecessary-type-arguments": "error", 35 | "@typescript-eslint/no-non-null-assertion": "error", 36 | "@typescript-eslint/no-empty-interface": "error", 37 | "@typescript-eslint/restrict-plus-operands": "error", 38 | "@typescript-eslint/no-extra-non-null-assertion": "error", 39 | "@typescript-eslint/prefer-nullish-coalescing": "error", 40 | "@typescript-eslint/prefer-optional-chain": "error", 41 | "@typescript-eslint/ban-ts-ignore": "error", 42 | "@typescript-eslint/prefer-includes": "error", 43 | "@typescript-eslint/prefer-for-of": "error", 44 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 45 | "@typescript-eslint/prefer-readonly": "error" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monolith-of-web", 3 | "private": true, 4 | "version": "0.1.3", 5 | "description": "", 6 | "main": "", 7 | "scripts": { 8 | "build": "TS_NODE_PROJECT=tsconfig.webpack.json webpack", 9 | "build:release": "TS_NODE_PROJECT=tsconfig.webpack.json NODE_ENV=production webpack --mode production", 10 | "clean": "rm -rf ./dist", 11 | "release": "npm-run-all clean build:release", 12 | "lint": "eslint '*.ts'", 13 | "start": "TS_NODE_PROJECT=tsconfig.webpack.json webpack-dev-server" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/rhysd/monolith-of-web.git" 18 | }, 19 | "keywords": [], 20 | "author": "rhysd ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/rhysd/monolith-of-web/issues" 24 | }, 25 | "homepage": "https://github.com/rhysd/monolith-of-web#readme", 26 | "devDependencies": { 27 | "@types/chrome": "0.0.91", 28 | "@types/copy-webpack-plugin": "^5.0.0", 29 | "@types/sanitize-filename": "^1.6.3", 30 | "@types/webpack": "^4.41.2", 31 | "@types/webpack-dev-server": "^3.9.0", 32 | "@typescript-eslint/eslint-plugin": "^2.16.0", 33 | "@typescript-eslint/parser": "^2.16.0", 34 | "copy-webpack-plugin": "^5.1.1", 35 | "eslint": "^6.8.0", 36 | "eslint-config-prettier": "^6.9.0", 37 | "eslint-plugin-prettier": "^3.1.2", 38 | "npm-run-all": "^4.1.5", 39 | "prettier": "^1.19.1", 40 | "ts-loader": "^6.2.1", 41 | "ts-node": "^8.6.2", 42 | "tsconfig-paths": "^3.9.0", 43 | "typescript": "^3.7.5", 44 | "webpack": "^4.41.5", 45 | "webpack-cli": "^3.3.12", 46 | "webpack-dev-server": "^4.11.1" 47 | }, 48 | "dependencies": { 49 | "@mdi/font": "^4.8.95", 50 | "balloon-css": "^1.0.4", 51 | "bulma": "^0.8.0", 52 | "monolith": "file:./monolith", 53 | "sanitize-filename": "^1.6.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Monolith of Web 10 | 11 | 12 |
13 | 19 |
20 |
21 |

22 | 23 |
24 |
25 |
26 |
27 |
28 | 34 | 35 | 36 | 42 | 43 | 44 | 50 | 51 | 52 | 58 | 59 | 60 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /background.ts: -------------------------------------------------------------------------------- 1 | import { monolithOfHtml, MonolithOptions } from 'monolith'; 2 | import sanitizeFileName from 'sanitize-filename'; 3 | 4 | declare global { 5 | interface Window { 6 | wasmLoadedInBackground?: boolean; 7 | } 8 | } 9 | 10 | const ANY_ORIGIN_PERMISSIONS = { permissions: [], origins: ['http://*/*', 'https://*/*'] }; 11 | 12 | function downloadURL(fileName: string, url: string) { 13 | const a = document.createElement('a'); 14 | a.download = fileName; 15 | a.href = url; 16 | a.click(); 17 | } 18 | 19 | function requestAnyOriginAccess() { 20 | return new Promise(resolve => { 21 | chrome.permissions.request(ANY_ORIGIN_PERMISSIONS, resolve); 22 | }); 23 | } 24 | 25 | function revokeAnyOriginAccess() { 26 | return new Promise(resolve => { 27 | chrome.permissions.remove(ANY_ORIGIN_PERMISSIONS, resolve); 28 | }); 29 | } 30 | 31 | async function download(msg: MessageCreateMonolith) { 32 | const granted = msg.cors && (await requestAnyOriginAccess()); 33 | console.log('Permissions for CORS request granted:', granted); 34 | 35 | const c = msg.config; 36 | console.log('Start monolith for', msg.url, 'with', c); 37 | 38 | const opts = MonolithOptions.new(); 39 | if (c.noJs) { 40 | opts.noJs(true); 41 | } 42 | if (c.noCss) { 43 | opts.noCss(true); 44 | } 45 | if (c.noIFrames) { 46 | opts.noFrames(true); 47 | } 48 | if (c.noImages) { 49 | opts.noImages(true); 50 | } 51 | 52 | const html = await monolithOfHtml(msg.html, msg.url, opts); 53 | const data = new Blob([html], { type: 'text/html' }); 54 | const obj = URL.createObjectURL(data); 55 | 56 | try { 57 | const file = `${sanitizeFileName(msg.title) || 'index'}.html`; 58 | downloadURL(file, obj); 59 | } finally { 60 | URL.revokeObjectURL(obj); 61 | if (granted) { 62 | const revoked = await revokeAnyOriginAccess(); 63 | console.log('Permissions for CORS request revoked:', revoked); 64 | } 65 | } 66 | } 67 | 68 | chrome.runtime.onMessage.addListener(async (msg: Message) => { 69 | switch (msg.type) { 70 | case 'bg:start': 71 | try { 72 | await download(msg); 73 | chrome.runtime.sendMessage({ type: 'popup:complete' }); 74 | } catch (err) { 75 | chrome.runtime.sendMessage({ 76 | type: 'popup:error', 77 | name: err.name || 'Error', 78 | message: err.message, 79 | }); 80 | } 81 | break; 82 | default: 83 | break; 84 | } 85 | }); 86 | 87 | window.wasmLoadedInBackground = true; 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 'Monolith of Web': Chrome Extension Port of [Monolith][1] 2 | ========================================================= 3 | 4 | ['Monolith of Web'][6] is a Chrome extension ported from CLI tool [Monolith][1]. Monolith is a CLI tool to 5 | download a web page as static single HTML file. 'Monolith of Web' provides the same functionality as 6 | a browser extension by compiling Monolith (written in Rust) into WebAssembly. 7 | 8 | ![usage screenshot](./resources/main.gif) 9 | 10 | ## Installation 11 | 12 | - Install from [Chrome Web Store][7] 13 | - Download `.crx` file from [releases page][5] and install it manually 14 | 15 | ## Usage 16 | 17 | ![popup screenshot](./resources/popup.png) 18 | 19 | 1. Go to a web page you want to store 20 | 2. Click 'Monolith of Web' icon in a browser bar (above popup window will open) 21 | 3. Click 'Get Monolith' button 22 | 4. Wait for the process completing 23 | 5. The generated single static HTML file is stored in your downloads folder 24 | 25 | By toggling icons at bottom of the popup window, you can determine to or not to include followings 26 | in the generated HTML file. 27 | 28 | - JavaScript 29 | - CSS 30 | - `