[
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"extends\": [\n        \"eslint:recommended\",\n        \"plugin:@typescript-eslint/recommended\",\n        \"prettier\",\n        \"prettier/@typescript-eslint\",\n        \"plugin:prettier/recommended\"\n    ],\n    \"ignorePatterns\": [\n        \"webpack.config.ts\"\n    ],\n    \"parser\": \"@typescript-eslint/parser\",\n    \"parserOptions\": {\n        \"project\": \"./tsconfig.json\"\n    },\n    \"plugins\": [\n        \"@typescript-eslint\",\n        \"prettier\"\n    ],\n    \"env\": {\n        \"es6\": true,\n        \"browser\": true,\n        \"node\": true\n    },\n    \"globals\": {\n        \"chrome\": \"readonly\"\n    },\n    \"rules\": {\n        \"prefer-spread\": \"off\",\n        \"@typescript-eslint/explicit-function-return-type\": \"off\",\n        \"@typescript-eslint/explicit-member-accessibility\": \"off\",\n        \"eqeqeq\": \"error\",\n        \"@typescript-eslint/no-floating-promises\": \"error\",\n        \"@typescript-eslint/no-unnecessary-type-arguments\": \"error\",\n        \"@typescript-eslint/no-non-null-assertion\": \"error\",\n        \"@typescript-eslint/no-empty-interface\": \"error\",\n        \"@typescript-eslint/restrict-plus-operands\": \"error\",\n        \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n        \"@typescript-eslint/prefer-nullish-coalescing\": \"error\",\n        \"@typescript-eslint/prefer-optional-chain\": \"error\",\n        \"@typescript-eslint/ban-ts-ignore\": \"error\",\n        \"@typescript-eslint/prefer-includes\": \"error\",\n        \"@typescript-eslint/prefer-for-of\": \"error\",\n        \"@typescript-eslint/prefer-string-starts-ends-with\": \"error\",\n        \"@typescript-eslint/prefer-readonly\": \"error\"\n    }\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non: [push, pull_request]\n\njobs:\n  check:\n    name: Try build and apply eslint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Checkout submodules\n        shell: bash\n        run: |\n          auth_header=\"$(git config --local --get http.https://github.com/.extraheader)\"\n          git submodule sync --recursive\n          git -c \"http.extraheader=$auth_header\" -c protocol.version=2 submodule update --init --force --recursive --depth=1\n      - uses: actions/setup-node@v1\n      - run: npm ci\n      - run: npm run build\n      - run: npm run lint\n"
  },
  {
    "path": ".gitignore",
    "content": "/node_modules\n/dist\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"monolith\"]\n\tpath = monolith\n\turl = https://github.com/rhysd/monolith.git\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"tabWidth\": 4,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"printWidth\": 120\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "<a name=\"v0.1.2\"></a>\n# [v0.1.2](https://github.com/rhysd/monolith-of-web/releases/tag/v0.1.2) - 12 Jan 2020\n\n- **Fix:** Handle CORS more properly\n- **Improve:** Update upstream monolith repo\n\n[Changes][v0.1.2]\n\n\n<a name=\"v0.1.1\"></a>\n# [v0.1.1](https://github.com/rhysd/monolith-of-web/releases/tag/v0.1.1) - 03 Jan 2020\n\n- **Improve:** Insecure permissions are no longer necessary. They were removed from permissions list and now only `activeTab` is required\n\n[Changes][v0.1.1]\n\n\n<a name=\"v0.1.0\"></a>\n# [v0.1.0](https://github.com/rhysd/monolith-of-web/releases/tag/v0.1.0) - 02 Jan 2020\n\nFirst release :tada:\n\n[Changes][v0.1.0]\n\n\n[v0.1.2]: https://github.com/rhysd/monolith-of-web/compare/v0.1.1...v0.1.2\n[v0.1.1]: https://github.com/rhysd/monolith-of-web/compare/v0.1.0...v0.1.1\n[v0.1.0]: https://github.com/rhysd/monolith-of-web/tree/v0.1.0\n\n <!-- Generated by changelog-from-release -->\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Thank you for contributing this repository!\n\nBefore contributing, please read 'Contributing' section of [README.md](./README.md).\n\nhttps://github.com/rhysd/monolith-of-web/blob/master/README.md#contributing\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2019 rhysd\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "'Monolith of Web': Chrome Extension Port of [Monolith][1]\n=========================================================\n\n['Monolith of Web'][6] is a Chrome extension ported from CLI tool [Monolith][1]. Monolith is a CLI tool to\ndownload a web page as static single HTML file. 'Monolith of Web' provides the same functionality as\na browser extension by compiling Monolith (written in Rust) into WebAssembly.\n\n![usage screenshot](./resources/main.gif)\n\n## Installation\n\n- Install from [Chrome Web Store][7]\n- Download `.crx` file from [releases page][5] and install it manually\n\n## Usage\n\n![popup screenshot](./resources/popup.png)\n\n1. Go to a web page you want to store\n2. Click 'Monolith of Web' icon in a browser bar (above popup window will open)\n3. Click 'Get Monolith' button\n4. Wait for the process completing\n5. The generated single static HTML file is stored in your downloads folder\n\nBy toggling icons at bottom of the popup window, you can determine to or not to include followings\nin the generated HTML file.\n\n- JavaScript\n- CSS\n- `<iframe/>`\n- Images\n\nThe button at right-bottom toggles if allow CORS request or not. Please read following 'Permissions'\nsection and 'CORS Requests in Background Page' section for more details.\n\n## Permissions\n\n- **Required permissions**\n  - `activeTab`: This extension gets an HTML text and a page title from the active tab to generate a monolith\n  - `storage`: This extension remembers the last state of toggle buttons at bottom in the popup window.\n- **Optional permissions**\n  - `http://*/*` and `https://*/*`: Allow any cross-origin requests in background page. This is runtime\n    permission so this extension does not require by default. **Only when you see a broken HTML file is\n    generated due to CORS error in background page, please enable this option.** The reason of these\n    permissions are explained in next 'CORS Requests in Background Page' section.\n\n## CORS Requests in Background Page\n\nThis extension generates a single HTML file in background page of Chrome extension. Since CSP in a\ncontent script is not applied in a background page, some resources in content's HTML cannot be fetched\nin background page.\n\nBy default, this extension ignores CORS errors in background page. It is usually not a problem since\nresources protected by CSP are usually scripts which don't affect main content. But a broken single HTML\npage may be generated due to CORS errors.\n\nWhen you see a broken page due to the CORS error in background page, please enable 'allow CORS requests'\nbutton at right-bottom in the popup window. Permission dialog will appear to require permissions for\nsending CORS requests in background page. After accepting it, CORS request error is disabled and all\nresources should be fetched with no error.\n\nAfter generating a single HTML file with the runtime permissions, this extension will remove the permissions\nas soon as possible for security.\n\n## Development\n\nWebAssembly port of Monolith is developed in [the forked repository][4]. Currently it has some differences\nand duplicates against the original repository. reqwest did not support Wasm before 0.10.0 so my Wasm\nport does not use it and uses `fetch()` directly via `js_sys` and `web_sys` crate.\n\nThis repository adds the forked Monolith repository as a Git submodule and uses it by bundling sources\nwith Webpack.\n\n## Contributing\n\n### Creating an issue\n\nBefore reporting an issue, please try the same URL with [CLI version][1]. If it is reproducible with\nCLI version, please report it to the CLI repository at first.\n\nIf it is not reproducible with CLI version (it means the issue only occurs with this extension), please\nreport it from [issues page][8].\n\n### Improve Wasm part\n\nThis repository only includes TypeScript part of extension. Wasm part is developed in\n[forked monolith repository][4]. If your improvement can be applied to [upstream][1], please make a\npull request in the upstream at first. After the pull request is merged, please make an issue to\nrequest to merge upstream at this repository or the forked repository.\n\n## License\n\nDistributed under [the MIT license](LICENSE).\n\n\n[1]: https://github.com/Y2Z/monolith\n[3]: https://chrome.google.com/webstore/detail/koalogomkahjlabefiglodpnhhkokekg\n[4]: https://github.com/rhysd/monolith\n[5]: https://github.com/rhysd/monolith-of-web/releases\n[6]: https://github.com/rhysd/monolith-of-web\n[7]: https://chrome.google.com/webstore/detail/monolith/koalogomkahjlabefiglodpnhhkokekg\n[8]: https://github.com/rhysd/monolith-of-web/issues\n"
  },
  {
    "path": "background.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Background page for Monolith</title>\n  </head>\n  <body>\n    <script src=\"./bootstrap.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "background.ts",
    "content": "import { monolithOfHtml, MonolithOptions } from 'monolith';\nimport sanitizeFileName from 'sanitize-filename';\n\ndeclare global {\n    interface Window {\n        wasmLoadedInBackground?: boolean;\n    }\n}\n\nconst ANY_ORIGIN_PERMISSIONS = { permissions: [], origins: ['http://*/*', 'https://*/*'] };\n\nfunction downloadURL(fileName: string, url: string) {\n    const a = document.createElement('a');\n    a.download = fileName;\n    a.href = url;\n    a.click();\n}\n\nfunction requestAnyOriginAccess() {\n    return new Promise<boolean>(resolve => {\n        chrome.permissions.request(ANY_ORIGIN_PERMISSIONS, resolve);\n    });\n}\n\nfunction revokeAnyOriginAccess() {\n    return new Promise<boolean>(resolve => {\n        chrome.permissions.remove(ANY_ORIGIN_PERMISSIONS, resolve);\n    });\n}\n\nasync function download(msg: MessageCreateMonolith) {\n    const granted = msg.cors && (await requestAnyOriginAccess());\n    console.log('Permissions for CORS request granted:', granted);\n\n    const c = msg.config;\n    console.log('Start monolith for', msg.url, 'with', c);\n\n    const opts = MonolithOptions.new();\n    if (c.noJs) {\n        opts.noJs(true);\n    }\n    if (c.noCss) {\n        opts.noCss(true);\n    }\n    if (c.noIFrames) {\n        opts.noFrames(true);\n    }\n    if (c.noImages) {\n        opts.noImages(true);\n    }\n\n    const html = await monolithOfHtml(msg.html, msg.url, opts);\n    const data = new Blob([html], { type: 'text/html' });\n    const obj = URL.createObjectURL(data);\n\n    try {\n        const file = `${sanitizeFileName(msg.title) || 'index'}.html`;\n        downloadURL(file, obj);\n    } finally {\n        URL.revokeObjectURL(obj);\n        if (granted) {\n            const revoked = await revokeAnyOriginAccess();\n            console.log('Permissions for CORS request revoked:', revoked);\n        }\n    }\n}\n\nchrome.runtime.onMessage.addListener(async (msg: Message) => {\n    switch (msg.type) {\n        case 'bg:start':\n            try {\n                await download(msg);\n                chrome.runtime.sendMessage({ type: 'popup:complete' });\n            } catch (err) {\n                chrome.runtime.sendMessage({\n                    type: 'popup:error',\n                    name: err.name || 'Error',\n                    message: err.message,\n                });\n            }\n            break;\n        default:\n            break;\n    }\n});\n\nwindow.wasmLoadedInBackground = true;\n"
  },
  {
    "path": "bootstrap.ts",
    "content": "// A dependency graph that contains any wasm must all be imported\n// asynchronously. This `bootstrap.js` file does the single async import, so\n// that no one else needs to worry about it again.\nimport('./background').catch(e => console.error('Error importing `background.js`:', e));\n"
  },
  {
    "path": "content.ts",
    "content": "const html = '<!DOCTYPE html>' + document.documentElement.outerHTML;\nconst url = location.href;\nconst title = document.title;\nconst msg: MessageToPopup = {\n    type: 'popup:content',\n    html,\n    title,\n    url,\n};\nchrome.runtime.sendMessage(msg);\n"
  },
  {
    "path": "lib.d.ts",
    "content": "interface Config {\n    noJs: boolean;\n    noCss: boolean;\n    noIFrames: boolean;\n    noImages: boolean;\n}\n\ntype MessageMonolithContent = {\n    type: 'popup:content';\n    html: string;\n    title: string;\n    url: string;\n};\ntype MessageDownloadComplete = {\n    type: 'popup:complete';\n};\ntype MessageDownloadError = {\n    type: 'popup:error';\n    name: string;\n    message: string;\n};\ntype MessageToPopup = MessageMonolithContent | MessageDownloadComplete | MessageDownloadError;\n\ntype MessageCreateMonolith = {\n    type: 'bg:start';\n    html: string;\n    title: string;\n    url: string;\n    cors: boolean;\n    config: Config;\n};\ntype MessageToBackground = MessageCreateMonolith;\n\ntype Message = MessageToPopup | MessageToBackground;\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"name\": \"Monolith\",\n  \"version\": \"0.1.3\",\n  \"description\": \"Get a monolith (single static HTML file) of the web page\",\n  \"manifest_version\": 2,\n  \"permissions\": [\n    \"activeTab\",\n    \"storage\"\n  ],\n  \"optional_permissions\": [\n    \"http://*/*\",\n    \"https://*/*\"\n  ],\n  \"icons\": {\n    \"16\": \"icon/icon-16.png\",\n    \"32\": \"icon/icon-32.png\",\n    \"48\": \"icon/icon-48.png\",\n    \"96\": \"icon/icon-96.png\",\n    \"128\": \"icon/icon-128.png\",\n    \"256\": \"icon/icon-256.png\"\n  },\n  \"browser_action\": {\n    \"default_icon\": {\n      \"16\": \"icon/icon-16.png\",\n      \"32\": \"icon/icon-32.png\"\n    },\n    \"default_title\": \"Monolith\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"background\": {\n    \"page\": \"background.html\",\n    \"persistent\": false\n  },\n  \"content_security_policy\": \"script-src 'self' 'wasm-eval'; object-src 'self'\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"monolith-of-web\",\n  \"private\": true,\n  \"version\": \"0.1.3\",\n  \"description\": \"\",\n  \"main\": \"\",\n  \"scripts\": {\n    \"build\": \"TS_NODE_PROJECT=tsconfig.webpack.json webpack\",\n    \"build:release\": \"TS_NODE_PROJECT=tsconfig.webpack.json NODE_ENV=production webpack --mode production\",\n    \"clean\": \"rm -rf ./dist\",\n    \"release\": \"npm-run-all clean build:release\",\n    \"lint\": \"eslint '*.ts'\",\n    \"start\": \"TS_NODE_PROJECT=tsconfig.webpack.json webpack-dev-server\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rhysd/monolith-of-web.git\"\n  },\n  \"keywords\": [],\n  \"author\": \"rhysd <https://rhysd.github.io>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/rhysd/monolith-of-web/issues\"\n  },\n  \"homepage\": \"https://github.com/rhysd/monolith-of-web#readme\",\n  \"devDependencies\": {\n    \"@types/chrome\": \"0.0.91\",\n    \"@types/copy-webpack-plugin\": \"^5.0.0\",\n    \"@types/sanitize-filename\": \"^1.6.3\",\n    \"@types/webpack\": \"^4.41.2\",\n    \"@types/webpack-dev-server\": \"^3.9.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^2.16.0\",\n    \"@typescript-eslint/parser\": \"^2.16.0\",\n    \"copy-webpack-plugin\": \"^5.1.1\",\n    \"eslint\": \"^6.8.0\",\n    \"eslint-config-prettier\": \"^6.9.0\",\n    \"eslint-plugin-prettier\": \"^3.1.2\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^1.19.1\",\n    \"ts-loader\": \"^6.2.1\",\n    \"ts-node\": \"^8.6.2\",\n    \"tsconfig-paths\": \"^3.9.0\",\n    \"typescript\": \"^3.7.5\",\n    \"webpack\": \"^4.41.5\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-dev-server\": \"^4.11.1\"\n  },\n  \"dependencies\": {\n    \"@mdi/font\": \"^4.8.95\",\n    \"balloon-css\": \"^1.0.4\",\n    \"bulma\": \"^0.8.0\",\n    \"monolith\": \"file:./monolith\",\n    \"sanitize-filename\": \"^1.6.3\"\n  }\n}\n"
  },
  {
    "path": "popup.css",
    "content": "main {\n    display: flex;\n    flex-direction: column;\n    margin: 32px;\n}\n#error-message {\n    margin-top: 32px;\n    display: none;\n}\n#get-monolith-msg {\n    margin-left: 0.3em;\n}\n.config-panel {\n    width: 100%;\n    display: flex;\n    justify-content: space-around;\n}\n.config-btn {\n    cursor: pointer;\n}\n.tooltip-bg-normal {\n    --balloon-color: hsl(204, 86%, 53%);\n}\n.tooltip-bg-danger {\n    --balloon-color: rgb(205, 0, 0);\n}\n"
  },
  {
    "path": "popup.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <link rel=\"stylesheet\" href=\"bulma.min.css\" />\n    <link rel=\"stylesheet\" href=\"materialdesignicons.min.css\" />\n    <link rel=\"stylesheet\" href=\"balloon.min.css\" />\n    <link rel=\"stylesheet\" href=\"popup.css\" />\n    <title>Monolith of Web</title>\n  </head>\n  <body>\n    <main>\n      <button id=\"get-monolith-btn\" class=\"button is-dark\">\n        <span class=\"icon\">\n          <i class=\"mdi mdi-arrow-down-bold-box\"></i>\n          <span id=\"get-monolith-msg\">Get Monolith</span>\n        </span>\n      </button>\n      <article id=\"error-message\" class=\"message is-danger\">\n        <div class=\"message-header\">\n          <p id=\"error-title\"></p>\n          <button id=\"error-close\" class=\"delete\" aria-label=\"delete\"></button>\n        </div>\n        <div id=\"error-body\" class=\"message-body\"></div>\n      </article>\n    </main>\n    <div class=\"config-panel\">\n      <span\n        id=\"config-js\"\n        class=\"config-btn icon is-medium tooltip-bg-normal\"\n        aria-label=\"Toggle including javascript\"\n        data-balloon-pos=\"up-left\"\n      >\n        <i class=\"mdi mdi-language-javascript mdi-24px\"></i>\n      </span>\n      <span\n        id=\"config-css\"\n        class=\"config-btn icon is-medium tooltip-bg-normal\"\n        aria-label=\"Toggle including CSS\"\n        data-balloon-pos=\"up\"\n      >\n        <i class=\"mdi mdi-language-css3 mdi-24px\"></i>\n      </span>\n      <span\n        id=\"config-iframes\"\n        class=\"config-btn icon is-medium tooltip-bg-normal\"\n        aria-label=\"Toggle including <iframe/>\"\n        data-balloon-pos=\"up\"\n      >\n        <i class=\"mdi mdi-iframe-outline mdi-24px\"></i>\n      </span>\n      <span\n        id=\"config-images\"\n        class=\"config-btn icon is-medium tooltip-bg-normal\"\n        aria-label=\"Toggle including images\"\n        data-balloon-pos=\"up\"\n      >\n        <i class=\"mdi mdi-file-image mdi-24px\"></i>\n      </span>\n      <span\n        id=\"config-allow-cors\"\n        class=\"config-btn icon is-medium tooltip-bg-danger\"\n        aria-label=\"Toggle allowing CORS requests\"\n        data-balloon-pos=\"up-right\"\n      >\n        <i class=\"mdi mdi-shield-check mdi-24px\"/></i>\n      </span>\n    </div>\n    <script src=\"./popup.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "popup.ts",
    "content": "import { loadFromStorage, storeToStorage, Storage, DEFAULT_STORAGE } from './storage';\n\ntype GetButtonState = 'normal' | 'loading' | 'success';\nclass GetButton {\n    private state: GetButtonState;\n\n    constructor(private elem: HTMLButtonElement) {\n        this.elem = elem;\n        this.state = 'normal';\n    }\n\n    clear() {\n        this.elem.classList.remove('is-loading', 'is-success');\n        this.elem.classList.add('is-dark');\n        this.setText('Get Monolith', 'arrow-down-bold-box');\n        this.state = 'normal';\n    }\n\n    startLoading() {\n        if (this.state !== 'normal') {\n            this.clear();\n        }\n        this.elem.classList.add('is-loading');\n        this.state = 'loading';\n    }\n\n    success() {\n        this.elem.classList.remove('is-dark', 'is-loading');\n        this.elem.classList.add('is-success');\n        this.setText('Success!', 'check-bold');\n        this.state = 'success';\n\n        setTimeout(() => {\n            if (this.state === 'success') {\n                this.clear();\n            }\n        }, 2000);\n    }\n\n    onClick(cb: () => void) {\n        this.elem.addEventListener('click', cb, { passive: true });\n    }\n\n    private setText(label: string, iconName: string) {\n        this.elem.innerHTML = '';\n\n        const icon = document.createElement('span');\n        icon.className = 'icon';\n        const check = document.createElement('i');\n        check.className = `mdi mdi-${iconName}`;\n        icon.appendChild(check);\n        this.elem.appendChild(icon);\n\n        const text = document.createElement('span');\n        text.innerText = label;\n        this.elem.appendChild(text);\n    }\n}\n\nclass ErrorMessage {\n    constructor(\n        private readonly container: HTMLElement,\n        private readonly title: HTMLElement,\n        private readonly body: HTMLElement,\n        closeBtn: HTMLButtonElement,\n    ) {\n        this.close = this.close.bind(this);\n        closeBtn.addEventListener('click', this.close, { passive: true });\n    }\n\n    show(title: string, message: string) {\n        this.title.innerText = title;\n        this.body.innerText = message;\n        this.container.style.display = 'block';\n    }\n\n    close() {\n        this.container.style.display = '';\n    }\n}\n\nconst COLOR_DISABLED = 'has-text-grey-light';\nclass ConfigButton {\n    constructor(private readonly elem: HTMLElement) {\n        elem.addEventListener('click', this.toggle.bind(this), { passive: true });\n    }\n\n    toggle() {\n        this.set(!this.enabled());\n    }\n\n    set(enabled: boolean) {\n        console.log('set!', enabled);\n        if (enabled) {\n            this.elem.classList.remove(COLOR_DISABLED);\n        } else {\n            this.elem.classList.add(COLOR_DISABLED);\n        }\n    }\n\n    enabled() {\n        return !this.elem.classList.contains(COLOR_DISABLED);\n    }\n}\n\nconst errorMessage = new ErrorMessage(\n    document.getElementById('error-message') as HTMLElement,\n    document.getElementById('error-title') as HTMLElement,\n    document.getElementById('error-body') as HTMLElement,\n    document.getElementById('error-close') as HTMLButtonElement,\n);\nconst getButton = new GetButton(document.getElementById('get-monolith-btn') as HTMLButtonElement);\nconst configButtons = {\n    noJs: new ConfigButton(document.getElementById('config-js') as HTMLElement),\n    noCss: new ConfigButton(document.getElementById('config-css') as HTMLElement),\n    noIFrames: new ConfigButton(document.getElementById('config-iframes') as HTMLElement),\n    noImages: new ConfigButton(document.getElementById('config-images') as HTMLElement),\n    allowCors: new ConfigButton(document.getElementById('config-allow-cors') as HTMLElement),\n};\n\ngetButton.onClick(() => {\n    getButton.startLoading();\n    chrome.tabs.executeScript({ file: 'content.js' });\n});\n\ntype BackgroundWindow = Window & {\n    wasmLoadedInBackground?: boolean;\n};\n\nfunction pollBackgroundWindowLoaded() {\n    return new Promise<boolean>(resolve => {\n        chrome.runtime.getBackgroundPage(w => {\n            resolve(!!(w as BackgroundWindow).wasmLoadedInBackground);\n        });\n    });\n}\n\nfunction sleep(ms: number) {\n    return new Promise<void>(resolve => setTimeout(resolve, ms));\n}\n\nasync function waitForBackgroundPageLoaded() {\n    // Retry for 12 * 250 = 3 seconds\n    const retries = 12;\n    const interval = 250;\n    for (let c = 0; c < retries; ++c) {\n        if (await pollBackgroundWindowLoaded()) {\n            return;\n        }\n        await sleep(interval);\n    }\n    throw new Error(\n        `No background page is open nor no background script was loaded successfully after ${retries / 4} seconds`,\n    );\n}\n\nasync function startMonolith(msg: MessageMonolithContent) {\n    const config = {\n        noJs: !configButtons.noJs.enabled(),\n        noCss: !configButtons.noCss.enabled(),\n        noIFrames: !configButtons.noIFrames.enabled(),\n        noImages: !configButtons.noImages.enabled(),\n    };\n    const cors = configButtons.allowCors.enabled();\n\n    const startMsg: MessageToBackground = {\n        ...msg,\n        type: 'bg:start',\n        config,\n        cors,\n    };\n\n    // Note: Retry is necessary since background page might not be fully opened yet.\n    // In the case, popup page must wait for the background page being loaded.\n    // When loading the background page, background.js loads Wasm file asynchronously.\n    // We need to wait for the page being fully loaded. Otherwise, the callback to\n    // receive bg:start is not set yet.\n    await waitForBackgroundPageLoaded();\n\n    // Note: Getting the background window object by chrome.runtime.getBackgroundPage()\n    // and call its method does not work. While executing JavaScript in background from\n    // popup window, chrome.permissions.request() does not work. It just fires its callback\n    // without requesting any permissions.\n    chrome.runtime.sendMessage(startMsg);\n\n    await storeToStorage(config, cors);\n}\n\nchrome.runtime.onMessage.addListener(async (msg: Message) => {\n    if (!msg.type.startsWith('popup:')) {\n        return;\n    }\n\n    switch (msg.type) {\n        case 'popup:content':\n            await startMonolith(msg);\n            break;\n        case 'popup:complete':\n            getButton.success();\n            break;\n        case 'popup:error':\n            getButton.clear();\n            errorMessage.show(msg.name || 'ERROR', msg.message);\n            break;\n        default:\n            console.error('Unexpected message:', msg);\n            break;\n    }\n});\n\nasync function setupConfigButtons() {\n    let storage: Storage;\n    try {\n        storage = await loadFromStorage();\n    } catch (err) {\n        storage = DEFAULT_STORAGE;\n    }\n\n    const { config, cors } = storage;\n    configButtons.noJs.set(!config.noJs);\n    configButtons.noCss.set(!config.noCss);\n    configButtons.noIFrames.set(!config.noIFrames);\n    configButtons.noImages.set(!config.noImages);\n    configButtons.allowCors.set(cors);\n}\n\nsetupConfigButtons().catch(err => console.error('Could not set config buttons:', err));\n"
  },
  {
    "path": "storage.ts",
    "content": "export interface Storage {\n    config: Config;\n    cors: boolean;\n}\n\nconst DEFAULT_CONFIG: Config = {\n    noJs: false,\n    noCss: false,\n    noIFrames: false,\n    noImages: false,\n};\nconst DEFAULT_CORS = false;\nexport const DEFAULT_STORAGE: Storage = {\n    config: DEFAULT_CONFIG,\n    cors: DEFAULT_CORS,\n};\n\nexport async function loadFromStorage() {\n    return new Promise<Storage>(resolve => {\n        chrome.storage.local.get(['config', 'cors'], items => {\n            console.log('load!', items);\n            resolve({\n                ...DEFAULT_STORAGE,\n                ...items,\n            });\n        });\n    });\n}\n\nexport async function storeToStorage(config: Config, cors: boolean) {\n    console.log('store!', config, cors);\n    return new Promise<void>(resolve => {\n        const s: Storage = { config, cors };\n        chrome.storage.local.set(s, resolve);\n    });\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist/\",\n    \"module\": \"esNext\",\n    \"moduleResolution\": \"node\",\n    \"preserveConstEnums\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitReturns\": true,\n    \"noImplicitThis\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noEmitOnError\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"strict\": true,\n    \"target\": \"es2019\",\n    \"sourceMap\": true,\n    \"esModuleInterop\": true\n  },\n  \"files\": [\n    \"popup.ts\",\n    \"bootstrap.ts\",\n    \"background.ts\",\n    \"content.ts\",\n    \"storage.ts\",\n    \"lib.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.webpack.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es5\",\n    \"esModuleInterop\": true\n  }\n}\n"
  },
  {
    "path": "webpack.config.ts",
    "content": "import * as webpack from 'webpack';\nimport CopyWebpackPlugin from 'copy-webpack-plugin';\nimport * as path from 'path';\n\nconst config: webpack.Configuration = {\n    mode: 'development',\n    entry: {\n        popup: './popup.ts',\n        bootstrap: './bootstrap.ts',\n        content: './content.ts',\n    },\n    devtool: 'inline-source-map',\n    output: {\n        path: path.resolve(__dirname, 'dist'),\n        filename: '[name].js',\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.ts$/,\n                use: 'ts-loader',\n                exclude: /node_modules/,\n            },\n        ],\n    },\n    resolve: {\n        extensions: ['.ts', '.js', '.wasm'],\n    },\n    plugins: [\n        new CopyWebpackPlugin([\n            'background.html',\n            'node_modules/bulma/css/bulma.min.css',\n            'node_modules/@mdi/font/css/materialdesignicons.min.css',\n            {\n                from: 'node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2',\n                to: 'fonts/',\n            },\n            'node_modules/balloon-css/balloon.min.css',\n            { from: 'icon', to: 'icon' },\n            'manifest.json',\n            'popup.html',\n            'popup.css',\n        ]),\n    ],\n    devServer: {\n        headers: {\n            'Access-Control-Allow-Origin': '*',\n        },\n        disableHostCheck: true,\n        writeToDisk: true, // Useful for Chrome extension\n    },\n};\n\nexport default config;\n"
  }
]