├── .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 |
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 | 
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 | 
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 | - ``
31 | - Images
32 |
33 | The button at right-bottom toggles if allow CORS request or not. Please read following 'Permissions'
34 | section and 'CORS Requests in Background Page' section for more details.
35 |
36 | ## Permissions
37 |
38 | - **Required permissions**
39 | - `activeTab`: This extension gets an HTML text and a page title from the active tab to generate a monolith
40 | - `storage`: This extension remembers the last state of toggle buttons at bottom in the popup window.
41 | - **Optional permissions**
42 | - `http://*/*` and `https://*/*`: Allow any cross-origin requests in background page. This is runtime
43 | permission so this extension does not require by default. **Only when you see a broken HTML file is
44 | generated due to CORS error in background page, please enable this option.** The reason of these
45 | permissions are explained in next 'CORS Requests in Background Page' section.
46 |
47 | ## CORS Requests in Background Page
48 |
49 | This extension generates a single HTML file in background page of Chrome extension. Since CSP in a
50 | content script is not applied in a background page, some resources in content's HTML cannot be fetched
51 | in background page.
52 |
53 | By default, this extension ignores CORS errors in background page. It is usually not a problem since
54 | resources protected by CSP are usually scripts which don't affect main content. But a broken single HTML
55 | page may be generated due to CORS errors.
56 |
57 | When you see a broken page due to the CORS error in background page, please enable 'allow CORS requests'
58 | button at right-bottom in the popup window. Permission dialog will appear to require permissions for
59 | sending CORS requests in background page. After accepting it, CORS request error is disabled and all
60 | resources should be fetched with no error.
61 |
62 | After generating a single HTML file with the runtime permissions, this extension will remove the permissions
63 | as soon as possible for security.
64 |
65 | ## Development
66 |
67 | WebAssembly port of Monolith is developed in [the forked repository][4]. Currently it has some differences
68 | and duplicates against the original repository. reqwest did not support Wasm before 0.10.0 so my Wasm
69 | port does not use it and uses `fetch()` directly via `js_sys` and `web_sys` crate.
70 |
71 | This repository adds the forked Monolith repository as a Git submodule and uses it by bundling sources
72 | with Webpack.
73 |
74 | ## Contributing
75 |
76 | ### Creating an issue
77 |
78 | Before reporting an issue, please try the same URL with [CLI version][1]. If it is reproducible with
79 | CLI version, please report it to the CLI repository at first.
80 |
81 | If it is not reproducible with CLI version (it means the issue only occurs with this extension), please
82 | report it from [issues page][8].
83 |
84 | ### Improve Wasm part
85 |
86 | This repository only includes TypeScript part of extension. Wasm part is developed in
87 | [forked monolith repository][4]. If your improvement can be applied to [upstream][1], please make a
88 | pull request in the upstream at first. After the pull request is merged, please make an issue to
89 | request to merge upstream at this repository or the forked repository.
90 |
91 | ## License
92 |
93 | Distributed under [the MIT license](LICENSE).
94 |
95 |
96 | [1]: https://github.com/Y2Z/monolith
97 | [3]: https://chrome.google.com/webstore/detail/koalogomkahjlabefiglodpnhhkokekg
98 | [4]: https://github.com/rhysd/monolith
99 | [5]: https://github.com/rhysd/monolith-of-web/releases
100 | [6]: https://github.com/rhysd/monolith-of-web
101 | [7]: https://chrome.google.com/webstore/detail/monolith/koalogomkahjlabefiglodpnhhkokekg
102 | [8]: https://github.com/rhysd/monolith-of-web/issues
103 |
--------------------------------------------------------------------------------
/popup.ts:
--------------------------------------------------------------------------------
1 | import { loadFromStorage, storeToStorage, Storage, DEFAULT_STORAGE } from './storage';
2 |
3 | type GetButtonState = 'normal' | 'loading' | 'success';
4 | class GetButton {
5 | private state: GetButtonState;
6 |
7 | constructor(private elem: HTMLButtonElement) {
8 | this.elem = elem;
9 | this.state = 'normal';
10 | }
11 |
12 | clear() {
13 | this.elem.classList.remove('is-loading', 'is-success');
14 | this.elem.classList.add('is-dark');
15 | this.setText('Get Monolith', 'arrow-down-bold-box');
16 | this.state = 'normal';
17 | }
18 |
19 | startLoading() {
20 | if (this.state !== 'normal') {
21 | this.clear();
22 | }
23 | this.elem.classList.add('is-loading');
24 | this.state = 'loading';
25 | }
26 |
27 | success() {
28 | this.elem.classList.remove('is-dark', 'is-loading');
29 | this.elem.classList.add('is-success');
30 | this.setText('Success!', 'check-bold');
31 | this.state = 'success';
32 |
33 | setTimeout(() => {
34 | if (this.state === 'success') {
35 | this.clear();
36 | }
37 | }, 2000);
38 | }
39 |
40 | onClick(cb: () => void) {
41 | this.elem.addEventListener('click', cb, { passive: true });
42 | }
43 |
44 | private setText(label: string, iconName: string) {
45 | this.elem.innerHTML = '';
46 |
47 | const icon = document.createElement('span');
48 | icon.className = 'icon';
49 | const check = document.createElement('i');
50 | check.className = `mdi mdi-${iconName}`;
51 | icon.appendChild(check);
52 | this.elem.appendChild(icon);
53 |
54 | const text = document.createElement('span');
55 | text.innerText = label;
56 | this.elem.appendChild(text);
57 | }
58 | }
59 |
60 | class ErrorMessage {
61 | constructor(
62 | private readonly container: HTMLElement,
63 | private readonly title: HTMLElement,
64 | private readonly body: HTMLElement,
65 | closeBtn: HTMLButtonElement,
66 | ) {
67 | this.close = this.close.bind(this);
68 | closeBtn.addEventListener('click', this.close, { passive: true });
69 | }
70 |
71 | show(title: string, message: string) {
72 | this.title.innerText = title;
73 | this.body.innerText = message;
74 | this.container.style.display = 'block';
75 | }
76 |
77 | close() {
78 | this.container.style.display = '';
79 | }
80 | }
81 |
82 | const COLOR_DISABLED = 'has-text-grey-light';
83 | class ConfigButton {
84 | constructor(private readonly elem: HTMLElement) {
85 | elem.addEventListener('click', this.toggle.bind(this), { passive: true });
86 | }
87 |
88 | toggle() {
89 | this.set(!this.enabled());
90 | }
91 |
92 | set(enabled: boolean) {
93 | console.log('set!', enabled);
94 | if (enabled) {
95 | this.elem.classList.remove(COLOR_DISABLED);
96 | } else {
97 | this.elem.classList.add(COLOR_DISABLED);
98 | }
99 | }
100 |
101 | enabled() {
102 | return !this.elem.classList.contains(COLOR_DISABLED);
103 | }
104 | }
105 |
106 | const errorMessage = new ErrorMessage(
107 | document.getElementById('error-message') as HTMLElement,
108 | document.getElementById('error-title') as HTMLElement,
109 | document.getElementById('error-body') as HTMLElement,
110 | document.getElementById('error-close') as HTMLButtonElement,
111 | );
112 | const getButton = new GetButton(document.getElementById('get-monolith-btn') as HTMLButtonElement);
113 | const configButtons = {
114 | noJs: new ConfigButton(document.getElementById('config-js') as HTMLElement),
115 | noCss: new ConfigButton(document.getElementById('config-css') as HTMLElement),
116 | noIFrames: new ConfigButton(document.getElementById('config-iframes') as HTMLElement),
117 | noImages: new ConfigButton(document.getElementById('config-images') as HTMLElement),
118 | allowCors: new ConfigButton(document.getElementById('config-allow-cors') as HTMLElement),
119 | };
120 |
121 | getButton.onClick(() => {
122 | getButton.startLoading();
123 | chrome.tabs.executeScript({ file: 'content.js' });
124 | });
125 |
126 | type BackgroundWindow = Window & {
127 | wasmLoadedInBackground?: boolean;
128 | };
129 |
130 | function pollBackgroundWindowLoaded() {
131 | return new Promise(resolve => {
132 | chrome.runtime.getBackgroundPage(w => {
133 | resolve(!!(w as BackgroundWindow).wasmLoadedInBackground);
134 | });
135 | });
136 | }
137 |
138 | function sleep(ms: number) {
139 | return new Promise(resolve => setTimeout(resolve, ms));
140 | }
141 |
142 | async function waitForBackgroundPageLoaded() {
143 | // Retry for 12 * 250 = 3 seconds
144 | const retries = 12;
145 | const interval = 250;
146 | for (let c = 0; c < retries; ++c) {
147 | if (await pollBackgroundWindowLoaded()) {
148 | return;
149 | }
150 | await sleep(interval);
151 | }
152 | throw new Error(
153 | `No background page is open nor no background script was loaded successfully after ${retries / 4} seconds`,
154 | );
155 | }
156 |
157 | async function startMonolith(msg: MessageMonolithContent) {
158 | const config = {
159 | noJs: !configButtons.noJs.enabled(),
160 | noCss: !configButtons.noCss.enabled(),
161 | noIFrames: !configButtons.noIFrames.enabled(),
162 | noImages: !configButtons.noImages.enabled(),
163 | };
164 | const cors = configButtons.allowCors.enabled();
165 |
166 | const startMsg: MessageToBackground = {
167 | ...msg,
168 | type: 'bg:start',
169 | config,
170 | cors,
171 | };
172 |
173 | // Note: Retry is necessary since background page might not be fully opened yet.
174 | // In the case, popup page must wait for the background page being loaded.
175 | // When loading the background page, background.js loads Wasm file asynchronously.
176 | // We need to wait for the page being fully loaded. Otherwise, the callback to
177 | // receive bg:start is not set yet.
178 | await waitForBackgroundPageLoaded();
179 |
180 | // Note: Getting the background window object by chrome.runtime.getBackgroundPage()
181 | // and call its method does not work. While executing JavaScript in background from
182 | // popup window, chrome.permissions.request() does not work. It just fires its callback
183 | // without requesting any permissions.
184 | chrome.runtime.sendMessage(startMsg);
185 |
186 | await storeToStorage(config, cors);
187 | }
188 |
189 | chrome.runtime.onMessage.addListener(async (msg: Message) => {
190 | if (!msg.type.startsWith('popup:')) {
191 | return;
192 | }
193 |
194 | switch (msg.type) {
195 | case 'popup:content':
196 | await startMonolith(msg);
197 | break;
198 | case 'popup:complete':
199 | getButton.success();
200 | break;
201 | case 'popup:error':
202 | getButton.clear();
203 | errorMessage.show(msg.name || 'ERROR', msg.message);
204 | break;
205 | default:
206 | console.error('Unexpected message:', msg);
207 | break;
208 | }
209 | });
210 |
211 | async function setupConfigButtons() {
212 | let storage: Storage;
213 | try {
214 | storage = await loadFromStorage();
215 | } catch (err) {
216 | storage = DEFAULT_STORAGE;
217 | }
218 |
219 | const { config, cors } = storage;
220 | configButtons.noJs.set(!config.noJs);
221 | configButtons.noCss.set(!config.noCss);
222 | configButtons.noIFrames.set(!config.noIFrames);
223 | configButtons.noImages.set(!config.noImages);
224 | configButtons.allowCors.set(cors);
225 | }
226 |
227 | setupConfigButtons().catch(err => console.error('Could not set config buttons:', err));
228 |
--------------------------------------------------------------------------------