├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── docs.yml ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── config.toml ├── content │ ├── _index.md │ ├── getting-started │ │ ├── _index.md │ │ ├── overview.md │ │ └── userchrome.md │ └── guides │ │ ├── _index.md │ │ ├── includingAddons.md │ │ └── removingPocket.md ├── sass │ ├── _search.scss │ ├── _variables.scss │ ├── fabric-icons-inline.scss │ └── main.scss ├── static │ ├── images │ │ └── userchrome │ │ │ ├── browser-toolbox.png │ │ │ ├── css-basic.png │ │ │ ├── css-final.png │ │ │ └── css-square-tabs.png │ ├── js.js │ └── logo.svg ├── templates │ ├── anchor-link.html │ ├── index.html │ └── page.html └── theme.toml ├── package.json ├── src ├── cmds.ts ├── commands │ ├── bootstrap.ts │ ├── bootstrap │ │ ├── arch.ts │ │ ├── debian.ts │ │ └── macos.ts │ ├── build.ts │ ├── discard.ts │ ├── download-artifacts.ts │ ├── download.ts │ ├── execute.ts │ ├── export-file.ts │ ├── fix-le.ts │ ├── import-patches.ts │ ├── index.ts │ ├── init.ts │ ├── license-check.ts │ ├── linus.d.ts │ ├── package.ts │ ├── reset.ts │ ├── run.ts │ ├── set-branch.ts │ ├── setupProject.ts │ ├── status.ts │ └── test.ts ├── constants │ └── index.ts ├── controllers │ ├── brandingPatch.ts │ └── patch.ts ├── index.ts ├── interfaces │ └── patch.ts ├── log.ts ├── manual-patches.ts ├── middleware │ ├── patch-check.ts │ ├── registerCommand.ts │ ├── sha-check.ts │ └── update-check.ts ├── types.d.ts └── utils │ ├── config.ts │ ├── delay.ts │ ├── dispatch.ts │ ├── download.ts │ ├── error-handler.ts │ ├── fs.ts │ ├── import.ts │ ├── index.ts │ ├── stringTemplate.ts │ ├── version.ts │ └── write-metadata.ts ├── template ├── .vscode │ └── settings.json ├── branding.optional │ ├── brand.dtd │ ├── brand.ftl │ └── brand.properties ├── configs │ ├── common │ │ └── mozconfig │ ├── linux │ │ ├── build_linux.sh │ │ ├── linux.dockerfile │ │ ├── mozconfig │ │ └── mozconfig-i686 │ ├── macos │ │ ├── build_macos.sh │ │ ├── macos.dockerfile │ │ ├── mozconfig │ │ └── mozconfig-i686 │ └── windows │ │ ├── mozconfig │ │ └── mozconfig-i686 └── src │ ├── README.md │ ├── browser │ ├── confvars-sh.patch.optional │ └── themes.optional │ │ ├── custom │ │ ├── linux │ │ │ └── linux.inc.css │ │ ├── macos │ │ │ └── macos.inc.css │ │ ├── shared │ │ │ └── shared.inc.css │ │ └── windows │ │ │ └── windows.inc.css │ │ ├── linux │ │ └── browser-css.patch │ │ ├── osx │ │ └── browser-css.patch │ │ ├── shared │ │ └── browser-inc-css.patch │ │ └── windows │ │ └── browser-css.patch │ ├── customui.optional │ ├── browser.html │ ├── css │ │ └── browser.css │ ├── jar.mn │ ├── moz.build │ └── scripts │ │ ├── browser.js │ │ └── devtools.js │ └── toolkit │ └── toolkit-mozbuild.patch.optional ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | template/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "eslint-config-prettier" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["@typescript-eslint"], 17 | "rules": {} 18 | } 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: trickypr 4 | liberapay: dothq 5 | patreon: dothq 6 | custom: https://store.dothq.co 7 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build docs 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'docs/**' 7 | - '.github/**' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docs: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 🛎️ 15 | uses: actions/checkout@v2.3.1 16 | 17 | - name: Deploy docs 18 | uses: shalzz/zola-deploy-action@v0.13.0 19 | env: 20 | # Target branch 21 | PAGES_BRANCH: gh-pages 22 | BUILD_DIR: docs/ 23 | # Provide personal access token 24 | TOKEN: ${{ secrets.TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | testing/ 107 | 108 | site/ 109 | 110 | docs/public/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | testing/ 2 | node_modules/ 3 | src/ 4 | !template/src/ 5 | docs/ 6 | site/ 7 | 8 | .prettierrc.json 9 | melon.json 10 | tsconfig.json 11 | yarn.lock 12 | .eslintignore 13 | .eslintrc.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitmoji.additionalEmojis": [ 3 | { 4 | "emoji": "🔄", 5 | "code": ":arrows_counterclockwise:", 6 | "description": "Update changelog" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.0.0-a.11] 11 | 12 | ### Fixed 13 | 14 | - Put the copy inputs around the right way 15 | 16 | ## [1.0.0-a.9] 17 | 18 | ### Fixed 19 | 20 | - Work around windows users not having the permissions to create symbolic links 21 | 22 | ## [1.0.0-a.8] 23 | 24 | ### Fixed 25 | 26 | - Windows tar compatibility 27 | - Make the progress bar be less flickery 28 | - `fs-extra` is being used for symlinks for cross-platform compatibility 29 | 30 | ### Changed 31 | 32 | - Windows now uses the same init script as everyone else 33 | 34 | ## [1.0.0-a.7] 35 | 36 | ### Fixed 37 | 38 | - Stop updates from crashing melon 39 | - Correct the api backend for updates 40 | - Stop crashes when symlinks don't exist 41 | - Error outputs are cleaner 42 | - Branding generators do not crash as much 43 | - Partly inited engine folders will not trigger unusual errors 44 | - Better error output when `bsdtar` causes errors 45 | 46 | ### Changed 47 | 48 | - The status command is now mildly more helpful 49 | - Removed unnecessary delays, improving performance 50 | - `export` is now an alias for `export-file` 51 | - Discard command now uses git instead of trying to download from firefox's source 52 | - Add the possibility of multiple branding types 53 | - Branding generator will regenerate locale files if the correct options are specified 54 | - Branding generator creates custom color for about page 55 | 56 | ### Added 57 | 58 | - Debian bootstrap 59 | - Verbose logging mode 60 | - Addons can be included 61 | 62 | ### Removed 63 | 64 | - `fs-extra` is no longer a depenancy 65 | 66 | ## [1.0.0-a.6] 67 | 68 | ### Fixed 69 | 70 | - Builds do not double count `#!/bin/node` 71 | - Run branding generator as a patch 72 | 73 | ## [1.0.0-a.5] 74 | 75 | ### Added 76 | 77 | - Userchrome docs 78 | - Basic branding generator 79 | 80 | ## [1.0.0-a.4] 81 | 82 | ### Added 83 | 84 | - Open devtools and reload button for custom UI 85 | 86 | ### Fixed 87 | 88 | - Theming patches don't cause errors now 89 | - Custom UI template compiles and runs 90 | 91 | ### Removed 92 | 93 | - Remove the melon executable from the template 94 | 95 | ## [1.0.0-a.3] 96 | 97 | ### Fixed 98 | 99 | - Include `template/src/` 100 | 101 | ## [1.0.0-a.2] 102 | 103 | ### Added 104 | 105 | - Initial beta release 106 | 107 | [unreleased]: https://github.com/dothq/melon/compare/v1.0.0-a.9...HEAD 108 | [1.0.0-a.9]: https://github.com/dothq/melon/compare/v1.0.0-a.8...v1.0.0-a.9 109 | [1.0.0-a.8]: https://github.com/dothq/melon/compare/v1.0.0-a.7...v1.0.0-a.8 110 | [1.0.0-a.7]: https://github.com/dothq/melon/compare/v1.0.0-a.6...v1.0.0-a.7 111 | [1.0.0-a.6]: https://github.com/dothq/melon/compare/v1.0.0-a.5...v1.0.0-a.6 112 | [1.0.0-a.5]: https://github.com/dothq/melon/compare/v1.0.0-a.4...v1.0.0-a.5 113 | [1.0.0-a.4]: https://github.com/dothq/melon/compare/v1.0.0-a.3...v1.0.0-a.4 114 | [1.0.0-a.3]: https://github.com/dothq/melon/compare/v1.0.0-a.2...v1.0.0-a.3 115 | [1.0.0-a.2]: https://github.com/dothq/melon/compare/v1.0.0-a.1...v1.0.0-a.2 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Depreciation 2 | 3 | This project is no longer used or maintained by Dot HQ. The [Pulse Browser](https://pulsebrowser.app/) developers [maintain and support a fork](https://github.com/pulse-browser/gluon). 4 | 5 |
6 | 7 |

8 | 9 |

10 | 11 | # Melon 12 | 13 | Build Firefox-based browsers with ease 14 | 15 | **This is still in a prerelease / prototype phase. Changes will be made, things will be broken** 16 | 17 |
18 | 19 | ## Installation 20 | 21 | Per project (recommended): 22 | 23 | ```sh 24 | npm install melon-build 25 | # or 26 | yarn add melon-build 27 | ``` 28 | 29 | Globally: 30 | 31 | ```sh 32 | npm install -g melon-build 33 | # or 34 | yarn global add melon-build 35 | 36 | # Note: Linux and mac users may have to run the above command with sudo 37 | ``` 38 | 39 | ## Documentation 40 | 41 | Documentation is available on [Github pages](https://dothq.github.io/melon/) or in the docs folder of this repository. 42 | 43 | ## Licencing notes 44 | 45 | The following is included in good faith. The writer is not a lawyer, and this is not legal advice. 46 | 47 | ### Melon 48 | 49 | Melon has been extracted from the [desktop version of Dot Browser](https://github.com/dothq/browser-desktop) under MPL v2.0. 50 | 51 | This Source Code Form is subject to the terms of the Mozilla Public 52 | License, v. 2.0. If a copy of the MPL was not distributed with this 53 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 54 | 55 | ### Firefox 56 | 57 | This program downloads and modifies Firefox. [Follow their license](https://hg.mozilla.org/mozilla-central/file/tip/LICENSE) when distributing your program. 58 | 59 | ### Tweemoji 60 | 61 | The melon icon is from tweemoji. 62 | 63 | Copyright 2020 Twitter, Inc and other contributors 64 | Code licensed under the MIT License: http://opensource.org/licenses/MIT 65 | Graphics licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/ 66 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | base_url = "/melon/" 2 | title = "Melon docs" 3 | compile_sass = true 4 | build_search_index = true 5 | 6 | [markdown] 7 | highlight_code = true 8 | highlight_theme = "base16-ocean-dark" 9 | 10 | [extra] 11 | logo = "https://camo.githubusercontent.com/d8b2046aa769e171cb4c89d0f86dfb52c7f677ff287c2b5ac9451ae3d45b2ef3/68747470733a2f2f7477656d6f6a692e6d617863646e2e636f6d2f762f31332e302e312f7376672f31663334392e737667" 12 | release = "https://api.github.com/repos/dothq/melon/releases/latest" 13 | favicon = "https://www.dothq.co/favicon.png" 14 | -------------------------------------------------------------------------------- /docs/content/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "index" 3 | insert_anchor_links = "right" 4 | +++ 5 | 6 | ## Welcome to the melon docs 7 | 8 | If you are new here, you should read the [Getting Started](./getting-started/overview/) guide. 9 | -------------------------------------------------------------------------------- /docs/content/getting-started/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Getting Started" 3 | weight = 1 4 | sort_by = "weight" 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/content/getting-started/overview.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Overview" 3 | weight = 5 4 | +++ 5 | 6 | ## Getting started with melon 7 | 8 | ### What is melon 9 | 10 | Melon is a build tool and documentation for creating firefox-based browsers. Its goal is to simplify the process of creating web browsers to encourage competition and development within the space. 11 | 12 | ### Getting help 13 | 14 | If you are having problems with following these instructions, or with melon in general, please contact us. You can [create a discussion on github](https://github.com/dothq/melon/discussions/new), ping @trickypr on the [Dot HQ discord](https://dothq.link/dsc), or [join our Matrix chat](https://dothq.link/matrix). 15 | 16 | ### System requirements 17 | 18 | - **OS**: Linux, Windows, MacOS (We only have active contributors on linux, so other platforms might be a touch buggy) 19 | - **Melon dependencies**: NodeJS and npm 20 | - **Browser dependencies**: TODO: find out what firefox's build dependencies are 21 | 22 | ### Getting started 23 | 24 | The first thing you are going to need to do is to install melon. As it is a nodejs program it can be installed through npm or yarn. 25 | 26 | ```sh 27 | npm install -g melon-build 28 | # or 29 | yarn global add melon-build 30 | 31 | # Note: Linux and mac users may have to run the above command with sudo 32 | ``` 33 | 34 | Now create a git repo and clone it to your local machine. Then run `melon setup-project` inside of that repo. This will ask you a variety of questions in relation to your project setup. Firstly, the release of the browser you want to bind to. 35 | 36 | - `Firefox nightly`: Updates every 12 hours, making it almost impossible to keep up to date **(not recommended)** 37 | - `Firefox beta`: Updates every 4 weeks. It will have unresolved bugs **(not recommended)** 38 | - `Firefox developer edition`: Tracks firefox beta **(not recommended)** 39 | - `Firefox stable`: Releases around every 4 weeks, however has most of the bugs from beta fixed 40 | - `Firefox extended support release (newer)`: The latest extended support release. Releases around once every 8 stable cycles (mozilla isn't clear on this). Receives regular small security patches and bug fixes, but no large breaking changes (e.g. [proton](https://www.omgubuntu.co.uk/2021/02/try-firefox-proton-redesign-ubuntu)) between releases. 41 | - `Firefox extended support release (newer)`: The oldest supported extended support release. Maximum security and stability, but will lose support sooner than the newer extended support release. 42 | 43 | Dot browser currently uses the stable releases, and keeping up to date can be a struggle with a small development team. 44 | 45 | Then next is the version of the browser you want to use. By default melon will populate this with the latest version available, which we recommend using. 46 | 47 | Next it will ask for the name of your browser. Avoid references to firefox or other mozilla brands if you can. 48 | 49 | Vendor is the company (or solo developer) who is creating the browser. 50 | 51 | The appid follows reverse dns naming conventions. For example, DotHQ owns the domain `dothq.co`, so our browser is `co.dothq.browser`. If you do not have a domain, you can use your username / psudomim as the appid, e.g. `trickypr.watermelon`. 52 | 53 | Next you need to chose a starting template for your browser. You can go with userchrome, where you apply css changes to firefox or custom html, where you have to write everything (tabs, navigation, search boxes) yourself. We generally recommend userchrome for new users, as it has the lowest learning curve. Additionally, you can chose to use no template. 54 | 55 | Now you have created the directory structure for your project, you can build it for the first time. First, ask melon to download the firefox source. 56 | 57 | ```sh 58 | melon download 59 | ``` 60 | 61 | After the source code has been downloaded, the changes to firefox described in the source code must be applied. 62 | 63 | ```sh 64 | melon import 65 | ``` 66 | 67 | Finally, you can start building the firefox source code. This takes around an hour and a half on my computer, but the binary output will be cached, making later builds faster 68 | 69 | ```sh 70 | melon build 71 | ``` 72 | 73 | Now you can finally start the browser! 74 | 75 | ```sh 76 | melon run 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/content/getting-started/userchrome.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Userchrome" 3 | weight = 10 4 | +++ 5 | 6 | This page will explain the process for applying custom css (or userchrome) to your new browser. I expect you to have already setup melon as described in the overview and have something that looks like the following on your screen. 7 | 8 | ![Firefox build without branding](https://cdn.statically.io/img/dothq.github.io/f=auto/melon/images/userchrome/css-basic.png) 9 | 10 | The firefox window shown above is constructed from (x)html, styled with css and made dynamic with javascript. This means that the entire browser can be styled with custom css, called userchrome. 11 | 12 | If you selected the userchrome option when setting up the project, melon will have already created the theme files for you. `src/browser/themes/custom/shared/shared.inc.css` will be included on all platforms, whilst platform specific styles will be included from similar files in `src/browser/themes/custom`. 13 | 14 | Additionally, firefox has an equivalent to "inspect element", but for the browser. Click on the hamburger menu, select "More tools", then "Browser toolbox" to open it. 15 | 16 | ![Browser toolbox](https://cdn.statically.io/img/dothq.github.io/f=auto/melon/images/userchrome/browser-toolbox.png) 17 | 18 | ## A touch of design 19 | 20 | This tutorial will attempt to replicate the design of [SimpleFox by Miguel R. Ávila](https://github.com/migueravila/SimpleFox), without copying its code. I would recommend creating your own visual identity for your browser. 21 | 22 | ## Squaring the tabs 23 | 24 | Firefox's proton made the tabs hover, with mixed reception. Let's reverse that. 25 | 26 | Using the select tool (top left of the browser toolbox) select the active tab and look for what element provides the background. In this case it is the `.tab-background` element. 27 | 28 | You can scroll down to find the code where the border radius is set. In firefox 91, this is: 29 | 30 | ```css 31 | .tab-background { 32 | border-radius: var(--tab-border-radius); 33 | margin-block: var(--tab-block-margin); 34 | } 35 | ``` 36 | 37 | Firefox uses css variables for a lot of its properties, meaning we can make the tabs square by setting the border radius to 0. Here, the margin, which makes the tabs "float is set", so setting it to zero will cause them to stop floating. This can be done by adding the following line to `src/browser/themes/custom/shared/shared.inc.css`: 38 | 39 | ```css 40 | :root { 41 | --tab-border-radius: 0 !important; 42 | --tab-block-margin: 0 !important; 43 | } 44 | ``` 45 | 46 | Rebuilding the browser, the tabs are now slightly closer to how we want them. 47 | 48 | ![Squared tabs](https://cdn.statically.io/img/dothq.github.io/f=auto/melon/images/userchrome/css-square-tabs.png) 49 | 50 | There is this weird padding to the left of the active tab. This is caused by the following css: 51 | 52 | ```css 53 | .tabbrowser-tab { 54 | min-height: var(--tab-min-height); 55 | padding-inline: 2px !important; 56 | } 57 | ``` 58 | 59 | As mozilla are using `!important` here, we have to use [css priority](https://marksheet.io/css-priority.html) to override it, rather than simply creating our own style with `!important`. 60 | 61 | ```css 62 | #tabbrowser-arrowscrollbox .tabbrowser-tab { 63 | padding-inline: 0 !important; 64 | } 65 | ``` 66 | 67 | Now, I want to remove the "Nightly" pill in the search bar, along with the background of it. Using the browser toolbox, we can figure out that we have to hide `#identity-icon-box`, remove the border on `#urlbar-background` and set `--toolbar-field-background-color` to the value of `--toolbar-bgcolor`. 68 | 69 | ![Final browser](https://cdn.statically.io/img/dothq.github.io/f=auto/melon/images/userchrome/css-final.png) 70 | 71 | I encourage you to experiment and customize your browser to fit what you want your browser to be. 72 | 73 | The source code for this tutorial can be found [here](https://github.com/trickypr/watermelon) 74 | -------------------------------------------------------------------------------- /docs/content/guides/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Guides" 3 | weight = 2 4 | sort_by = "weight" 5 | +++ 6 | -------------------------------------------------------------------------------- /docs/content/guides/includingAddons.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Including addons" 3 | weight = 10 4 | +++ 5 | 6 | # Including addons 7 | 8 | Melon provides an automated system for including extensions in your project. The addons are downloaded and included during the `download` build step. Addons can be included in the project config (`melon.json`). 9 | 10 | ```json 11 | { 12 | // Your options here 13 | "addons": { 14 | "ublock": { 15 | "id": "uBlock0@raymondhill.net", 16 | "url": "https://github.com/gorhill/uBlock/releases/download/1.39.0/uBlock0_1.39.0.firefox.xpi" 17 | } 18 | } 19 | } 20 | ``` 21 | 22 | Note that the `id` is the gecko application id specified in the `manifest.json`. 23 | 24 | ```json 25 | { 26 | // ... 27 | 28 | "browser_specific_settings": { 29 | "gecko": { 30 | "id": "uBlock0@raymondhill.net", 31 | "strict_min_version": "60.0" 32 | } 33 | } 34 | 35 | // ... 36 | } 37 | ``` 38 | 39 | ## Specifying location in customizable ui 40 | 41 | By default, when an addon with a toolbar button, it will placed next to the hamburger menu. However, you may want to place it somewhere else. To do this, you must change the customizable ui in a similar way to how you would to remove pocket. 42 | 43 | You are going to want to open `engine/browser/components/customizableui/CustomizableUI.jsm`. At the top, you want to import the `ExtensionCommon` module. 44 | 45 | ```js 46 | const { makeWidgetId } = ChromeUtils.import( 47 | 'resource://gre/modules/ExtensionCommon.jsm' 48 | ).ExtensionCommon 49 | ``` 50 | 51 | Then, at the top add a constant with the id of the addon at the top of the file, for example: 52 | 53 | ```js 54 | const kUBlockOriginID = 'uBlock0@raymondhill.net' 55 | ``` 56 | 57 | Now, you can go down to the `navbarPlacements` array (around line 240) and add 58 | 59 | ```js 60 | `${makeWidgetId(kUBlockOriginID)}-browser-action`, 61 | ``` 62 | 63 | To the array where you want the icon to appear, for example: 64 | 65 | ```js 66 | let navbarPlacements = [ 67 | 'back-button', 68 | 'forward-button', 69 | 'stop-reload-button', 70 | Services.policies.isAllowed('removeHomeButtonByDefault') 71 | ? null 72 | : 'home-button', 73 | 'spring', 74 | `${makeWidgetId(kUBlockOriginID)}-browser-action`, 75 | 'urlbar-container', 76 | 'spring', 77 | 'save-to-pocket-button', 78 | 'downloads-button', 79 | AppConstants.MOZ_DEV_EDITION ? 'developer-button' : null, 80 | 'fxa-toolbar-menu-button', 81 | ].filter((name) => name) 82 | ``` 83 | 84 | Finally, export the changes you have made: 85 | 86 | ```sh 87 | melon export-file browser/components/customizableui/CustomizableUI.jsm 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/content/guides/removingPocket.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Removing pocket" 3 | weight = 5 4 | +++ 5 | 6 | # Removing pocket 7 | 8 | **Note:** This expects you have melon setup. 9 | 10 | ## Disabling in firefox.js 11 | 12 | The goal of this guide is to disable pocket and remove its icon from the toolbar. The first changes we will need to make is to the firefox.js file located in `engine/browser/app/profile/firefox.js`. Scroll to the lines that include the following settings (around line 1980 in firefox 94): 13 | 14 | ```js 15 | pref('extensions.pocket.api', 'api.getpocket.com') 16 | pref('extensions.pocket.enabled', true) 17 | pref('extensions.pocket.oAuthConsumerKey', '40249-e88c401e1b1f2242d9e441c4') 18 | pref('extensions.pocket.site', 'getpocket.com') 19 | pref('extensions.pocket.onSaveRecs', true) 20 | pref('extensions.pocket.onSaveRecs.locales', 'en-US,en-GB,en-CA') 21 | ``` 22 | 23 | Delete these lines and replace them with the following: 24 | 25 | ```js 26 | // Taken from BetterFox user.js 27 | user_pref('extensions.pocket.enabled', false) 28 | user_pref('extensions.pocket.api', ' ') 29 | user_pref('extensions.pocket.oAuthConsumerKey', ' ') 30 | user_pref('extensions.pocket.site', ' ') 31 | ``` 32 | 33 | Next, you will need to remove pocket from the new tab page. You can do this by simply adding the following line to the bottom of `firefox.js`: 34 | 35 | ```js 36 | user_pref( 37 | 'browser.newtabpage.activity-stream.section.highlights.includePocket', 38 | false 39 | ) 40 | ``` 41 | 42 | Now you simply need to export the changes made to `firefox.js`: 43 | 44 | ```sh 45 | melon export-file browser/app/profile/firefox.js 46 | ``` 47 | 48 | ## Removing pocket icon from toolbar 49 | 50 | Whilst the steps above will have disabled pocket. The pocket icon will still be visible in the toolbar. Instead you must remove it from the CustomizableUI layout. Open `engine/browser/components/customizableui/CustomizableUI.jsm` and find the array that looks like this (around line 240): 51 | 52 | ```js 53 | let navbarPlacements = [ 54 | 'back-button', 55 | 'forward-button', 56 | 'stop-reload-button', 57 | Services.policies.isAllowed('removeHomeButtonByDefault') 58 | ? null 59 | : 'home-button', 60 | 'spring', 61 | 'urlbar-container', 62 | 'spring', 63 | 'save-to-pocket-button', 64 | 'downloads-button', 65 | AppConstants.MOZ_DEV_EDITION ? 'developer-button' : null, 66 | 'fxa-toolbar-menu-button', 67 | ].filter((name) => name) 68 | ``` 69 | 70 | Remove the `save-to-pocket-button` item from the array and export the changes: 71 | 72 | ```sh 73 | melon export-file browser/components/customizableui/CustomizableUI.jsm 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/sass/_search.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | display: none; 3 | 4 | &--is-visible { 5 | display: block; 6 | width: 100%; 7 | } 8 | 9 | #search { 10 | width: 100%; 11 | display: block; 12 | border:none; 13 | border-left: 1px solid $color; 14 | padding:1px 0; 15 | text-align: left; 16 | line-height: $baseline; 17 | font-size: $font-size; 18 | font-family:$font-family; 19 | color:$color; 20 | background:transparent; 21 | } 22 | 23 | #search:focus { 24 | outline:none; 25 | border:none; 26 | } 27 | 28 | .search-results { 29 | &__header { 30 | font-weight: bold; 31 | padding: 1rem 0rem; 32 | } 33 | 34 | &__items { 35 | margin: 0 2vw; 36 | padding: 0; 37 | list-style: circle; 38 | } 39 | 40 | &__item { 41 | margin-bottom: 1rem; 42 | } 43 | 44 | &__teaser { 45 | 46 | } 47 | } 48 | } 49 | 50 | #on_right { 51 | display: block; 52 | text-align: right; 53 | margin-bottom: $baseline; 54 | } 55 | 56 | #search-ico { 57 | font-family: 'FabricMDL2Icons'; 58 | cursor: pointer; 59 | font-size: $baseline; 60 | line-height: 1; 61 | } -------------------------------------------------------------------------------- /docs/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg: #f9f9f9; 3 | --fg: #222; 4 | 5 | --links: #00f; 6 | --hover-links: #c00; 7 | --visited-links: #009; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --bg: #333; 13 | --fg: #f9f9f9; 14 | 15 | --links: rgb(142, 142, 255); 16 | --hover-links: rgb(204, 101, 101); 17 | --visited-links: rgb(86, 86, 151); 18 | } 19 | } 20 | 21 | $baseline: 1.5rem; 22 | 23 | $background: var(--bg); 24 | $color: var(--fg); 25 | 26 | $links: var(--links); 27 | $hover-links: var(--hover-links); 28 | $visited-links: var(--visited-links); 29 | 30 | $font-size: 1.125rem; 31 | $font-family: Segoe UI, system-ui, -apple-system, sans-serif; 32 | $line-height: 1.75; 33 | $code_font: 400 1.125rem/1.75 SFMono-Regular, Consolas, Liberation Mono, Menlo, 34 | monospace; 35 | -------------------------------------------------------------------------------- /docs/sass/fabric-icons-inline.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Your use of the content in the files referenced here is subject to the terms of the license at https://aka.ms/fabric-assets-license 3 | */ 4 | @font-face { 5 | font-family: 'FabricMDL2Icons'; 6 | src: url('data:application/octet-stream;base64,d09GRgABAAAAAAusAA4AAAAAFLgABDXDAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAEgAAABgMUZ1H2NtYXAAAAGMAAAAWgAAAYKg2Y81Y3Z0IAAAAegAAAAgAAAAKgnZCa9mcGdtAAACCAAAAPAAAAFZ/J7mjmdhc3AAAAL4AAAADAAAAAwACAAbZ2x5ZgAAAwQAAANyAAAEuLnx29VoZWFkAAAGeAAAADIAAAA2A3zu4GhoZWEAAAasAAAAFQAAACQQAQgDaG10eAAABsQAAAAYAAAAGA+HAaZsb2NhAAAG3AAAABYAAAAWBoYE+m1heHAAAAb0AAAAHQAAACAAJAHEbmFtZQAABxQAAAP3AAAJ+o6N8lFwb3N0AAALDAAAABQAAAAg/1EAgXByZXAAAAsgAAAAiQAAANN4vfIOeJxjYGHfzjiBgZWBgXUWqzEDA6M0hGa+yJDGJMTBysrFyMQIBgxAIMCAAL7BCgoMDs8Z3ulxgPkQkgGsjgXCU2BgAADc3QgGeJxjYGBgZoBgGQZGBhCoAfIYwXwWhgQgLcIgABRhec7wXPG50XO/54df7H5x4mXBO73//xkYsIlKMko8lLgqsVXCUdxL3E5shuBtqMkYgJENu/hIAgCdyyInAAB4nGPQYghlKGBoYFjFyMDYwOzAeIDBAYsIEAAAqhwHlXicXY+/TsNADMZzJLSEJ0A6IZ11KkOViJ3phksk1CUlDOelgNRKpO+AlIXFA8/ibhnzYgjMEf4utr/P+ny/c6f5yXx2nKVHKilWnDfhoNQLDurtmf35IU/vNmVhTNV5VvdlwWoJomtOF/VNsGjI0PWWTG0eH7acLWKXxY7w0nDShk7qbQB2qL/HHeJVPJLFI4QS30/xfYxL+rUsVobTiyasA/des/OoAUzFYxN49BoQf8ikP3VnE+NsOWXbwE5zgkSfygL3RJqE+0uPf/Wgkv+G+23Iv6tB9U3c9Bb0h2HBgrChl2fbUAkaYPkOhPxkxgABAAIACAAK//8AD3icXVNNaBtXEJ55b1dPsl0165UqUOJ1dze7mx+quFrJilwQwgQ3P8UpOGCKUhNfSnrqJb/Q4BcoGAr9CfSUGHpyLr2VJCT0klsv7SVXQ29uySmJCbQr7646byWnpjvsm583b+bNN/OAwX0A7Sv9GnAQAC3DNjzbsO/zP+JH7FFyFvRr/a9/0BaBPg6AMg85OgAFKMJR+CWzctCOPwY48ATegtzrJzAGnNZ8Juskz7yPdtMuG2+WPPwD//26lDIGKRmurQFTifJE4EKL8tUtrVwqaq7jB5ijtdloYQ2bjY5m1jus2agx1ymycslienf1wcbti/X6xdsbD1ZvbV+KX5jVqm/yA+cvDG3Xn230ehvPro94Hobm4bEL5+OXpl+tmty4tH1raNuFfe4Zp8olSEFE9U9CFYLsjozqCoxGh4VI4NEfEtnoEpquUSHRsAUcrLmlaHu75NYOIsWJCbeESkfJQO6CvPsZJ1lItR/JP/W7yj8BJndlKhEGhHtCR/r37jFIYdgPCdS0vOqHIOwBVSLTLmTcEBBJreehl26hTCGW+lbfy9NZ9KKeTkhHFAPf4D0OUEBRwKCArQJWtDv8izsxEfFsIZUuvV+NlQtAhgkImgwKMw4GVEY3IQRCMww8ewSKQoEqTYH3UEpvczOWzBtAQppGNSZSA21r10OZIy2Vm1sIfckIlL5Us6fCMwnvwTn4fIR6qchc26mxwC7yTGiqHti0VbE7PEQakVY2NLMfYE15DEeFPEazoywirL9TLuWOo8XD3NP5K8thuHxlfgDty0tzE+nribmly+0BzF9drteXr87j0I4TmT2WVnvm8NjDqu9XH44dnmlbAziy0LCsxsIRXe5JA/i/F5Mqh4rpn1o5eXLllK9iq9x7egqTzokpGkh/6oQzqaLsRVN8/x4gfgoi96GI1NMsMNVAtsijWLziIo5eCZJiscMFFzv0HiWwPIhf4W0wqVM+1FW3iAQaNDg50VS8hUYL9SHGOYG6iR2szDYbvuuQKlqcusXenU7WeJd3F+YSme6w038n371MHqd/6c+PnZmdtg4lYbq+wn6fOt0rH50uVseSj5x1HLBPsBt/n75Yw672Mf6YrqY7485P6dM00JbSn7/EdvLtDVz8JpVW88yx4CxFWcGb7LepQ1HZmg4KFGXdgX8Bg/8uhAAAeJxjYGRgYGAxPVwnx6UQz2/zlYGbgwEE9v892ACi78Sumg+iORjA4pwMTCAKAB/CCRAAAHicY2BkYOBgAAE4yciACpgAAsoAHQAAAAUqAKYIAAAAAAAAgACAAAAAgAAAAV0AgAAAABYASACYAN4BAAEiAVQB4gH4AlwAAHicY2BkYGDgYshiYGUAAUYwyQXCjJEgJgAOogDqAAAAeJy1VE+LGzcUf1472S1plhIo5KhDKZvFjLNuIDQ5LUlzyl42YSGXgjySZ0TGIyFpMkzpoccc+jF6CfRTlBZ67LmfoOeeeux7bzReb+yGbaEeRvPT0/v7e08GgLujz2EE/e8LfHs8gju46/Ee7MNXCY9R/jzhCeKvE74BH4NL+CZ8At8mvA9fwvcJH8Cn8EvCt+AYfk/49ujn0SThQzje+xWjjCYf4U7t/ZnwCD4bXyS8B4fjbxIeo/xtwhPEPyZ8A+6Of0v4JojxHwnvg58cJHwAx5PBzy14Ofkh4dvjt5O/Ej6Elwff/fROzO+fPBRnJvc22GUUT6x31stobJ2J06oS56YoYxDnOmj/RqvsmVx4k4uzp8/n4jQEHcO5LppK+u2DbcmF9gE9iwfZ/KQ/pcP+7IUurBYmCCmil0qvpH8t7FLEUm/kV3jbOBLnduVkbXTIdiZfxugezWZt22ar4TxDm1nsnC28dGU3W9o6htmleWicq4xWgg4y8co2YiU70QSNSWBiJBbRitxrGfVUKBNcJbupkLUSzhs8zVFF41cG4bRfmRjR3aLjIiqT65p84UEQ1g9gSRGm26U6b1WTx6kg5tF2SjZDAFOLtjR5uZFZi0FNnVeNwjats7d11Ykjc0/o1QJzuVRHDx/KltWVqQvhdYjYKWL1MgCZr309ZgaODEaJekUt8AajKtvWlZXqKnuyp0p7KsdiKFyb6JoolKYySafUlbvKKA5j3SV1agg6RH5KszCYc3b9bsM7EDCH+3ACDxGdgYEcPFgI+C4houwJIo93nlaJEoOohgxPTqHCR8A5ygoo8SzwTuNXo/YbXBVqPkO7Be7JN8V4iv8sc7YPrEl2ZFVAg/4kal7H4jo6F5xHSDkLeIDZzLHWTdvBctPuBWdjcRWoQ1VJfCMzoFC64ixfo4xYopOSdXfxV/C+QQYH7Ry/K9xLzMkwW9m/YJ54jih9BDN8Wn4y9Pe+fZbizBB37KVgPw49dChdsjeqdrYzeuCcHXbEcB/F2oJ6/4prEsxEh9+GueuZ6BkbtElmuWqPGlSHhinuFes57njHEuKD4jjuTG+bJy867SX7dtxXqjnyGVktOI+hExVXRFZDXr1F4C74LclyXcP0Wl11vFdok+N+ynz1M9/Hna7jvF+B4Ulsmacc192ctalS0s6xmobnTu3knmwqRkeofw+/NKGLxMsu730O/5XbS++KPRUo8zzHMd2pYVZ3VTBE387r8cYMUCV9LZHjDbeA/Pe1KpS0XLnlW/mh2ZNXpkpzX2xa+6p63PDNatiSsh26OfghzYpv8j/PaP/PWKfOXHofbohJLNP8UL4LZrrv7f9wt/8GD0U4iAB4nGNgZgCD/34M5QyYgAsAKTQB0nic28CgzbCJkZNJm3ETF4jcztWaG2qrysChvZ07NdhBTwbE4onwsNCQBLF4nc215YVBLD4dFRkRHhCLX05CmI8DxBLg4+FkZwGxBMEAxBLaMKEgwADIYtjOCDeaCW40M9xoFrjRrHCj2eQkoUazw43mgBvNCTd6kzAju/YGBgXX2kwJFwDEASgaAAAA') format('truetype'); 7 | } 8 | 9 | .ms-Icon { 10 | -moz-osx-font-smoothing: grayscale; 11 | -webkit-font-smoothing: antialiased; 12 | display: inline-block; 13 | font-family: 'FabricMDL2Icons'; 14 | font-style: normal; 15 | font-weight: normal; 16 | speak: none; 17 | } 18 | 19 | // Mixins 20 | @mixin ms-Icon--ChevronRightSmall { content: "\E970"; } 21 | @mixin ms-Icon--ChromeClose { content: "\E8BB"; } 22 | @mixin ms-Icon--Copy { content: "\E8C8"; } 23 | @mixin ms-Icon--GlobalNavButton { content: "\E700"; } 24 | @mixin ms-Icon--MiniLink { content: "\E732"; } 25 | @mixin ms-Icon--Page { content: "\E7C3"; } 26 | @mixin ms-Icon--ProductRelease { content: "\EE2E"; } 27 | @mixin ms-Icon--Save { content: "\E74E"; } 28 | @mixin ms-Icon--Search { content: "\E721"; } 29 | 30 | 31 | // Classes 32 | .ms-Icon--ChevronRightSmall:before { @include ms-Icon--ChevronRightSmall } 33 | .ms-Icon--ChromeClose:before { @include ms-Icon--ChromeClose } 34 | .ms-Icon--Copy:before { @include ms-Icon--Copy } 35 | .ms-Icon--GlobalNavButton:before { @include ms-Icon--GlobalNavButton } 36 | .ms-Icon--MiniLink:before { @include ms-Icon--MiniLink } 37 | .ms-Icon--Page:before { @include ms-Icon--Page } 38 | .ms-Icon--ProductRelease:before { @include ms-Icon--ProductRelease } 39 | .ms-Icon--Save:before { @include ms-Icon--Save } 40 | .ms-Icon--Search:before { @include ms-Icon--Search } -------------------------------------------------------------------------------- /docs/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | html { 4 | font-kerning: normal; 5 | text-rendering: optimizeLegibility; 6 | scroll-behavior: smooth; 7 | } 8 | 9 | body { 10 | margin: $baseline 0; 11 | font-size: $font-size; 12 | font-family: $font-family; 13 | line-height: $line-height; 14 | background: $background; 15 | color: $color; 16 | } 17 | 18 | #wrap { 19 | max-width: 800px; 20 | } 21 | 22 | @keyframes fade-in { 23 | 0% { 24 | opacity: 0; 25 | } 26 | 27 | 50% { 28 | opacity: 0.8; 29 | } 30 | 31 | 100% { 32 | opacity: 1; 33 | } 34 | } 35 | 36 | a { 37 | &:link { 38 | color: $links; 39 | text-decoration: none; 40 | } 41 | 42 | &:hover { 43 | color: $hover-links; 44 | } 45 | 46 | &:visited { 47 | color: $visited-links; 48 | } 49 | } 50 | 51 | h1 { 52 | font-size: 3rem; 53 | } 54 | 55 | h2, 56 | h3, 57 | h4 { 58 | .anchor { 59 | visibility: hidden; 60 | text-decoration: none; 61 | cursor: pointer; 62 | line-height: 1; 63 | color: $color; 64 | } 65 | 66 | &:hover { 67 | .anchor { 68 | visibility: visible; 69 | animation: fade-in 0.3s ease-in-out; 70 | font-family: 'FabricMDL2Icons'; 71 | } 72 | } 73 | } 74 | 75 | pre { 76 | margin: $baseline 0; 77 | border-radius: 4px; 78 | padding: $baseline; 79 | overflow: auto; 80 | position: relative; 81 | 82 | code { 83 | background: transparent; 84 | 85 | &::after { 86 | content: attr(data-lang); 87 | font-style: italic; 88 | line-height: 1; 89 | opacity: 0.3; 90 | position: absolute; 91 | bottom: $baseline; 92 | right: $baseline; 93 | z-index: 1; 94 | } 95 | } 96 | } 97 | 98 | code { 99 | font: $code_font; 100 | } 101 | 102 | .copy-code-button { 103 | font-family: 'FabricMDL2Icons'; 104 | display: none; 105 | background: $background; 106 | border-radius: 4px; 107 | border: none; 108 | cursor: pointer; 109 | animation: fade-in 0.3s ease-in-out; 110 | font-size: $baseline; 111 | color: $color; 112 | z-index: 10; 113 | position: absolute; 114 | top: $baseline; 115 | right: $baseline; 116 | } 117 | 118 | pre:hover .copy-code-button { 119 | display: block; 120 | } 121 | 122 | nav { 123 | position: sticky; 124 | height: 92vh; 125 | top: $baseline; 126 | left: $baseline; 127 | bottom: $baseline; 128 | padding-right: $baseline; 129 | width: 20rem; 130 | 131 | img { 132 | width: 128px; 133 | } 134 | 135 | h1 { 136 | margin: 0; 137 | line-height: 1; 138 | } 139 | } 140 | 141 | #toc { 142 | margin-left: calc(#{$baseline} + #{$font-size}); 143 | padding: 0; 144 | margin: 0 0 0 $baseline; 145 | font-size: 80%; 146 | 147 | li { 148 | color: $color; 149 | margin-left: $font-size; 150 | 151 | &::before { 152 | display: inline-block; 153 | content: ''; 154 | } 155 | 156 | ul { 157 | padding: 0; 158 | } 159 | } 160 | } 161 | 162 | main { 163 | display: flex; 164 | flex-flow: row nowrap; 165 | animation: fade-in 0.4s ease-in-out; 166 | } 167 | 168 | #release { 169 | text-align: left; 170 | margin: $baseline 0; 171 | 172 | &::before { 173 | display: inline-block; 174 | content: '\EE2E'; 175 | font-family: 'FabricMDL2Icons'; 176 | margin-right: calc(#{$baseline} / 8); 177 | } 178 | } 179 | 180 | @keyframes slideIn { 181 | 0% { 182 | max-height: 0; 183 | opacity: 0; 184 | } 185 | 100% { 186 | max-height: 999px; 187 | opacity: 1; 188 | } 189 | } 190 | @keyframes slideOut { 191 | 0% { 192 | height: auto; 193 | opacity: 1; 194 | } 195 | 100% { 196 | height: 0; 197 | opacity: 0; 198 | } 199 | } 200 | 201 | nav label { 202 | display: block; 203 | } 204 | 205 | #trees { 206 | overflow-y: auto; 207 | height: 80%; 208 | } 209 | 210 | .subtree { 211 | overflow: hidden; 212 | margin: calc(#{$baseline} / 8) 0; 213 | transition: overflow 0.2s ease-in-out; 214 | padding: 0; 215 | } 216 | 217 | .tree-toggle-label { 218 | user-select: none; 219 | cursor: pointer; 220 | } 221 | 222 | .tree-toggle-label::before { 223 | display: inline-block; 224 | content: '\E970'; 225 | font-family: 'FabricMDL2Icons'; 226 | font-size: 0.75rem; 227 | transform: rotate(0deg); 228 | transform-origin: 50% 50% 0px; 229 | transition: transform 0.1s linear 0s; 230 | margin-right: 2px; 231 | } 232 | 233 | .tree-toggle { 234 | position: absolute; 235 | opacity: 0; 236 | z-index: -1; 237 | } 238 | 239 | .tree-toggle:checked + .tree-toggle-label::before { 240 | content: '\E970'; 241 | font-family: 'FabricMDL2Icons'; 242 | font-size: 0.75rem; 243 | transform: rotate(90deg); 244 | transform-origin: 50% 50% 0px; 245 | transition: transform 0.1s linear 0s; 246 | margin-right: 2px; 247 | } 248 | 249 | .tree-toggle:checked + .tree-toggle-label { 250 | font-weight: bold; 251 | } 252 | 253 | .tree-toggle + .tree-toggle-label + .subtree { 254 | animation-name: slideOut; 255 | animation-duration: 0.25s; 256 | animation-fill-mode: both; 257 | } 258 | 259 | .tree-toggle:checked + .tree-toggle-label + .subtree { 260 | animation-name: slideIn; 261 | animation-duration: 0.25s; 262 | animation-fill-mode: both; 263 | } 264 | 265 | .subtree li { 266 | list-style-type: none; 267 | margin-left: $baseline; 268 | 269 | a { 270 | color: $color; 271 | } 272 | 273 | &::before { 274 | content: '\E7C3'; 275 | font-family: 'FabricMDL2Icons'; 276 | font-size: 0.75rem; 277 | } 278 | } 279 | 280 | .active a { 281 | font-weight: bold; 282 | } 283 | 284 | article { 285 | width: calc(100% - (#{$baseline} * 4 + 20rem)); 286 | margin-left: calc(#{$baseline} * 2); 287 | 288 | img { 289 | max-width: 100%; 290 | } 291 | } 292 | 293 | #mobile { 294 | display: none; 295 | } 296 | 297 | @media screen and (max-width: 1023px) { 298 | main { 299 | flex-flow: column nowrap; 300 | width: 100%; 301 | } 302 | 303 | nav { 304 | position: inherit; 305 | height: auto; 306 | margin: $baseline $baseline 0 $baseline; 307 | } 308 | 309 | article { 310 | width: calc(100% - (#{$baseline} * 2)); 311 | margin: 0 $baseline; 312 | z-index: 1; 313 | } 314 | 315 | #mobile { 316 | font-family: 'FabricMDL2Icons'; 317 | cursor: pointer; 318 | font-size: $baseline; 319 | margin: 0 $baseline 0 0; 320 | display: block; 321 | color: $color; 322 | } 323 | 324 | #trees { 325 | display: none; 326 | position: absolute; 327 | background: $background; 328 | height: auto; 329 | width: 100vw; 330 | z-index: 10; 331 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.1); 332 | } 333 | 334 | #on_right { 335 | margin-top: $baseline; 336 | } 337 | } 338 | 339 | @import 'fabric-icons-inline'; 340 | @import 'search'; 341 | -------------------------------------------------------------------------------- /docs/static/images/userchrome/browser-toolbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/docs/static/images/userchrome/browser-toolbox.png -------------------------------------------------------------------------------- /docs/static/images/userchrome/css-basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/docs/static/images/userchrome/css-basic.png -------------------------------------------------------------------------------- /docs/static/images/userchrome/css-final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/docs/static/images/userchrome/css-final.png -------------------------------------------------------------------------------- /docs/static/images/userchrome/css-square-tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/docs/static/images/userchrome/css-square-tabs.png -------------------------------------------------------------------------------- /docs/static/js.js: -------------------------------------------------------------------------------- 1 | // search script, borrowed from book theme 2 | 3 | function debounce(func, wait) { 4 | let timeout; 5 | 6 | return function () { 7 | const context = this; 8 | const args = arguments; 9 | clearTimeout(timeout); 10 | 11 | timeout = setTimeout(() => { 12 | timeout = null; 13 | func.apply(context, args); 14 | }, wait); 15 | }; 16 | } 17 | 18 | // Taken from mdbook 19 | // The strategy is as follows: 20 | // First, assign a value to each word in the document: 21 | // Words that correspond to search terms (stemmer aware): 40 22 | // Normal words: 2 23 | // First word in a sentence: 8 24 | // Then use a sliding window with a constant number of words and count the 25 | // sum of the values of the words within the window. Then use the window that got the 26 | // maximum sum. If there are multiple maximas, then get the last one. 27 | // Enclose the terms in . 28 | function makeTeaser(body, terms) { 29 | const TERM_WEIGHT = 40; 30 | const NORMAL_WORD_WEIGHT = 2; 31 | const FIRST_WORD_WEIGHT = 8; 32 | const TEASER_MAX_WORDS = 30; 33 | 34 | const stemmedTerms = terms.map((w) => elasticlunr.stemmer(w.toLowerCase())); 35 | let termFound = false; 36 | let index = 0; 37 | const weighted = []; // contains elements of ["word", weight, index_in_document] 38 | 39 | // split in sentences, then words 40 | const sentences = body.toLowerCase().split(". "); 41 | 42 | for (var i in sentences) { 43 | const words = sentences[i].split(" "); 44 | let value = FIRST_WORD_WEIGHT; 45 | 46 | for (const j in words) { 47 | var word = words[j]; 48 | 49 | if (word.length > 0) { 50 | for (const k in stemmedTerms) { 51 | if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) { 52 | value = TERM_WEIGHT; 53 | termFound = true; 54 | } 55 | } 56 | weighted.push([word, value, index]); 57 | value = NORMAL_WORD_WEIGHT; 58 | } 59 | 60 | index += word.length; 61 | index += 1; // ' ' or '.' if last word in sentence 62 | } 63 | 64 | index += 1; // because we split at a two-char boundary '. ' 65 | } 66 | 67 | if (weighted.length === 0) { 68 | return body; 69 | } 70 | 71 | const windowWeights = []; 72 | const windowSize = Math.min(weighted.length, TEASER_MAX_WORDS); 73 | // We add a window with all the weights first 74 | let curSum = 0; 75 | for (var i = 0; i < windowSize; i++) { 76 | curSum += weighted[i][1]; 77 | } 78 | windowWeights.push(curSum); 79 | 80 | for (var i = 0; i < weighted.length - windowSize; i++) { 81 | curSum -= weighted[i][1]; 82 | curSum += weighted[i + windowSize][1]; 83 | windowWeights.push(curSum); 84 | } 85 | 86 | // If we didn't find the term, just pick the first window 87 | let maxSumIndex = 0; 88 | if (termFound) { 89 | let maxFound = 0; 90 | // backwards 91 | for (var i = windowWeights.length - 1; i >= 0; i--) { 92 | if (windowWeights[i] > maxFound) { 93 | maxFound = windowWeights[i]; 94 | maxSumIndex = i; 95 | } 96 | } 97 | } 98 | 99 | const teaser = []; 100 | let startIndex = weighted[maxSumIndex][2]; 101 | for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) { 102 | var word = weighted[i]; 103 | if (startIndex < word[2]) { 104 | // missing text from index to start of `word` 105 | teaser.push(body.substring(startIndex, word[2])); 106 | startIndex = word[2]; 107 | } 108 | 109 | // add around search terms 110 | if (word[1] === TERM_WEIGHT) { 111 | teaser.push(""); 112 | } 113 | startIndex = word[2] + word[0].length; 114 | teaser.push(body.substring(word[2], startIndex)); 115 | 116 | if (word[1] === TERM_WEIGHT) { 117 | teaser.push(""); 118 | } 119 | } 120 | teaser.push("…"); 121 | return teaser.join(""); 122 | } 123 | 124 | function formatSearchResultItem(item, terms) { 125 | const li = document.createElement("li"); 126 | li.classList.add("search-results__item"); 127 | li.innerHTML = `${item.doc.title}`; 128 | li.innerHTML += `
${makeTeaser(item.doc.body, terms)}
`; 129 | return li; 130 | } 131 | 132 | // Go from the book view to the search view 133 | function toggleSearchMode() { 134 | const $wrapContent = document.querySelector("#wrap"); 135 | const $searchIcon = document.querySelector("#search-ico"); 136 | const $searchContainer = document.querySelector(".search-container"); 137 | if ($searchContainer.classList.contains("search-container--is-visible")) { 138 | $searchContainer.classList.remove("search-container--is-visible"); 139 | $wrapContent.style.display = ""; 140 | $searchIcon.className = "ms-Icon--Search"; 141 | } else { 142 | $searchContainer.classList.add("search-container--is-visible"); 143 | $wrapContent.style.display = "none"; 144 | $searchIcon.className = "ms-Icon--ChromeClose"; 145 | document.getElementById("search").focus(); 146 | } 147 | } 148 | 149 | function initSearch() { 150 | const $searchInput = document.getElementById("search"); 151 | if (!$searchInput) { 152 | return; 153 | } 154 | const $searchIcon = document.querySelector("#search-ico"); 155 | $searchIcon.addEventListener("click", toggleSearchMode); 156 | 157 | const $searchResults = document.querySelector(".search-results"); 158 | const $searchResultsHeader = document.querySelector(".search-results__header"); 159 | const $searchResultsItems = document.querySelector(".search-results__items"); 160 | const MAX_ITEMS = 100; 161 | 162 | const options = { 163 | bool: "AND", 164 | fields: { 165 | title: {boost: 2}, 166 | body: {boost: 1}, 167 | } 168 | }; 169 | let currentTerm = ""; 170 | const index = elasticlunr.Index.load(window.searchIndex); 171 | 172 | $searchInput.addEventListener("keyup", debounce(() => { 173 | const term = $searchInput.value.trim(); 174 | if (term === currentTerm || !index) { 175 | return; 176 | } 177 | $searchResults.style.display = term === "" ? "none" : "block"; 178 | $searchResultsItems.innerHTML = ""; 179 | if (term === "") { 180 | return; 181 | } 182 | 183 | const results = index.search(term, options).filter((r) => r.doc.body !== ""); 184 | if (results.length === 0) { 185 | $searchResultsHeader.innerText = `Nothing like «${term}»`; 186 | return; 187 | } 188 | 189 | currentTerm = term; 190 | $searchResultsHeader.innerText = `${results.length} found for «${term}»:`; 191 | for (let i = 0; i < Math.min(results.length, MAX_ITEMS); i++) { 192 | if (!results[i].doc.body) { 193 | continue; 194 | } 195 | // var item = document.createElement("li"); 196 | // item.innerHTML = formatSearchResultItem(results[i], term.split(" ")); 197 | console.log(results[i]); 198 | $searchResultsItems.appendChild(formatSearchResultItem(results[i], term.split(" "))); 199 | } 200 | }, 150)); 201 | } 202 | 203 | if (document.readyState === "complete" || 204 | (document.readyState !== "loading" && !document.documentElement.doScroll) 205 | ) { 206 | initSearch(); 207 | } else { 208 | document.addEventListener("DOMContentLoaded", initSearch); 209 | } 210 | 211 | // mobile 212 | 213 | function burger() { 214 | const x = document.querySelector("#trees"); 215 | const y = document.querySelector("#mobile"); 216 | 217 | if (x.style.display === "block") { 218 | x.style.display = "none"; 219 | y.className = "ms-Icon--GlobalNavButton"; 220 | } else { 221 | x.style.display = "block"; 222 | y.className = "ms-Icon--ChromeClose"; 223 | } 224 | } 225 | 226 | // https://aaronluna.dev/blog/add-copy-button-to-code-blocks-hugo-chroma/ 227 | 228 | function createCopyButton(highlightDiv) { 229 | const button = document.createElement("button"); 230 | button.className = "copy-code-button "; 231 | button.type = "button"; 232 | button.innerHTML = ""; 233 | button.addEventListener("click", () => 234 | copyCodeToClipboard(button, highlightDiv) 235 | ); 236 | addCopyButtonToDom(button, highlightDiv); 237 | } 238 | 239 | async function copyCodeToClipboard(button, highlightDiv) { 240 | const codeToCopy = highlightDiv.querySelector(":last-child > code") 241 | .innerText; 242 | try { 243 | result = await navigator.permissions.query({ name: "clipboard-write" }); 244 | if (result.state == "granted" || result.state == "prompt") { 245 | await navigator.clipboard.writeText(codeToCopy); 246 | } else { 247 | copyCodeBlockExecCommand(codeToCopy, highlightDiv); 248 | } 249 | } catch (_) { 250 | copyCodeBlockExecCommand(codeToCopy, highlightDiv); 251 | } finally { 252 | codeWasCopied(button); 253 | } 254 | } 255 | 256 | function copyCodeBlockExecCommand(codeToCopy, highlightDiv) { 257 | const textArea = document.createElement("textArea"); 258 | textArea.contentEditable = "true"; 259 | textArea.readOnly = "false"; 260 | textArea.className = "copyable-text-area"; 261 | textArea.value = codeToCopy; 262 | highlightDiv.insertBefore(textArea, highlightDiv.firstChild); 263 | const range = document.createRange(); 264 | range.selectNodeContents(textArea); 265 | const sel = window.getSelection(); 266 | sel.removeAllRanges(); 267 | sel.addRange(range); 268 | textArea.setSelectionRange(0, 999999); 269 | document.execCommand("copy"); 270 | highlightDiv.removeChild(textArea); 271 | } 272 | 273 | function codeWasCopied(button) { 274 | button.blur(); 275 | button.innerHTML = ""; 276 | setTimeout(() => { 277 | button.innerHTML = ""; 278 | }, 2000); 279 | } 280 | 281 | function addCopyButtonToDom(button, highlightDiv) { 282 | highlightDiv.insertBefore(button, highlightDiv.firstChild); 283 | const wrapper = document.createElement("div"); 284 | wrapper.className = "highlight-wrapper"; 285 | highlightDiv.parentNode.insertBefore(wrapper, highlightDiv); 286 | wrapper.appendChild(highlightDiv); 287 | } 288 | 289 | document 290 | .querySelectorAll("pre") 291 | .forEach((highlightDiv) => createCopyButton(highlightDiv)); 292 | -------------------------------------------------------------------------------- /docs/static/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/templates/anchor-link.html: -------------------------------------------------------------------------------- 1 |   -------------------------------------------------------------------------------- /docs/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if config.extra.favicon %} 7 | 8 | {% endif %} 9 | 10 | {% block title %}{{ config.title }}{% endblock title %} 11 | 12 | 13 | 14 | {% if config.extra.release %} 15 | 26 | {% endif %} 27 |
28 | 29 | {% block nav %} 30 | 31 | 102 | {% endblock nav %} 103 | 104 |
105 | 106 | {% if config.build_search_index %} 107 |
108 | 109 |
110 |
111 | 112 |
113 |
114 |
    115 |
    116 |
    117 | {% endif %} 118 | 119 |
    120 | {% block content %} 121 | {{ section.content | safe }} 122 | {% endblock content %} 123 |
    124 | 125 |
    126 |
    127 | 128 | {% if config.build_search_index %} 129 | 130 | 131 | 132 | {% endif %} 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "index.html" %} {% block title %} {{ page.title }} | {{ config.title 2 | }} {% endblock title %} {% block content %} {{ page.content | safe }} {% 3 | endblock content %} 4 | -------------------------------------------------------------------------------- /docs/theme.toml: -------------------------------------------------------------------------------- 1 | name = "Melon" 2 | description = "🍉 Build Firefox-based browsers with ease" 3 | license = "MPL 2.0" 4 | homepage = "https://github.com/dothq/melon" 5 | min_version = "1.14.0" 6 | 7 | [author] 8 | name = "TrickyPR" 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "melon-build", 3 | "version": "1.0.0-a.11", 4 | "description": "🍉 Build Firefox-based browsers with ease", 5 | "main": "index.js", 6 | "reveal": true, 7 | "bin": { 8 | "melon": "./dist/index.js" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\"", 12 | "build": "rm -rf dist/ && tsc && echo \"#!/usr/bin/env node\"|cat - ./dist/index.js > /tmp/out && mv /tmp/out ./dist/index.js && chmod +x ./dist/index.js", 13 | "format": "prettier . -w", 14 | "lint": "eslint .", 15 | "docs:install": "", 16 | "docs:build": "zola --root docs/ build", 17 | "docs:dev": "zola --root docs/ serve" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/dothq/melon.git" 22 | }, 23 | "keywords": [ 24 | "firefox", 25 | "firefox-fork", 26 | "build-tool" 27 | ], 28 | "authors": [ 29 | "TrickyPR", 30 | "EnderDev" 31 | ], 32 | "license": "MPL-2.0", 33 | "bugs": { 34 | "url": "https://github.com/dothq/melon/issues" 35 | }, 36 | "homepage": "https://github.com/dothq/melon#readme", 37 | "dependencies": { 38 | "axios": "^0.21.1", 39 | "chalk": "^4.1.0", 40 | "cli-progress": "^3.9.1", 41 | "commander": "^6.2.1", 42 | "execa": "^5.1.1", 43 | "fs-extra": "^10.0.0", 44 | "glob": "^7.1.7", 45 | "linus": "^0.0.6", 46 | "listr": "^0.14.3", 47 | "prompts": "^2.4.1", 48 | "rimraf": "^3.0.2", 49 | "sharp": "^0.29.1" 50 | }, 51 | "devDependencies": { 52 | "@types/cli-progress": "^3.9.2", 53 | "@types/fs-extra": "^9.0.13", 54 | "@types/listr": "^0.14.4", 55 | "@types/node": "^14.14.16", 56 | "@types/prompts": "^2.0.14", 57 | "@types/rimraf": "^3.0.0", 58 | "@types/sharp": "^0.29.2", 59 | "@typescript-eslint/eslint-plugin": "^4.32.0", 60 | "@typescript-eslint/parser": "^4.32.0", 61 | "eslint": "^7.2.0", 62 | "eslint-config-airbnb": "18.2.1", 63 | "eslint-config-prettier": "^8.3.0", 64 | "eslint-plugin-import": "^2.22.1", 65 | "eslint-plugin-jsx-a11y": "^6.4.1", 66 | "eslint-plugin-react": "^7.21.5", 67 | "eslint-plugin-react-hooks": "^1.7.0", 68 | "prettier": "^2.2.1", 69 | "typescript": "^4.1.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/cmds.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrap, 3 | build, 4 | discard, 5 | download, 6 | downloadArtifacts, 7 | execute, 8 | exportFile, 9 | fixLineEndings, 10 | importPatches, 11 | init, 12 | licenseCheck, 13 | melonPackage, 14 | reset, 15 | run, 16 | setBranch, 17 | setupProject, 18 | status, 19 | test, 20 | } from './commands' 21 | import { Cmd } from './types' 22 | 23 | export const commands: Cmd[] = [ 24 | { 25 | cmd: 'bootstrap', 26 | description: 'Bootstrap the melon app.', 27 | controller: bootstrap, 28 | }, 29 | { 30 | cmd: 'build', 31 | aliases: ['b'], 32 | description: 33 | 'Build the melon app. Specify the OS param for cross-platform builds.', 34 | options: [ 35 | { 36 | arg: '--a, --arch ', 37 | description: 'Specify architecture for build', 38 | }, 39 | { 40 | arg: '--u, --ui', 41 | description: 42 | 'Only builds the ui. Faster but not as powerful as a regular build.', 43 | }, 44 | ], 45 | controller: build, 46 | }, 47 | { 48 | cmd: 'discard ', 49 | description: 'Discard a files changes.', 50 | options: [ 51 | { 52 | arg: '--keep, --keep-patch', 53 | description: 'Keep the patch file instead of removing it', 54 | }, 55 | ], 56 | controller: discard, 57 | }, 58 | { 59 | cmd: 'download [ffVersion]', 60 | description: 'Download Firefox.', 61 | controller: download, 62 | }, 63 | { 64 | cmd: 'download-artifacts', 65 | description: 'Download Windows artifacts from GitHub.', 66 | flags: { 67 | platforms: ['win32'], 68 | }, 69 | controller: downloadArtifacts, 70 | }, 71 | { 72 | cmd: 'execute', 73 | description: 'Execute a command inside the engine directory.', 74 | controller: execute, 75 | }, 76 | { 77 | cmd: 'export-file ', 78 | aliases: ['export'], 79 | description: 'Export a changed file as a patch.', 80 | controller: exportFile, 81 | }, 82 | { 83 | cmd: 'lfify', 84 | aliases: ['fix-le'], 85 | description: 'Convert CRLF line endings to Unix LF line endings.', 86 | controller: fixLineEndings, 87 | }, 88 | { 89 | cmd: 'import [type]', 90 | aliases: ['import-patches', 'i'], 91 | description: 'Import patches into the browser.', 92 | options: [ 93 | { 94 | arg: '-m, --minimal', 95 | description: 'Import patches in minimal mode', 96 | }, 97 | { 98 | arg: '--noignore', 99 | description: "Bypass .gitignore. You shouldn't really use this.", 100 | }, 101 | ], 102 | controller: importPatches, 103 | }, 104 | { 105 | cmd: 'ff-init ', 106 | aliases: ['ff-initialise', 'ff-initialize'], 107 | description: 'Initialise the Firefox directory.', 108 | controller: init, 109 | }, 110 | { 111 | cmd: 'license-check', 112 | aliases: ['lc'], 113 | description: 'Check the src directory for the absence of MPL-2.0 header.', 114 | controller: licenseCheck, 115 | }, 116 | { 117 | cmd: 'package', 118 | aliases: ['pack'], 119 | description: 'Package the browser for distribution.', 120 | controller: melonPackage, 121 | }, 122 | { 123 | cmd: 'reset', 124 | description: 'Reset the source directory to stock Firefox.', 125 | controller: reset, 126 | }, 127 | { 128 | cmd: 'run [chrome]', 129 | aliases: ['r', 'open'], 130 | description: 'Run the browser.', 131 | controller: run, 132 | }, 133 | { 134 | cmd: 'set-branch ', 135 | description: 'Change the default branch.', 136 | controller: setBranch, 137 | }, 138 | { 139 | cmd: 'setup-project', 140 | description: 'Sets up a melon project for the first time', 141 | controller: setupProject, 142 | }, 143 | { 144 | cmd: 'status', 145 | description: 'Status and files changed for src directory.', 146 | controller: status, 147 | }, 148 | { 149 | cmd: 'test', 150 | description: 'Run the test suite for the melon app.', 151 | controller: test, 152 | }, 153 | ] 154 | -------------------------------------------------------------------------------- /src/commands/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import distro from 'linus' 4 | import { bin_name, log } from '..' 5 | import { ENGINE_DIR } from '../constants' 6 | import { dispatch } from '../utils' 7 | import { pacmanInstall } from './bootstrap/arch' 8 | import { aptInstall } from './bootstrap/debian' 9 | 10 | export const bootstrap = async () => { 11 | if (process.platform == 'win32') 12 | log.error( 13 | `You do not need to bootstrap on Windows. As long as you ran |${bin_name} download-artifacts| everything should work fine.` 14 | ) 15 | 16 | log.info(`Bootstrapping Dot Browser for Desktop...`) 17 | 18 | const args = ['--application-choice', 'browser'] 19 | 20 | if (process.platform === 'linux') { 21 | linuxBootstrap() 22 | } else { 23 | console.info( 24 | `Custom bootstrapping doesn't work on ${process.platform}. Consider contributing to improve support` 25 | ) 26 | 27 | console.info(`Passing through to |mach bootstrap|`) 28 | 29 | await dispatch(`./mach`, ['bootstrap', ...args], ENGINE_DIR) 30 | } 31 | } 32 | 33 | function getDistro(): Promise { 34 | return new Promise((resolve, reject) => { 35 | distro.name((err: Error, name: string) => { 36 | if (name) resolve(name) 37 | else { 38 | reject(err || 'Failed to get linux distro') 39 | } 40 | }) 41 | }) 42 | } 43 | 44 | async function linuxBootstrap() { 45 | const distro = await getDistro() 46 | 47 | switch (distro) { 48 | // Both arch and manjaro use the same package repo and the same package manager 49 | case 'ManjaroLinux': 50 | case 'ArchLinux': 51 | console.log( 52 | await pacmanInstall( 53 | // Shared packages 54 | 'base-devel', 55 | 'nodejs', 56 | 'unzip', 57 | 'zip', 58 | 59 | // Needed for desktop apps 60 | 'alsa-lib', 61 | 'dbus-glib', 62 | 'gtk3', 63 | 'libevent', 64 | 'libvpx', 65 | 'libxt', 66 | 'mime-types', 67 | 'nasm', 68 | 'startup-notification', 69 | 'gst-plugins-base-libs', 70 | 'libpulse', 71 | 'xorg-server-xvfb', 72 | 'gst-libav', 73 | 'gst-plugins-good', 74 | 'wasi-libc' 75 | ) 76 | ) 77 | break 78 | 79 | case 'Debian': 80 | case 'Ubuntu': 81 | case 'Pop': 82 | console.log( 83 | await aptInstall( 84 | 'python3-distutils', 85 | 'libssl-dev', 86 | 'build-essential', 87 | 'libpulse-dev', 88 | 'clang', 89 | 'nasm', 90 | 'libpango1.0-dev', 91 | 'libx11-dev', 92 | 'libx11-xcb-dev', 93 | 'libgtk-3-dev', 94 | 'm4', 95 | 'libgtk2.0-dev', 96 | 'libdbus-glib-1-dev', 97 | 'libxt-dev' 98 | ) 99 | ) 100 | break 101 | 102 | default: 103 | log.error(`Unimplemented distro '${distro}'`) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/commands/bootstrap/arch.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | 3 | export async function pacmanInstall(...packages: string[]): Promise { 4 | return (await execa('sudo', ['pacman', '--noconfirm', '-S', ...packages])) 5 | .stdout 6 | } 7 | -------------------------------------------------------------------------------- /src/commands/bootstrap/debian.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | 3 | export async function aptInstall(...packages: string[]): Promise { 4 | return (await execa('sudo', ['apt', 'install', '-y', ...packages])).stdout 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/bootstrap/macos.ts: -------------------------------------------------------------------------------- 1 | const brewDependencies = [ 2 | 'gnu-tar', // MacOS tar doesn't support the --transform option 3 | ] 4 | 5 | export async function bootstrapMacos() {} 6 | -------------------------------------------------------------------------------- /src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, readFileSync, writeFileSync } from 'fs' 3 | import { join, resolve } from 'path' 4 | import { bin_name, config, log } from '..' 5 | import { BUILD_TARGETS, CONFIGS_DIR, ENGINE_DIR } from '../constants' 6 | import { patchCheck } from '../middleware/patch-check' 7 | import { dispatch, stringTemplate } from '../utils' 8 | 9 | const platform: Record = { 10 | win32: 'windows', 11 | darwin: 'macos', 12 | linux: 'linux', 13 | } 14 | 15 | const applyConfig = async (os: string, arch: string) => { 16 | log.info('Applying mozconfig...') 17 | 18 | let changeset 19 | 20 | try { 21 | // Retrieve changeset 22 | const { stdout } = await execa('git', ['rev-parse', 'HEAD']) 23 | changeset = stdout.trim() 24 | } catch (e) { 25 | log.warning( 26 | 'Melon expects that you are building your browser with git as your version control' 27 | ) 28 | log.warning( 29 | 'If you are using some other version control system, please migrate to git' 30 | ) 31 | log.warning('Otherwise, you can setup git in this folder by running:') 32 | log.warning(' |git init|') 33 | 34 | throw e 35 | } 36 | 37 | const templateOptions = { 38 | name: config.name, 39 | vendor: config.name, 40 | appId: config.appId, 41 | brandingDir: existsSync(join(ENGINE_DIR, 'branding', 'melon')) 42 | ? 'branding/melon' 43 | : 'branding/unofficial', 44 | binName: config.binaryName, 45 | changeset, 46 | } 47 | 48 | const commonConfig = stringTemplate( 49 | readFileSync(resolve(CONFIGS_DIR, 'common', 'mozconfig'), 'utf-8'), 50 | templateOptions 51 | ) 52 | 53 | const osConfig = stringTemplate( 54 | readFileSync( 55 | resolve( 56 | CONFIGS_DIR, 57 | os, 58 | arch === 'i686' ? 'mozconfig-i686' : 'mozconfig' 59 | ), 60 | 'utf-8' 61 | ), 62 | templateOptions 63 | ) 64 | 65 | // Allow a custom config to be placed in /mozconfig. This will not be committed 66 | // to origin 67 | let customConfig = existsSync(join(process.cwd(), 'mozconfig')) 68 | ? readFileSync(join(process.cwd(), 'mozconfig')).toString() 69 | : '' 70 | 71 | customConfig = stringTemplate(customConfig, templateOptions) 72 | 73 | const internalConfig = `# Internally defined by melon` 74 | 75 | const mergedConfig = 76 | `# This file is automatically generated. You should only modify this if you know what you are doing!\n\n` + 77 | commonConfig + 78 | '\n\n' + 79 | osConfig + 80 | '\n\n' + 81 | customConfig + 82 | '\n\n' + 83 | internalConfig 84 | 85 | writeFileSync(resolve(ENGINE_DIR, 'mozconfig'), mergedConfig) 86 | 87 | log.info(`Config for this \`${os}\` build:`) 88 | 89 | mergedConfig.split('\n').map((ln) => { 90 | if (ln.startsWith('mk') || ln.startsWith('ac') || ln.startsWith('export')) 91 | log.info( 92 | `\t${ln 93 | .replace(/mk_add_options /, '') 94 | .replace(/ac_add_options /, '') 95 | .replace(/export /, '')}` 96 | ) 97 | }) 98 | } 99 | 100 | const genericBuild = async (os: string, fast = false) => { 101 | log.info(`Building for "${os}"...`) 102 | 103 | log.warning( 104 | `If you get any dependency errors, try running |${bin_name} bootstrap|.` 105 | ) 106 | 107 | const buildOptions = ['build'] 108 | 109 | if (fast) { 110 | buildOptions.push('faster') 111 | } 112 | 113 | log.info(buildOptions.join(' ')) 114 | 115 | await dispatch(`./mach`, buildOptions, ENGINE_DIR) 116 | } 117 | 118 | const parseDate = (d: number) => { 119 | d /= 1000 120 | const h = Math.floor(d / 3600) 121 | const m = Math.floor((d % 3600) / 60) 122 | const s = Math.floor((d % 3600) % 60) 123 | 124 | const hDisplay = h > 0 ? h + (h == 1 ? ' hour, ' : ' hours, ') : '' 125 | const mDisplay = m > 0 ? m + (m == 1 ? ' minute, ' : ' minutes, ') : '' 126 | const sDisplay = s > 0 ? s + (s == 1 ? ' second' : ' seconds') : '' 127 | return hDisplay + mDisplay + sDisplay 128 | } 129 | 130 | const success = (date: number) => { 131 | // mach handles the success messages 132 | console.log() 133 | log.info(`Total build time: ${parseDate(Date.now() - date)}.`) 134 | } 135 | 136 | interface Options { 137 | arch: string 138 | ui: boolean 139 | } 140 | 141 | export const build = async (options: Options): Promise => { 142 | const d = Date.now() 143 | 144 | // Host build 145 | 146 | const prettyHost = platform[process.platform] 147 | 148 | if (BUILD_TARGETS.includes(prettyHost)) { 149 | await patchCheck() 150 | 151 | applyConfig(prettyHost, options.arch) 152 | 153 | log.info('Starting build...') 154 | 155 | await genericBuild(prettyHost, options.ui).then((_) => success(d)) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/commands/discard.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, statSync } from 'fs' 3 | import { resolve } from 'path' 4 | import rimraf from 'rimraf' 5 | import { log } from '..' 6 | import { ENGINE_DIR, PATCHES_DIR } from '../constants' 7 | 8 | interface Options { 9 | keep?: boolean 10 | } 11 | 12 | export const discard = async ( 13 | file: string, 14 | options: Options 15 | ): Promise => { 16 | const realFile = resolve(ENGINE_DIR, file) 17 | 18 | log.info(`Discarding ${file}...`) 19 | 20 | if (!existsSync(realFile)) throw new Error(`File ${file} does not exist`) 21 | if (!statSync(realFile).isFile()) throw new Error('Target must be a file.') 22 | 23 | try { 24 | await execa('git', ['restore', file], { cwd: ENGINE_DIR }) 25 | } catch (e) { 26 | log.warning(`The file ${file} was not changed`) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/download-artifacts.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { homedir } from 'os' 3 | import { posix, resolve, sep } from 'path' 4 | import { log } from '..' 5 | import { downloadFileToLocation } from '../utils/download' 6 | 7 | export const downloadArtifacts = async (): Promise => { 8 | if (process.platform !== 'win32') 9 | return log.error( 10 | 'This is not a Windows machine, will not download artifacts.' 11 | ) 12 | if (process.env.MOZILLABUILD) 13 | return log.error( 14 | 'Run this command in Git Bash, it does not work in Mozilla Build.' 15 | ) 16 | 17 | const filename = 'mozbuild.tar.bz2' 18 | const url = `https://github.com/dothq/windows-artifacts/releases/latest/download/mozbuild.tar.bz2` 19 | let home = homedir().split(sep).join(posix.sep) 20 | 21 | if (process.platform == 'win32') { 22 | home = `/${home.replace(/\:/, '').replace(/\\/g, '/').toLowerCase()}` 23 | } 24 | 25 | log.info(`Downloading Windows artifacts...`) 26 | 27 | await downloadFileToLocation(url, resolve(process.cwd(), filename)) 28 | 29 | log.info('Unpacking mozbuild...') 30 | 31 | await execa('tar', ['-xvf', filename, '-C', home]) 32 | 33 | log.info('Done extracting mozbuild artifacts.') 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/download.ts: -------------------------------------------------------------------------------- 1 | import { 2 | existsSync, 3 | mkdirSync, 4 | readFileSync, 5 | rmdirSync, 6 | unlinkSync, 7 | writeFileSync, 8 | } from 'fs' 9 | import { homedir } from 'os' 10 | import { basename, join, posix, resolve, sep } from 'path' 11 | 12 | import execa from 'execa' 13 | import Listr from 'listr' 14 | 15 | import { bin_name, config, log } from '..' 16 | import { ENGINE_DIR, MELON_TMP_DIR } from '../constants' 17 | import { 18 | ensureDir, 19 | getConfig, 20 | walkDirectoryTree, 21 | writeMetadata, 22 | } from '../utils' 23 | import { downloadFileToLocation } from '../utils/download' 24 | import { downloadArtifacts } from './download-artifacts' 25 | import { discard, init } from '.' 26 | 27 | const gFFVersion = getConfig().version.version 28 | 29 | export const download = async (): Promise => { 30 | const version = gFFVersion 31 | 32 | // If gFFVersion isn't specified, provide legible error 33 | if (!version) { 34 | log.error( 35 | 'You have not specified a version of firefox in your config file. This is required to build a firefox fork' 36 | ) 37 | process.exit(1) 38 | } 39 | 40 | const addons = Object.keys(config.addons).map((addon) => ({ 41 | name: addon, 42 | ...config.addons[addon], 43 | })) 44 | 45 | await new Listr([ 46 | { 47 | title: 'Downloading firefox source', 48 | skip: () => { 49 | if ( 50 | existsSync(ENGINE_DIR) && 51 | existsSync(resolve(ENGINE_DIR, 'toolkit', 'moz.build')) 52 | ) { 53 | return 'Firefox has already been downloaded, unpacked and inited' 54 | } 55 | }, 56 | task: async (ctx, task) => { 57 | ctx.firefoxSourceTar = await downloadFirefoxSource(version, task) 58 | }, 59 | }, 60 | { 61 | title: 'Unpack firefox source', 62 | enabled: (ctx) => ctx.firefoxSourceTar, 63 | task: async (ctx, task) => { 64 | await unpackFirefoxSource(ctx.firefoxSourceTar, task) 65 | }, 66 | }, 67 | { 68 | title: 'Install windows artifacts', 69 | enabled: (ctx) => process.platform == 'win32', 70 | task: async (ctx) => { 71 | if (existsSync(resolve(homedir(), '.mozbuild'))) { 72 | log.info('Mozbuild directory already exists, not redownloading') 73 | } else { 74 | log.info('Mozbuild not found, downloading artifacts.') 75 | await downloadArtifacts() 76 | } 77 | }, 78 | }, 79 | { 80 | title: 'Init firefox', 81 | enabled: (ctx) => ctx.firefoxSourceTar && !process.env.CI_SKIP_INIT, 82 | task: async (_ctx, task) => await init(ENGINE_DIR, task), 83 | }, 84 | ...addons 85 | .map((addon) => includeAddon(addon.name, addon.url, addon.id)) 86 | .reduce((acc, cur) => [...acc, ...cur], []), 87 | { 88 | title: 'Add addons to mozbuild', 89 | task: async (ctx, task) => { 90 | // Discard the file to make sure it has no changes 91 | await discard('browser/extensions/moz.build', {}) 92 | 93 | const path = join(ENGINE_DIR, 'browser', 'extensions', 'moz.build') 94 | 95 | // Append all the files to the bottom 96 | writeFileSync( 97 | path, 98 | `${readFileSync(path, 'utf8')}\nDIRS += [${addons 99 | .map((addon) => addon.name) 100 | .sort() 101 | .map((addon) => `"${addon}"`) 102 | .join(',')}]` 103 | ) 104 | }, 105 | }, 106 | { 107 | title: 'Write metadata', 108 | task: () => writeMetadata(), 109 | }, 110 | { 111 | title: 'Cleanup', 112 | task: (ctx) => { 113 | let cwd = process.cwd().split(sep).join(posix.sep) 114 | 115 | if (process.platform == 'win32') { 116 | cwd = './' 117 | } 118 | 119 | if (ctx.firefoxSourceTar) { 120 | unlinkSync(resolve(cwd, '.dotbuild', 'engines', ctx.firefoxSourceTar)) 121 | } 122 | }, 123 | }, 124 | ]).run() 125 | 126 | log.success( 127 | `You should be ready to make changes to ${config.name}.\n\n\t You should import the patches next, run |${bin_name} import|.\n\t To begin building ${config.name}, run |${bin_name} build|.` 128 | ) 129 | console.log() 130 | } 131 | 132 | const includeAddon = ( 133 | name: string, 134 | downloadURL: string, 135 | id: string 136 | ): Listr.ListrTask[] => { 137 | const tempFile = join(MELON_TMP_DIR, name + '.xpi') 138 | const outPath = join(ENGINE_DIR, 'browser', 'extensions', name) 139 | 140 | return [ 141 | { 142 | title: `Download addon from ${downloadURL}`, 143 | skip: () => { 144 | if (existsSync(outPath)) { 145 | return `${downloadURL} has already been loaded to ${name}` 146 | } 147 | }, 148 | task: async (ctx, task) => { 149 | await downloadFileToLocation( 150 | downloadURL, 151 | tempFile, 152 | (msg) => (task.output = msg) 153 | ) 154 | ctx[name] = tempFile 155 | }, 156 | }, 157 | { 158 | title: `Unpack to ${name}`, 159 | enabled: (ctx) => typeof ctx[name] !== 'undefined', 160 | task: (ctx, task) => { 161 | const onData = (data: any) => { 162 | const d = data.toString() 163 | 164 | d.split('\n').forEach((line: any) => { 165 | if (line.trim().length !== 0) { 166 | const t = line.split(' ') 167 | t.shift() 168 | task.output = t.join(' ') 169 | } 170 | }) 171 | } 172 | 173 | task.output = `Unpacking extension...` 174 | 175 | return new Promise((res) => { 176 | mkdirSync(outPath, { 177 | recursive: true, 178 | }) 179 | 180 | const tarProc = execa('unzip', [ctx[name], '-d', outPath]) 181 | 182 | tarProc.stdout?.on('data', onData) 183 | tarProc.stdout?.on('error', (data) => { 184 | throw data 185 | }) 186 | 187 | tarProc.on('exit', () => { 188 | task.output = '' 189 | res() 190 | }) 191 | }) 192 | }, 193 | }, 194 | { 195 | title: 'Generate mozbuild', 196 | enabled: (ctx) => typeof ctx[name] !== 'undefined', 197 | task: async (ctx, task) => { 198 | const files = await walkDirectoryTree(outPath) 199 | 200 | function runTree(tree: any, parent: string): string { 201 | if (Array.isArray(tree)) { 202 | return tree 203 | .sort() 204 | .map( 205 | (file) => 206 | `FINAL_TARGET_FILES.features["${id}"]${parent} += ["${file 207 | .replace(outPath + '/', '') 208 | .replace(outPath, '')}"]` 209 | ) 210 | .join('\n') 211 | } 212 | 213 | const current = (tree['.'] as string[]) 214 | .sort() 215 | .map( 216 | (f) => 217 | `FINAL_TARGET_FILES.features["${id}"]${parent} += ["${f 218 | .replace(outPath + '/', '') 219 | .replace(outPath, '')}"]` 220 | ) 221 | .join('\n') 222 | 223 | const children = Object.keys(tree) 224 | .filter((folder) => folder !== '.') 225 | .filter((folder) => typeof tree[folder] !== 'undefined') 226 | .map((folder) => runTree(tree[folder], `${parent}["${folder}"]`)) 227 | .join('\n') 228 | 229 | return `${current}\n${children}` 230 | } 231 | 232 | writeFileSync( 233 | join(outPath, 'moz.build'), 234 | ` 235 | DEFINES["MOZ_APP_VERSION"] = CONFIG["MOZ_APP_VERSION"] 236 | DEFINES["MOZ_APP_MAXVERSION"] = CONFIG["MOZ_APP_MAXVERSION"] 237 | 238 | ${runTree(files, '')}` 239 | ) 240 | }, 241 | }, 242 | ] 243 | } 244 | 245 | async function unpackFirefoxSource( 246 | name: string, 247 | task: Listr.ListrTaskWrapper 248 | ): Promise { 249 | let cwd = process.cwd().split(sep).join(posix.sep) 250 | 251 | if (process.platform == 'win32') { 252 | cwd = './' 253 | } 254 | 255 | task.output = `Unpacking Firefox...` 256 | 257 | if (existsSync(ENGINE_DIR)) rmdirSync(ENGINE_DIR) 258 | mkdirSync(ENGINE_DIR) 259 | 260 | try { 261 | await execa( 262 | 'tar', 263 | [ 264 | '--transform', 265 | `s,firefox-${gFFVersion},engine,`, 266 | `--show-transformed`, 267 | process.platform == 'win32' ? '--force-local' : null, 268 | '-xf', 269 | resolve(cwd, '.dotbuild', 'engines', name), 270 | ].filter((x) => x) as string[] 271 | ) 272 | } catch (e) { 273 | const error = e as unknown as Error 274 | error.message = `\nThe following error may have been caused because you are using bsdtar. 275 | For MacOS users, please run |brew install gnu-tar| if the error includes "--transform is not supported" 276 | ${error.message}\n` 277 | 278 | throw e 279 | } 280 | } 281 | 282 | async function downloadFirefoxSource( 283 | version: string, 284 | task: Listr.ListrTaskWrapper 285 | ) { 286 | const base = `https://archive.mozilla.org/pub/firefox/releases/${version}/source/` 287 | const filename = `firefox-${version}.source.tar.xz` 288 | 289 | const url = base + filename 290 | 291 | task.output = `Locating Firefox release ${version}...` 292 | 293 | await ensureDir(resolve(process.cwd(), `.dotbuild`, `engines`)) 294 | 295 | if ( 296 | existsSync( 297 | resolve( 298 | process.cwd(), 299 | `.dotbuild`, 300 | `engines`, 301 | `firefox-${version.split('b')[0]}` 302 | ) 303 | ) 304 | ) { 305 | log.error( 306 | `Cannot download version ${ 307 | version.split('b')[0] 308 | } as it already exists at "${resolve( 309 | process.cwd(), 310 | `firefox-${version.split('b')[0]}` 311 | )}"` 312 | ) 313 | } 314 | 315 | if (version.includes('b')) 316 | task.output = 317 | 'WARNING Version includes non-numeric characters. This is probably a beta.' 318 | 319 | // Do not re-download if there is already an existing workspace present 320 | if (existsSync(ENGINE_DIR)) { 321 | log.error( 322 | `Workspace already exists.\nRemove that workspace and run |${bin_name} download ${version}| again.` 323 | ) 324 | } 325 | 326 | task.output = `Downloading Firefox release ${version}...` 327 | 328 | await downloadFileToLocation( 329 | url, 330 | resolve(process.cwd(), `.dotbuild`, `engines`, filename) 331 | ) 332 | return filename 333 | } 334 | -------------------------------------------------------------------------------- /src/commands/execute.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { log } from '..' 3 | import { ENGINE_DIR } from '../constants' 4 | import { dispatch } from '../utils' 5 | 6 | export const execute = async (_: any, cmd: any[]) => { 7 | if (existsSync(ENGINE_DIR)) { 8 | if (!cmd || cmd.length == 0) 9 | log.error('You need to specify a command to run.') 10 | 11 | const bin = cmd[0] 12 | const args = cmd 13 | args.shift() 14 | 15 | log.info( 16 | `Executing \`${bin}${args.length !== 0 ? ` ` : ``}${args.join( 17 | ' ' 18 | )}\` in \`src\`...` 19 | ) 20 | dispatch(bin, args, ENGINE_DIR) 21 | } else { 22 | log.error(`Unable to locate src directory.`) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/export-file.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, writeFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { log } from '..' 5 | import { ENGINE_DIR, SRC_DIR } from '../constants' 6 | import { delay, ensureDir } from '../utils' 7 | 8 | export const exportFile = async (file: string): Promise => { 9 | log.info(`Exporting ${file}...`) 10 | 11 | if (!existsSync(resolve(ENGINE_DIR, file))) 12 | throw new Error( 13 | `File ${file} could not be found in engine directory. Check the path for any mistakes and try again.` 14 | ) 15 | 16 | const proc = await execa( 17 | 'git', 18 | [ 19 | 'diff', 20 | '--src-prefix=a/', 21 | '--dst-prefix=b/', 22 | '--full-index', 23 | resolve(ENGINE_DIR, file), 24 | ], 25 | { 26 | cwd: ENGINE_DIR, 27 | stripFinalNewline: false, 28 | } 29 | ) 30 | const name = `${file 31 | .split('/') 32 | [file.replace(/\./g, '-').split('/').length - 1].replace(/\./g, '-')}.patch` 33 | 34 | const patchPath = file.replace(/\./g, '-').split('/').slice(0, -1) 35 | 36 | await ensureDir(resolve(SRC_DIR, ...patchPath)) 37 | 38 | if (proc.stdout.length >= 8000) { 39 | log.warning('') 40 | log.warning( 41 | `Exported patch is over 8000 characters. This patch may become hard to manage in the future.` 42 | ) 43 | log.warning( 44 | `We recommend trying to decrease your patch size by making minimal edits to the source.` 45 | ) 46 | log.warning('') 47 | await delay(2000) 48 | } 49 | 50 | writeFileSync(resolve(SRC_DIR, ...patchPath, name), proc.stdout) 51 | log.info(`Wrote "${name}" to patches directory.`) 52 | console.log() 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/fix-le.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readdirSync, readFileSync } from 'fs' 2 | import { resolve } from 'path' 3 | import { log } from '..' 4 | import { ENGINE_DIR, PATCHES_DIR } from '../constants' 5 | import { dispatch } from '../utils' 6 | 7 | export const fixLineEndings = async (): Promise => { 8 | let patches = readdirSync(PATCHES_DIR) 9 | 10 | patches = patches.filter((p) => p !== '.index') 11 | 12 | await Promise.all( 13 | patches.map(async (patch) => { 14 | const patchContents = readFileSync(resolve(PATCHES_DIR, patch), 'utf-8') 15 | const originalPath = patchContents 16 | .split('diff --git a/')[1] 17 | .split(' b/')[0] 18 | 19 | if (existsSync(resolve(ENGINE_DIR, originalPath))) { 20 | dispatch('dos2unix', [originalPath], ENGINE_DIR).then(async (_) => { 21 | await dispatch('dos2unix', [patch], PATCHES_DIR) 22 | }) 23 | } else { 24 | log.warning(`Skipping ${patch} as it no longer exists in tree...`) 25 | } 26 | }) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/import-patches.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync, readdirSync, writeFileSync } from 'fs' 2 | import { sync } from 'glob' 3 | import { join, resolve } from 'path' 4 | import { config, log } from '..' 5 | import { SRC_DIR } from '../constants' 6 | import { BrandingPatch, BRANDING_DIR } from '../controllers/brandingPatch' 7 | import { ManualPatch, PatchBase, PatchFile } from '../controllers/patch' 8 | import manualPatches from '../manual-patches' 9 | import { patchCountFile } from '../middleware/patch-check' 10 | import { walkDirectory } from '../utils' 11 | 12 | function enumerate(array: T[]): [T, number][] { 13 | return array.map<[T, number]>((item, i) => [item, i]) 14 | } 15 | 16 | const importManual = async (minimal?: boolean, noIgnore?: boolean) => { 17 | log.info(`Applying ${manualPatches.length} manual patches...`) 18 | 19 | if (!minimal) console.log() 20 | 21 | for await (const [{ name, action, src }, i] of enumerate(manualPatches)) { 22 | const patch = new ManualPatch( 23 | name, 24 | [i, manualPatches.length], 25 | { minimal, noIgnore }, 26 | action, 27 | src 28 | ) 29 | 30 | await patch.apply() 31 | } 32 | 33 | log.success(`Successfully imported ${manualPatches.length} manual patches!`) 34 | console.log() 35 | 36 | log.info('Storing patch count...') 37 | 38 | const fileList = await walkDirectory(resolve(process.cwd(), 'src')) 39 | const fileCount = fileList.length 40 | 41 | writeFileSync(patchCountFile, fileCount.toString()) 42 | } 43 | 44 | const importPatchFiles = async (minimal?: boolean, noIgnore?: boolean) => { 45 | let patches = sync('**/*.patch', { 46 | nodir: true, 47 | cwd: SRC_DIR, 48 | }) 49 | 50 | patches = patches 51 | .filter((p) => p !== '.index') 52 | .filter((p) => !p.includes('node_modules')) 53 | 54 | log.info(`Applying ${patches.length} patch files...`) 55 | 56 | if (!minimal) console.log() 57 | 58 | for await (const [patchName, i] of enumerate(patches)) { 59 | const patch = new PatchFile( 60 | patchName, 61 | [i, patches.length], 62 | { minimal, noIgnore }, 63 | resolve(SRC_DIR, patchName) 64 | ) 65 | 66 | await patch.apply() 67 | } 68 | 69 | console.log() 70 | // TODO: Setup a custom patch doctor 71 | // await dispatch( 72 | // `./${bin_name}`, 73 | // ['doctor', 'patches'], 74 | // process.cwd(), 75 | // true, 76 | // true 77 | // ) 78 | 79 | log.success(`Successfully imported ${patches.length} patch files!`) 80 | } 81 | 82 | const importMelonPatches = async (minimal?: boolean) => { 83 | const patches: PatchBase[] = [] 84 | 85 | log.info(`Applying ${patches.length} melon patches...`) 86 | 87 | if (config.buildOptions.generateBranding) { 88 | for (const brandingStyle of readdirSync(BRANDING_DIR).filter((file) => 89 | lstatSync(join(BRANDING_DIR, file)).isDirectory() 90 | )) { 91 | console.log(brandingStyle) 92 | patches.push(new BrandingPatch(brandingStyle, minimal)) 93 | } 94 | } 95 | 96 | if (!minimal) console.log() 97 | 98 | for await (const [patch, i] of enumerate(patches)) { 99 | await patch.applyWithStatus([i, patches.length]) 100 | } 101 | 102 | console.log() 103 | 104 | log.success(`Successfully imported ${patches.length} melon patches!`) 105 | } 106 | 107 | interface Args { 108 | minimal?: boolean 109 | noignore?: boolean 110 | } 111 | 112 | export const importPatches = async ( 113 | type: string, 114 | args: Args 115 | ): Promise => { 116 | if (type) { 117 | if (type == 'manual') await importManual(args.minimal) 118 | else if (type == 'file') await importPatchFiles(args.minimal) 119 | } else { 120 | await importMelonPatches(args.minimal) 121 | await importManual(args.minimal, args.noignore) 122 | await importPatchFiles(args.minimal, args.noignore) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bootstrap' 2 | export * from './build' 3 | export * from './discard' 4 | export * from './download' 5 | export * from './download-artifacts' 6 | export * from './execute' 7 | export * from './export-file' 8 | export * from './fix-le' 9 | export * from './import-patches' 10 | export * from './init' 11 | export * from './license-check' 12 | export * from './package' 13 | export * from './reset' 14 | export * from './run' 15 | export * from './set-branch' 16 | export * from './status' 17 | export * from './test' 18 | export * from './setupProject' 19 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander' 2 | import { existsSync, readFileSync } from 'fs' 3 | import Listr from 'listr' 4 | import { resolve } from 'path' 5 | import { bin_name, log } from '..' 6 | import { dispatch } from '../utils' 7 | 8 | export const init = async ( 9 | directory: Command | string, 10 | task?: Listr.ListrTaskWrapper 11 | ): Promise => { 12 | function logInfo(data: string) { 13 | if (task) { 14 | task.output = data 15 | } else { 16 | log.info(data) 17 | } 18 | } 19 | 20 | const cwd = process.cwd() 21 | 22 | const dir = resolve(cwd as string, directory.toString()) 23 | 24 | if (!existsSync(dir)) { 25 | log.error( 26 | `Directory "${directory}" not found.\nCheck the directory exists and run |${bin_name} init| again.` 27 | ) 28 | } 29 | 30 | let version = readFileSync( 31 | resolve( 32 | cwd, 33 | directory.toString(), 34 | 'browser', 35 | 'config', 36 | 'version_display.txt' 37 | ), 38 | 'utf-8' 39 | ) 40 | 41 | if (!version) 42 | log.error( 43 | `Directory "${directory}" not found.\nCheck the directory exists and run |${bin_name} init| again.` 44 | ) 45 | 46 | version = version.trim().replace(/\\n/g, '') 47 | 48 | logInfo('Initializing git, this may take some time') 49 | await dispatch('git', ['init'], dir as string, false, logInfo) 50 | await dispatch( 51 | 'git', 52 | ['checkout', '--orphan', version], 53 | dir as string, 54 | false, 55 | logInfo 56 | ) 57 | await dispatch('git', ['add', '-f', '.'], dir as string, false, logInfo) 58 | await dispatch( 59 | 'git', 60 | ['commit', '-am', `"Firefox ${version}"`], 61 | dir as string, 62 | false, 63 | logInfo 64 | ) 65 | await dispatch( 66 | 'git', 67 | ['checkout', '-b', 'dot'], 68 | dir as string, 69 | false, 70 | logInfo 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/commands/license-check.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { readdirSync, readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { log } from '..' 5 | import { ENGINE_DIR, PATCHES_DIR } from '../constants' 6 | 7 | const ignoredExt = ['.json', '.bundle.js'] 8 | 9 | export const licenseCheck = async () => { 10 | log.info('Checking project...') 11 | 12 | let patches = readdirSync(PATCHES_DIR).map((p) => p) 13 | 14 | patches = patches.filter((p) => p !== '.index') 15 | 16 | const originalPaths = patches.map((p) => { 17 | const data = readFileSync(resolve(PATCHES_DIR, p), 'utf-8') 18 | 19 | return data.split('diff --git a/')[1].split(' b/')[0] 20 | }) 21 | 22 | const passed: string[] = [] 23 | const failed: string[] = [] 24 | const ignored: string[] = [] 25 | 26 | originalPaths.forEach((p) => { 27 | const data = readFileSync(resolve(ENGINE_DIR, p), 'utf-8') 28 | const headerRegion = data.split('\n').slice(0, 32).join(' ') 29 | 30 | const passes = 31 | headerRegion.includes('http://mozilla.org/MPL/2.0') && 32 | headerRegion.includes('This Source Code Form') && 33 | headerRegion.includes('copy of the MPL') 34 | 35 | const isIgnored = !!ignoredExt.find((i) => p.endsWith(i)) 36 | isIgnored && ignored.push(p) 37 | 38 | if (!isIgnored) { 39 | if (passes) passed.push(p) 40 | else if (!passes) failed.push(p) 41 | } 42 | }) 43 | 44 | const maxPassed = 5 45 | let i = 0 46 | 47 | for (const p of passed) { 48 | log.info(`${p}... ${chalk.green('✔ Pass - MPL-2.0')}`) 49 | 50 | if (i >= maxPassed) { 51 | log.info( 52 | `${chalk.gray.italic( 53 | `${passed.length - maxPassed} other files...` 54 | )} ${chalk.green('✔ Pass - MPL-2.0')}` 55 | ) 56 | break 57 | } 58 | 59 | ++i 60 | } 61 | 62 | failed.forEach((p, i) => { 63 | log.info(`${p}... ${chalk.red('❗ Failed')}`) 64 | }) 65 | 66 | ignored.forEach((p, i) => { 67 | log.info(`${p}... ${chalk.gray('➖ Ignored')}`) 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /src/commands/linus.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'linus' { 2 | export function name(callback: (error: Error, name: string) => void): void 3 | } 4 | -------------------------------------------------------------------------------- /src/commands/package.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { bin_name, log } from '..' 5 | import { ENGINE_DIR } from '../constants' 6 | 7 | export const melonPackage = async () => { 8 | if (existsSync(ENGINE_DIR)) { 9 | const artifactPath = resolve(ENGINE_DIR, 'mach') 10 | 11 | if (existsSync(artifactPath)) { 12 | const args = ['package'] 13 | 14 | log.info( 15 | `Packaging \`dot\` with args ${JSON.stringify(args.slice(1, 0))}...` 16 | ) 17 | 18 | execa(artifactPath, args).stdout?.pipe(process.stdout) 19 | } else { 20 | log.error(`Cannot binary with name \`mach\` in ${resolve(ENGINE_DIR)}`) 21 | } 22 | } else { 23 | log.error( 24 | `Unable to locate any source directories.\nRun |${bin_name} download| to generate the source directory.` 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/reset.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync } from 'fs' 3 | import { resolve } from 'path' 4 | import prompts from 'prompts' 5 | import rimraf from 'rimraf' 6 | import { bin_name, log } from '..' 7 | import { ENGINE_DIR } from '../constants' 8 | import { IPatch } from '../interfaces/patch' 9 | import manualPatches from '../manual-patches' 10 | 11 | export const reset = async (): Promise => { 12 | try { 13 | log.warning( 14 | 'This will clear all your unexported changes in the `src` directory!' 15 | ) 16 | log.warning(`You can export your changes by running |${bin_name} export|.`) 17 | 18 | const { answer } = await prompts({ 19 | type: 'confirm', 20 | name: 'answer', 21 | message: 'Are you sure you want to continue?', 22 | }) 23 | 24 | if (answer) { 25 | await execa('git', ['checkout', '.'], { cwd: ENGINE_DIR }) 26 | 27 | manualPatches.forEach(async (patch: IPatch) => { 28 | const { src, action } = patch 29 | 30 | if (action == 'copy') { 31 | if (typeof src === 'string') { 32 | const path = resolve(ENGINE_DIR, src) 33 | 34 | if (path !== ENGINE_DIR) { 35 | log.info(`Deleting ${src}...`) 36 | 37 | if (existsSync(path)) rimraf.sync(path) 38 | } 39 | } else if (Array.isArray(src)) { 40 | src.forEach((i) => { 41 | const path = resolve(ENGINE_DIR, i) 42 | 43 | if (path !== ENGINE_DIR) { 44 | log.info(`Deleting ${i}...`) 45 | 46 | if (existsSync(path)) rimraf.sync(path) 47 | } 48 | }) 49 | } 50 | } else { 51 | log.warning( 52 | 'Resetting does not work on manual patches that have a `delete` action, skipping...' 53 | ) 54 | } 55 | }) 56 | 57 | const leftovers = new Set() 58 | 59 | const { stdout: origFiles } = await execa( 60 | 'git', 61 | ['clean', '-e', "'!*.orig'", '--dry-run'], 62 | { cwd: ENGINE_DIR } 63 | ) 64 | 65 | const { stdout: rejFiles } = await execa( 66 | 'git', 67 | ['clean', '-e', "'!*.rej'", '--dry-run'], 68 | { cwd: ENGINE_DIR } 69 | ) 70 | 71 | origFiles 72 | .split('\n') 73 | .map((f) => leftovers.add(f.replace(/Would remove /, ''))) 74 | rejFiles 75 | .split('\n') 76 | .map((f) => leftovers.add(f.replace(/Would remove /, ''))) 77 | 78 | Array.from(leftovers).forEach((f: any) => { 79 | const path = resolve(ENGINE_DIR, f) 80 | 81 | if (path !== ENGINE_DIR) { 82 | log.info(`Deleting ${f}...`) 83 | 84 | rimraf.sync(resolve(ENGINE_DIR, f)) 85 | } 86 | }) 87 | 88 | log.success('Reset successfully.') 89 | log.info( 90 | 'Next time you build, it may need to recompile parts of the program because the cache was invalidated.' 91 | ) 92 | } 93 | } catch (e) {} 94 | } 95 | -------------------------------------------------------------------------------- /src/commands/run.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readdirSync } from 'fs' 2 | import { resolve } from 'path' 3 | import { bin_name, log } from '..' 4 | import { ENGINE_DIR } from '../constants' 5 | import { dispatch } from '../utils' 6 | 7 | export const run = async (chrome?: string) => { 8 | const dirs = readdirSync(ENGINE_DIR) 9 | const objDirname: any = dirs.find((dir) => dir.startsWith('obj-')) 10 | 11 | if (!objDirname) { 12 | throw new Error('Dot Browser needs to be built before you can do this.') 13 | } 14 | 15 | const objDir = resolve(ENGINE_DIR, objDirname) 16 | 17 | if (existsSync(objDir)) { 18 | dispatch( 19 | './mach', 20 | ['run'].concat(chrome ? ['-chrome', chrome] : []), 21 | ENGINE_DIR, 22 | true 23 | ) 24 | } else { 25 | log.error( 26 | `Unable to locate any built binaries.\nRun |${bin_name} build| to initiate a build.` 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/set-branch.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, readFileSync, writeFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { log } from '..' 5 | 6 | export const setBranch = async (branch: string): Promise => { 7 | if (!existsSync(resolve(process.cwd(), '.dotbuild', 'metadata'))) { 8 | return log.error('Cannot find metadata, aborting...') 9 | } 10 | 11 | const metadata = JSON.parse( 12 | readFileSync(resolve(process.cwd(), '.dotbuild', 'metadata'), 'utf-8') 13 | ) 14 | 15 | try { 16 | await execa('git', ['rev-parse', '--verify', branch]) 17 | 18 | metadata.branch = branch 19 | 20 | writeFileSync( 21 | resolve(process.cwd(), '.dotbuild', 'metadata'), 22 | JSON.stringify(metadata) 23 | ) 24 | 25 | log.success(`Default branch is at \`${branch}\`.`) 26 | } catch (e) { 27 | return log.error(`Branch with name \`${branch}\` does not exist.`) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/setupProject.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs' 2 | import { copyFile } from 'fs/promises' 3 | import { join, dirname } from 'path' 4 | 5 | import prompts from 'prompts' 6 | 7 | import { log } from '..' 8 | import { 9 | Config, 10 | configPath, 11 | delay, 12 | getLatestFF, 13 | projectDir, 14 | SupportedProducts, 15 | walkDirectory, 16 | } from '../utils' 17 | 18 | // ============================================================================= 19 | // User interaction portion 20 | 21 | export async function setupProject(): Promise { 22 | try { 23 | if (existsSync(configPath)) { 24 | log.warning('There is already a config file. This will overwrite it!') 25 | await delay(1000) 26 | } 27 | 28 | if (configPath.includes('.optional')) { 29 | log.error( 30 | 'The text ".optional" cannot be in the path to your custom browser' 31 | ) 32 | process.exit(1) 33 | } 34 | 35 | // Ask user for assorted information 36 | const { product } = await prompts({ 37 | type: 'select', 38 | name: 'product', 39 | message: 'Select a product to fork', 40 | choices: [ 41 | { title: 'Firefox stable', value: SupportedProducts.Firefox }, 42 | { 43 | title: 'Firefox extended support (older)', 44 | value: SupportedProducts.FirefoxESR, 45 | }, 46 | { 47 | title: 'Firefox extended support (newer)', 48 | value: SupportedProducts.FirefoxESRNext, 49 | }, 50 | { 51 | title: 'Firefox developer edition (Not recommended)', 52 | value: SupportedProducts.FirefoxDev, 53 | }, 54 | { 55 | title: 'Firefox beta (Not recommended)', 56 | value: SupportedProducts.FirefoxBeta, 57 | }, 58 | { 59 | title: 'Firefox Nightly (Not recommended)', 60 | value: SupportedProducts.FirefoxNightly, 61 | }, 62 | ], 63 | }) 64 | 65 | if (typeof product === 'undefined') return 66 | 67 | const productVersion = await getLatestFF(product) 68 | 69 | const { version, name, appId, vendor, ui } = await prompts([ 70 | { 71 | type: 'text', 72 | name: 'version', 73 | message: 'Enter the version of this product', 74 | initial: productVersion, 75 | }, 76 | { 77 | type: 'text', 78 | name: 'name', 79 | message: 'Enter a product name', 80 | initial: 'Example browser', 81 | }, 82 | { 83 | type: 'text', 84 | name: 'vendor', 85 | message: 'Enter a vendor', 86 | initial: 'Example company', 87 | }, 88 | { 89 | type: 'text', 90 | name: 'appId', 91 | message: 'Enter an appid', 92 | initial: 'com.example.browser', 93 | // Horrible validation to make sure people don't chose something entirely wrong 94 | validate: (t: string) => t.includes('.'), 95 | }, 96 | { 97 | type: 'select', 98 | name: 'ui', 99 | message: 'Select a ui mode template', 100 | choices: [ 101 | { 102 | title: 'None', 103 | value: 'none', 104 | }, 105 | { 106 | title: 'User Chrome (custom browser css, simplest)', 107 | value: 'uc', 108 | }, 109 | { 110 | title: 'Custom html', 111 | value: 'html', 112 | }, 113 | ], 114 | }, 115 | ]) 116 | 117 | const config: Partial = { 118 | name, 119 | vendor, 120 | appId, 121 | version: { product, version, displayVersion: '1.0.0' }, 122 | buildOptions: { 123 | generateBranding: false, 124 | windowsUseSymbolicLinks: false, 125 | }, 126 | } 127 | 128 | await copyRequired() 129 | 130 | if (ui === 'html') { 131 | await copyOptional([ 132 | 'customui', 133 | 'toolkit-mozbuild.patch', 134 | 'confvars-sh.patch', 135 | ]) 136 | } else if (ui === 'uc') { 137 | await copyOptional(['browser/themes']) 138 | } 139 | 140 | writeFileSync(configPath, JSON.stringify(config, null, 2)) 141 | 142 | // Append important stuff to gitignore 143 | const gitignore = join(projectDir, '.gitignore') 144 | let gitignoreContents = '' 145 | 146 | if (existsSync(gitignore)) { 147 | gitignoreContents = readFileSync(gitignore, 'utf8') 148 | } 149 | 150 | gitignoreContents += '\n.dotbuild/\nengine/\nfirefox-*/\nnode_modules/\n' 151 | 152 | writeFileSync(gitignore, gitignoreContents) 153 | } catch (e) { 154 | console.log(e) 155 | } 156 | } 157 | 158 | // ============================================================================= 159 | // Filesystem templating 160 | 161 | export const templateDir = join(__dirname, '../..', 'template') 162 | 163 | async function copyOptional(files: string[]) { 164 | await Promise.all( 165 | ( 166 | await walkDirectory(templateDir) 167 | ) 168 | .filter((f) => f.includes('.optional')) 169 | .filter((f) => files.map((file) => f.includes(file)).some((b) => b)) 170 | .map(async (file) => { 171 | const out = join(projectDir, file.replace(templateDir, '')).replace( 172 | '.optional', 173 | '' 174 | ) 175 | if (!existsSync(out)) { 176 | mkdirSync(dirname(out), { recursive: true }) 177 | await copyFile(file, out) 178 | } 179 | }) 180 | ) 181 | } 182 | 183 | async function copyRequired() { 184 | await Promise.all( 185 | ( 186 | await walkDirectory(templateDir) 187 | ) 188 | .filter((f) => !f.includes('.optional')) 189 | .map(async (file) => { 190 | const out = join(projectDir, file.replace(templateDir, '')) 191 | if (!existsSync(out)) { 192 | mkdirSync(dirname(out), { recursive: true }) 193 | await copyFile(file, out) 194 | } 195 | }) 196 | ) 197 | } 198 | -------------------------------------------------------------------------------- /src/commands/status.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { log } from '..' 3 | import { ENGINE_DIR } from '../constants' 4 | import { dispatch, hasConfig } from '../utils' 5 | 6 | export const status = async (): Promise => { 7 | const configExists = hasConfig() 8 | const engineExists = existsSync(ENGINE_DIR) 9 | 10 | if (!configExists && !engineExists) { 11 | log.info( 12 | "Melon doesn't appear to be setup for this project. You can set it up by running |melon setup-project|" 13 | ) 14 | 15 | return 16 | } 17 | 18 | if (engineExists) { 19 | log.info("The following changes have been made to firefox's source code") 20 | await dispatch('git', ['diff'], ENGINE_DIR) 21 | 22 | return 23 | } else { 24 | log.info( 25 | "It appears that melon has been configured, but you haven't run |melon download|" 26 | ) 27 | 28 | return 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { dispatch } from '../utils' 3 | 4 | export const test = async () => { 5 | dispatch('yarn', ['test'], resolve(process.cwd(), 'src', 'dot')) 6 | } 7 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, mkdirSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { log } from '..' 5 | 6 | export const BUILD_TARGETS = ['linux', 'windows', 'macos'] 7 | 8 | export const ARCHITECTURE = ['i686', 'x86_64'] 9 | 10 | export const PATCH_ARGS = [ 11 | '--ignore-space-change', 12 | '--ignore-whitespace', 13 | '--verbose', 14 | ] 15 | 16 | export const ENGINE_DIR = resolve(process.cwd(), 'engine') 17 | export const SRC_DIR = resolve(process.cwd(), 'src') 18 | export const PATCHES_DIR = resolve(process.cwd(), 'patches') 19 | export const COMMON_DIR = resolve(process.cwd(), 'common') 20 | export const CONFIGS_DIR = resolve(process.cwd(), 'configs') 21 | export const MELON_DIR = resolve(process.cwd(), '.dotbuild') 22 | export const MELON_TMP_DIR = resolve(process.cwd(), '.dotbuild', 'engine') 23 | 24 | mkdirSync(MELON_TMP_DIR, { recursive: true }) 25 | 26 | export let CONFIG_GUESS: string = '' 27 | 28 | // We should only try and generate this config file if the engine directory has 29 | // been created 30 | if (existsSync(ENGINE_DIR)) { 31 | if (!existsSync(resolve(ENGINE_DIR, 'build/autoconf/config.guess'))) { 32 | log.warning( 33 | 'The engine directory has been created, but has not been build properly.' 34 | ) 35 | } else { 36 | try { 37 | CONFIG_GUESS = execa.commandSync('./build/autoconf/config.guess', { 38 | cwd: ENGINE_DIR, 39 | }).stdout 40 | } catch (e) { 41 | log.warning( 42 | 'An error occurred running engine/build/autoconf/config.guess' 43 | ) 44 | log.warning(e) 45 | log.askForReport() 46 | 47 | process.exit(1) 48 | } 49 | } 50 | } 51 | 52 | export const OBJ_DIR = resolve(ENGINE_DIR, `obj-${CONFIG_GUESS}`) 53 | 54 | export const FTL_STRING_LINE_REGEX = 55 | /(([a-zA-Z0-9-]*|\.[a-z-]*) =(.*|\.)|\[[a-zA-Z0-9]*\].*(\n\s?\s?})?|\*\[[a-zA-Z0-9]*\] .*(\n\s?\s?})?)/gm 56 | -------------------------------------------------------------------------------- /src/controllers/brandingPatch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | copyFileSync, 3 | existsSync, 4 | mkdirSync, 5 | readdirSync, 6 | readFileSync, 7 | rmdirSync, 8 | writeFileSync, 9 | } from 'fs' 10 | import { dirname, extname, join } from 'path' 11 | import sharp from 'sharp' 12 | import { config, log } from '..' 13 | import { templateDir } from '../commands' 14 | 15 | import { CONFIGS_DIR, ENGINE_DIR } from '../constants' 16 | import { 17 | defaultBrandsConfig, 18 | mkdirpSync, 19 | stringTemplate, 20 | walkDirectory, 21 | } from '../utils' 22 | import { PatchBase } from './patch' 23 | 24 | export const BRANDING_DIR = join(CONFIGS_DIR, 'branding') 25 | const BRANDING_STORE = join(ENGINE_DIR, 'browser', 'branding') 26 | const BRANDING_FF = join(BRANDING_STORE, 'unofficial') 27 | 28 | const CSS_REPLACE_REGEX = new RegExp( 29 | '#130829|hsla\\(235, 43%, 10%, .5\\)', 30 | 'gm' 31 | ) 32 | 33 | export class BrandingPatch extends PatchBase { 34 | name: string 35 | 36 | outputPath: string 37 | configPath: string 38 | 39 | constructor(name: string, minimal?: boolean) { 40 | super(`Browser branding: ${name}`, [0, 0], { minimal }) 41 | this.name = name 42 | 43 | this.outputPath = join(BRANDING_STORE, name) 44 | this.configPath = join(BRANDING_DIR, name) 45 | } 46 | 47 | private checkForFaults(): void { 48 | if (!existsSync(this.configPath)) { 49 | throw new Error(`Branding ${this.name} does not exist`) 50 | } 51 | 52 | const requiredFiles = ['logo.png'].map((file) => 53 | join(this.configPath, file) 54 | ) 55 | const requiredFilesExist = this.filesExist(requiredFiles) 56 | 57 | if (!requiredFilesExist) { 58 | throw new Error( 59 | `Missing some of the required files: ${requiredFiles 60 | .filter((file) => !existsSync(file)) 61 | .join(', ')}` 62 | ) 63 | } 64 | } 65 | 66 | async apply(): Promise { 67 | this.start() 68 | 69 | try { 70 | log.debug('Checking branding files') 71 | 72 | this.checkForFaults() 73 | 74 | const brandingConfig = { 75 | ...(config.brands[this.name] || {}), 76 | ...defaultBrandsConfig, 77 | } 78 | 79 | log.debug(`Creating folder ${this.outputPath}`) 80 | 81 | if (existsSync(this.outputPath)) 82 | rmdirSync(this.outputPath, { recursive: true }) 83 | mkdirSync(this.outputPath, { recursive: true }) 84 | 85 | await this.setupImages() 86 | 87 | log.debug(`Setup locales`) 88 | 89 | this.setupLocales(brandingConfig) 90 | 91 | log.debug(`Copying files from ${BRANDING_FF}`) 92 | 93 | await this.copyMozillaFiles(brandingConfig) 94 | 95 | this.done = true 96 | } catch (e) { 97 | this.error = e 98 | this.done = false 99 | } 100 | } 101 | 102 | private async copyMozillaFiles(brandingConfig: { 103 | backgroundColor: string 104 | brandShorterName: string 105 | brandShortName: string 106 | brandFullName: string 107 | }) { 108 | const files = (await walkDirectory(BRANDING_FF)).filter( 109 | (file) => 110 | !existsSync(join(this.outputPath, file.replace(BRANDING_FF, ''))) 111 | ) 112 | 113 | const css = files.filter((file) => extname(file).includes('css')) 114 | 115 | const everythingElse = files.filter((file) => !css.includes(file)) 116 | 117 | css 118 | .map((filePath) => [ 119 | readFileSync(filePath, 'utf-8'), 120 | join(this.outputPath, filePath.replace(BRANDING_FF, '')), 121 | ]) 122 | .map(([contents, path]) => [ 123 | contents.replace(CSS_REPLACE_REGEX, 'var(--theme-bg)') + 124 | `:root { --theme-bg: ${brandingConfig.backgroundColor} }`, 125 | path, 126 | ]) 127 | .forEach(([contents, path]) => { 128 | mkdirSync(dirname(path), { recursive: true }) 129 | writeFileSync(path, contents) 130 | }) 131 | 132 | // Copy everything else from the default firefox branding directory 133 | everythingElse.forEach((file) => { 134 | mkdirpSync(dirname(join(this.outputPath, file.replace(BRANDING_FF, '')))) 135 | copyFileSync(file, join(this.outputPath, file.replace(BRANDING_FF, ''))) 136 | }) 137 | } 138 | 139 | private setupLocales(brandingConfig: { 140 | backgroundColor: string 141 | brandShorterName: string 142 | brandShortName: string 143 | brandFullName: string 144 | }) { 145 | readdirSync(join(templateDir, 'branding.optional')) 146 | .map((file) => [ 147 | readFileSync(join(templateDir, 'branding.optional', file), 'utf-8'), 148 | join(this.outputPath, 'locales/en-US', file), 149 | ]) 150 | .forEach(([contents, path]) => { 151 | mkdirSync(dirname(path), { recursive: true }) 152 | writeFileSync(path, stringTemplate(contents, brandingConfig)) 153 | }) 154 | } 155 | 156 | private async setupImages() { 157 | log.debug('Creating default*.png files') 158 | 159 | for (const size of [16, 22, 24, 32, 48, 64, 128, 256]) { 160 | await sharp(join(this.configPath, 'logo.png')) 161 | .resize(size, size) 162 | .toFile(join(this.outputPath, `default${size}.png`)) 163 | } 164 | 165 | log.debug('Creating firefox*.ico') 166 | 167 | await sharp(join(this.configPath, 'logo.png')) 168 | .resize(512, 512) 169 | .toFile(join(this.outputPath, 'firefox.ico')) 170 | await sharp(join(this.configPath, 'logo.png')) 171 | .resize(64, 64) 172 | .toFile(join(this.outputPath, 'firefox64.ico')) 173 | 174 | log.debug('Creating content/about-logo*.png') 175 | 176 | mkdirSync(join(this.outputPath, 'content'), { recursive: true }) 177 | 178 | await sharp(join(this.configPath, 'logo.png')) 179 | .resize(512, 512) 180 | .toFile(join(this.outputPath, 'content', 'about-logo.png')) 181 | await sharp(join(this.configPath, 'logo.png')) 182 | .resize(1024, 1024) 183 | .toFile(join(this.outputPath, 'content', 'about-logo@2x.png')) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/controllers/patch.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import execa from 'execa' 3 | import { existsSync, rmdirSync, rmSync, statSync } from 'fs' 4 | import { resolve } from 'path' 5 | import readline from 'readline' 6 | import { log } from '..' 7 | import { ENGINE_DIR, PATCH_ARGS } from '../constants' 8 | import { copyManual } from '../utils' 9 | 10 | export abstract class PatchBase { 11 | protected name: string 12 | 13 | protected status: number[] 14 | 15 | protected options: { 16 | minimal?: boolean 17 | noIgnore?: boolean 18 | } 19 | 20 | private _done = false 21 | 22 | protected error: Error | unknown 23 | 24 | constructor( 25 | name: string, 26 | status: number[], 27 | options: { 28 | minimal?: boolean 29 | noIgnore?: boolean 30 | } = {} 31 | ) { 32 | this.name = name 33 | this.status = status 34 | this.options = options 35 | } 36 | 37 | protected get done(): boolean { 38 | return this._done 39 | } 40 | 41 | protected set done(_: boolean) { 42 | this._done = _ 43 | 44 | if (this.options.minimal) return 45 | 46 | // Move to the start of the last line 47 | readline.moveCursor(process.stdout, 0, -1) 48 | readline.clearLine(process.stdout, 1) 49 | 50 | // Print the status 51 | log.info( 52 | `${chalk.gray(`(${this.status[0]}/${this.status[1]})`)} Applying ${ 53 | this.name 54 | }... ${chalk[this._done ? 'green' : 'red'].bold( 55 | this._done ? 'Done ✔' : 'Error ❗' 56 | )}` 57 | ) 58 | 59 | // If there is an error present, it should be thrown 60 | if (this.error) { 61 | throw this.error 62 | } 63 | } 64 | 65 | protected start(): void { 66 | if (this.options.minimal) return 67 | 68 | log.info( 69 | `${chalk.gray(`(${this.status[0]}/${this.status[1]})`)} Applying ${ 70 | this.name 71 | }...` 72 | ) 73 | } 74 | 75 | public applyWithStatus(status: [number, number]): Promise { 76 | this.status = status 77 | return this.apply() 78 | } 79 | 80 | protected filesExist(files: string[]): boolean { 81 | return files.every((file) => existsSync(file)) 82 | } 83 | 84 | abstract apply(): Promise 85 | } 86 | 87 | export class ManualPatch extends PatchBase { 88 | private action: 'copy' | 'delete' 89 | 90 | private src: string | string[] 91 | 92 | constructor( 93 | name: string, 94 | status: number[], 95 | options: { 96 | minimal?: boolean 97 | noIgnore?: boolean 98 | } = {}, 99 | action: 'copy' | 'delete', 100 | src: string | string[] 101 | ) { 102 | super(name, status, options) 103 | 104 | this.action = action 105 | this.src = src 106 | } 107 | 108 | private delete(parent: string, loc: string) { 109 | const target = resolve(parent, loc) 110 | 111 | if (!existsSync(target)) { 112 | log.error( 113 | `We were unable to delete the file or directory \`${this.src}\` as it doesn't exist in the src directory.` 114 | ) 115 | 116 | return 117 | } 118 | 119 | const targetInfo = statSync(target) 120 | 121 | if (targetInfo.isDirectory()) { 122 | rmdirSync(target) 123 | } else { 124 | rmSync(target, { force: true }) 125 | } 126 | } 127 | 128 | async apply(): Promise { 129 | this.start() 130 | 131 | try { 132 | switch (this.action) { 133 | case 'copy': 134 | if (typeof this.src === 'string') { 135 | await copyManual(this.src, this.options.noIgnore) 136 | } else if (Array.isArray(this.src)) { 137 | for (const item of this.src) { 138 | await copyManual(item, this.options.noIgnore) 139 | } 140 | } else { 141 | throw new Error( 142 | `'${this.src}' is not a valid source. Please provide a string or an array` 143 | ) 144 | } 145 | 146 | break 147 | 148 | case 'delete': 149 | if (typeof this.src === 'string') { 150 | this.delete(ENGINE_DIR, this.src) 151 | } else if (Array.isArray(this.src)) { 152 | for (const item of this.src) { 153 | this.delete(ENGINE_DIR, item) 154 | } 155 | } else { 156 | throw new Error( 157 | `'${this.src}' is not a valid source. Please provide a string or an array` 158 | ) 159 | } 160 | 161 | break 162 | 163 | default: 164 | throw new Error(`Unknown manual patch action: ${this.action}`) 165 | } 166 | 167 | this.done = true 168 | } catch (e) { 169 | this.error = e 170 | this.done = false 171 | } 172 | } 173 | } 174 | 175 | export class PatchFile extends PatchBase { 176 | private src: string 177 | 178 | constructor( 179 | name: string, 180 | status: number[], 181 | options: { 182 | minimal?: boolean 183 | noIgnore?: boolean 184 | } = {}, 185 | src: string 186 | ) { 187 | super(name, status, options) 188 | this.src = src 189 | } 190 | 191 | async apply(): Promise { 192 | this.start() 193 | 194 | try { 195 | try { 196 | await execa('git', ['apply', '-R', ...PATCH_ARGS, this.src], { 197 | cwd: ENGINE_DIR, 198 | }) 199 | } catch (e) { 200 | null 201 | } 202 | 203 | const { stdout, exitCode } = await execa( 204 | 'git', 205 | ['apply', ...PATCH_ARGS, this.src], 206 | { cwd: ENGINE_DIR } 207 | ) 208 | 209 | if (exitCode != 0) throw stdout 210 | 211 | this.done = true 212 | } catch (e) { 213 | this.error = e 214 | this.done = false 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Init the logger before literally anything else to stop really obscure error 2 | // messages from occurring 3 | import Log from './log' 4 | export const log = new Log() 5 | 6 | import chalk from 'chalk' 7 | import commander, { Command } from 'commander' 8 | import { existsSync, readFileSync } from 'fs' 9 | import { resolve } from 'path' 10 | import { errorHandler, getConfig } from './utils' 11 | import { commands } from './cmds' 12 | import { ENGINE_DIR } from './constants' 13 | import { shaCheck } from './middleware/sha-check' 14 | import { updateCheck } from './middleware/update-check' 15 | import { registerCommand } from './middleware/registerCommand' 16 | 17 | // We have to use a dynamic require here, otherwise the typescript compiler 18 | // mucks up the directory structure 19 | // eslint-disable-next-line @typescript-eslint/no-var-requires 20 | const { version: melon } = require('../package.json') 21 | 22 | export const config = getConfig() 23 | 24 | const program = new Command() 25 | 26 | program.storeOptionsAsProperties(false).passCommandToAction(false) 27 | 28 | let reportedFFVersion 29 | 30 | if (existsSync(resolve(ENGINE_DIR, 'browser', 'config', 'version.txt'))) { 31 | const version = readFileSync( 32 | resolve(ENGINE_DIR, 'browser', 'config', 'version.txt'), 33 | 'utf-8' 34 | ).replace(/\n/g, '') 35 | 36 | if (version !== config.version.version) reportedFFVersion = version 37 | } 38 | 39 | export const bin_name = 'melon' 40 | 41 | program.version(` 42 | \t${chalk.bold(config.name)} ${config.version.displayVersion} 43 | \t${chalk.bold('Firefox')} ${config.version.version} ${ 44 | reportedFFVersion ? `(being reported as ${reportedFFVersion})` : `` 45 | } 46 | \t${chalk.bold('Melon')} ${melon} 47 | 48 | ${ 49 | reportedFFVersion 50 | ? `Mismatch detected between expected Firefox version and the actual version. 51 | You may have downloaded the source code using a different version and 52 | then switched to another branch.` 53 | : `` 54 | } 55 | `) 56 | program.name(bin_name) 57 | 58 | program.option( 59 | '-v, --verbose', 60 | 'Outputs extra debugging messages to the console' 61 | ) 62 | 63 | commands.forEach((command) => { 64 | if (command.flags) { 65 | if ( 66 | command.flags.platforms && 67 | !command.flags.platforms.includes(process.platform) 68 | ) { 69 | return 70 | } 71 | } 72 | 73 | const _cmd = commander.command(command.cmd) 74 | 75 | _cmd.description(command.description) 76 | 77 | command?.aliases?.forEach((alias) => { 78 | _cmd.alias(alias) 79 | }) 80 | 81 | command?.options?.forEach((opt) => { 82 | _cmd.option(opt.arg, opt.description) 83 | }) 84 | 85 | _cmd.action(async (...args: unknown[]) => { 86 | log.isDebug = program.opts().verbose 87 | 88 | registerCommand(command.cmd) 89 | 90 | await shaCheck(command.cmd) 91 | await updateCheck() 92 | 93 | command.controller(...args) 94 | }) 95 | 96 | program.addCommand(_cmd) 97 | }) 98 | 99 | process.on('uncaughtException', errorHandler) 100 | process.on('unhandledException', (err) => errorHandler(err, true)) 101 | 102 | program.parse(process.argv) 103 | -------------------------------------------------------------------------------- /src/interfaces/patch.ts: -------------------------------------------------------------------------------- 1 | export interface IPatch { 2 | name: string 3 | action: 'copy' | 'delete' 4 | src: string | string[] 5 | markers?: { 6 | [key: string]: [string, string] 7 | } 8 | indent?: number 9 | } 10 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | class Log { 4 | private startTime: number 5 | 6 | isDebug = false 7 | 8 | constructor() { 9 | const d = new Date() 10 | 11 | this.startTime = d.getTime() 12 | } 13 | 14 | getDiff(): string { 15 | const d = new Date() 16 | 17 | const currentTime = d.getTime() 18 | 19 | const elapsedTime = currentTime - this.startTime 20 | 21 | const secs = Math.floor((elapsedTime / 1000) % 60) 22 | const mins = Math.floor((elapsedTime / (60 * 1000)) % 60) 23 | const hours = Math.floor((elapsedTime / (60 * 60 * 1000)) % 24) 24 | 25 | const format = (r: number) => (r.toString().length == 1 ? `0${r}` : r) 26 | 27 | return `${format(hours)}:${format(mins)}:${format(secs)}` 28 | } 29 | 30 | debug(...args: unknown[]): void { 31 | if (this.isDebug) console.debug(...args) 32 | } 33 | 34 | info(...args: unknown[]): void { 35 | console.info(chalk.blueBright.bold(this.getDiff()), ...args) 36 | } 37 | 38 | warning(...args: unknown[]): void { 39 | console.warn(chalk.yellowBright.bold(' WARNING'), ...args) 40 | } 41 | 42 | hardWarning(...args: unknown[]): void { 43 | console.info('', chalk.bgRed.bold('WARNING'), ...args) 44 | } 45 | 46 | success(...args: unknown[]): void { 47 | console.log(`\n${chalk.greenBright.bold('SUCCESS')}`, ...args) 48 | } 49 | 50 | error(...args: unknown[]): void { 51 | if (args[0] instanceof Error) { 52 | throw args[0] 53 | } 54 | 55 | throw new Error( 56 | ...args.map((a) => 57 | typeof a !== 'undefined' ? (a as object).toString() : a 58 | ) 59 | ) 60 | } 61 | 62 | askForReport(): void { 63 | console.info( 64 | 'The following error is a bug. Please open an issue on the melon issue structure with a link to your repository and the output from this command.' 65 | ) 66 | console.info( 67 | 'The melon issue tracker is located at: https://github.com/dothq/melon/issues' 68 | ) 69 | } 70 | } 71 | 72 | export default Log 73 | -------------------------------------------------------------------------------- /src/manual-patches.ts: -------------------------------------------------------------------------------- 1 | import { sync } from 'glob' 2 | import { SRC_DIR } from './constants' 3 | import { IPatch } from './interfaces/patch' 4 | 5 | const files = sync('**/*', { 6 | nodir: true, 7 | cwd: SRC_DIR, 8 | }).filter( 9 | (f) => !(f.endsWith('.patch') || f.split('/').includes('node_modules')) 10 | ) 11 | 12 | const manualPatches: IPatch[] = [] 13 | 14 | files.map((i) => { 15 | const group = i.split('/')[0] 16 | 17 | if (!manualPatches.find((m) => m.name == group)) { 18 | manualPatches.push({ 19 | name: group, 20 | action: 'copy', 21 | src: files.filter((f) => f.split('/')[0] == group), 22 | }) 23 | } 24 | }) 25 | 26 | export default manualPatches 27 | -------------------------------------------------------------------------------- /src/middleware/patch-check.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Responsible for checking if all new patches have been applied 3 | */ 4 | 5 | import { existsSync, readFileSync, writeFileSync } from 'fs' 6 | import { resolve } from 'path' 7 | import { log } from '..' 8 | import { walkDirectory } from '../utils' 9 | 10 | export const patchCountFile = resolve(process.cwd(), '.dotbuild', 'patchCount') 11 | 12 | export const patchCheck = async (): Promise => { 13 | const fileList = await walkDirectory(resolve(process.cwd(), 'src')) 14 | const patchCount = fileList.length 15 | 16 | if (!existsSync(patchCountFile)) { 17 | writeFileSync(patchCountFile, '0') 18 | } 19 | 20 | const recordedPatchCount = Number(readFileSync(patchCountFile).toString()) 21 | 22 | if (patchCount !== recordedPatchCount) { 23 | log.hardWarning( 24 | 'You have not imported all of your patches. This may lead to unexpected behavior' 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/middleware/registerCommand.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'fs' 2 | import { resolve } from 'path' 3 | 4 | export function registerCommand(command: string): void { 5 | writeFileSync(resolve(process.cwd(), '.dotbuild', 'command'), command) 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware/sha-check.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { existsSync, readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { bin_name, log } from '..' 5 | 6 | const blacklistedCommands = ['reset', 'init', 'set-branch'] 7 | 8 | export const shaCheck = async (command: string): Promise => { 9 | if ( 10 | blacklistedCommands.filter((c) => command.startsWith(c)).length !== 0 || 11 | !existsSync(resolve(process.cwd(), '.dotbuild', 'metadata')) 12 | ) 13 | return 14 | 15 | const metadata = JSON.parse( 16 | readFileSync(resolve(process.cwd(), '.dotbuild', 'metadata'), 'utf-8') 17 | ) 18 | 19 | const { stdout: currentBranch } = await execa('git', [ 20 | 'branch', 21 | '--show-current', 22 | ]) 23 | 24 | if (metadata && metadata.branch) { 25 | if (metadata.branch !== currentBranch) { 26 | log.warning(`The current branch \`${currentBranch}\` differs from the original branch \`${metadata.branch}\`. 27 | 28 | \t If you are changing the Firefox version, you will need to reset the tree 29 | \t with |${bin_name} reset --hard| and then |${bin_name} download|. 30 | 31 | \t Or you can change the default branch by typing |${bin_name} set-branch |.`) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/middleware/update-check.ts: -------------------------------------------------------------------------------- 1 | import { config, log } from '..' 2 | import { getLatestFF } from '../utils' 3 | 4 | export const updateCheck = async (): Promise => { 5 | const firefoxVersion = config.version.version 6 | 7 | try { 8 | const version = await getLatestFF(config.version.product) 9 | 10 | if (firefoxVersion && version !== firefoxVersion) 11 | log.warning( 12 | `Latest version of Firefox (${version}) does not match frozen version (${firefoxVersion}).` 13 | ) 14 | } catch (e) { 15 | log.warning(`Failed to check for updates.`) 16 | log.askForReport() 17 | log.error(e) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Cmd { 2 | cmd: string 3 | description: string 4 | 5 | controller: (...args: any) => void 6 | 7 | options?: CmdOption[] 8 | aliases?: string[] 9 | flags?: { 10 | platforms?: CmdFlagPlatform[] 11 | } 12 | } 13 | 14 | export interface CmdOption { 15 | arg: string 16 | description: string 17 | } 18 | 19 | export type CmdFlagPlatform = NodeJS.Platform 20 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Responsible for loading, parsing and checking the config file for melon 3 | */ 4 | 5 | import { existsSync, readFileSync } from 'fs' 6 | import { join } from 'path' 7 | 8 | import { log } from '..' 9 | 10 | export const projectDir = process.cwd() 11 | export const configPath = join(projectDir, 'melon.json') 12 | 13 | let hasWarnedAboutConfig = false 14 | 15 | export enum SupportedProducts { 16 | Firefox = 'firefox', 17 | FirefoxESR = 'firefox-esr', 18 | FirefoxESRNext = 'firefox-esr-next', 19 | FirefoxDev = 'firefox-dev', 20 | FirefoxBeta = 'firefox-beta', 21 | FirefoxNightly = 'firefox-nightly', 22 | } 23 | 24 | export const validProducts = [ 25 | SupportedProducts.Firefox, 26 | SupportedProducts.FirefoxESR, 27 | SupportedProducts.FirefoxESRNext, 28 | SupportedProducts.FirefoxDev, 29 | SupportedProducts.FirefoxBeta, 30 | SupportedProducts.FirefoxNightly, 31 | ] 32 | 33 | export interface Config { 34 | /** 35 | * The name of the product to build 36 | */ 37 | name: string 38 | /** 39 | * The name of the company the build is for 40 | */ 41 | vendor: string 42 | /** 43 | * e.g. co.dothq.melon 44 | */ 45 | appId: string 46 | binaryName: string 47 | version: { 48 | /** 49 | * What branch of firefox you are forking. e.g. stable ('firefox'), dev ('firefox-dev') 50 | * , esr ('firefox-esr') etc. 51 | * 52 | * For use in code, use {@link SupportedProducts} 53 | */ 54 | product: SupportedProducts 55 | /** 56 | * The version of the selected product you are forking 57 | */ 58 | version?: string 59 | /** 60 | * The version of your output product. E.g. 1.3.5 61 | * This is in relation to the product you are building. For example, for 62 | * dothq, it might be Dot Browser 1.3.5 63 | */ 64 | displayVersion: string 65 | } 66 | buildOptions: { 67 | generateBranding: boolean 68 | windowsUseSymbolicLinks: boolean 69 | } 70 | addons: Record 71 | brands: Record< 72 | string, 73 | { 74 | backgroundColor: string 75 | brandShorterName: string 76 | brandShortName: string 77 | brandFullName: string 78 | } 79 | > 80 | } 81 | 82 | const defaultConfig: Config = { 83 | name: 'Unknown melon build', 84 | vendor: 'Unknown', 85 | appId: 'unknown.appid', 86 | binaryName: 'firefox', 87 | version: { 88 | product: SupportedProducts.Firefox, 89 | displayVersion: '1.0.0', 90 | }, 91 | buildOptions: { 92 | generateBranding: true, 93 | windowsUseSymbolicLinks: false, 94 | }, 95 | addons: {}, 96 | brands: {}, 97 | } 98 | 99 | export const defaultBrandsConfig = { 100 | backgroundColor: '#2B2A33', 101 | brandShorterName: 'Nightly', 102 | brandShortName: 'Nightly', 103 | brandFullName: 'Nightly', 104 | } 105 | 106 | export function hasConfig(): boolean { 107 | return existsSync(configPath) 108 | } 109 | 110 | export function getConfig(): Config { 111 | const configExists = hasConfig() 112 | 113 | let fileContents = '{}' 114 | let fileParsed: Config 115 | 116 | if (!configExists) { 117 | if (!hasWarnedAboutConfig) { 118 | log.warning( 119 | `Config file not found at ${configPath}. It is recommended to create one by running |melon setup-project|` 120 | ) 121 | hasWarnedAboutConfig = true 122 | } 123 | } else { 124 | fileContents = readFileSync(configPath).toString() 125 | } 126 | 127 | try { 128 | // Try to parse the contents of the file. May not be valid JSON 129 | fileParsed = JSON.parse(fileContents) 130 | } catch (e) { 131 | // Report the error to the user 132 | log.error(`Error parsing melon config file located at ${configPath}`) 133 | log.error(e) 134 | process.exit(1) 135 | } 136 | 137 | // Merge the default config with the file parsed config 138 | fileParsed = { ...defaultConfig, ...fileParsed } 139 | 140 | // =========================================================================== 141 | // Config Validation 142 | 143 | if (!validProducts.includes(fileParsed.version.product)) { 144 | log.error(`${fileParsed.version.product} is not a valid product`) 145 | process.exit(1) 146 | } 147 | 148 | return fileParsed 149 | } 150 | -------------------------------------------------------------------------------- /src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export const delay = (delay: number): Promise => 2 | new Promise((resolve) => { 3 | setTimeout(() => resolve(true), delay) 4 | }) 5 | -------------------------------------------------------------------------------- /src/utils/dispatch.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { log } from '..' 3 | 4 | export const dispatch = ( 5 | cmd: string, 6 | args?: string[], 7 | cwd?: string, 8 | killOnError?: boolean, 9 | logger = (data: string) => log.info(data) 10 | ): Promise => { 11 | const handle = (data: string | Error, killOnError?: boolean) => { 12 | const d = data.toString() 13 | 14 | d.split('\n').forEach((line: string) => { 15 | if (line.length !== 0) logger(line.replace(/\s\d{1,5}:\d\d\.\d\d /g, '')) 16 | }) 17 | 18 | if (killOnError) { 19 | log.error('Command failed. See error above.') 20 | process.exit(1) 21 | } 22 | } 23 | 24 | return new Promise((resolve) => { 25 | process.env.MACH_USE_SYSTEM_PYTHON = 'true' 26 | 27 | const proc = execa(cmd, args, { 28 | cwd: cwd || process.cwd(), 29 | env: process.env, 30 | }) 31 | 32 | proc.stdout?.on('data', (d) => handle(d)) 33 | proc.stderr?.on('data', (d) => handle(d)) 34 | 35 | proc.stdout?.on('error', (d) => handle(d, killOnError)) 36 | proc.stderr?.on('error', (d) => handle(d, killOnError)) 37 | 38 | proc.on('exit', () => { 39 | resolve(true) 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream } from 'fs' 2 | 3 | import axios from 'axios' 4 | import cliProgress from 'cli-progress' 5 | import { Duplex } from 'stream' 6 | 7 | export async function downloadFileToLocation( 8 | url: string, 9 | writeOutPath: string, 10 | consoleWriter?: (message: string) => void 11 | ): Promise { 12 | return new Promise((resolve, reject) => 13 | (async () => { 14 | const { data, headers } = await axios.get(url, { 15 | responseType: 'stream', 16 | }) 17 | 18 | const length = headers['content-length'] 19 | 20 | const writer = createWriteStream(writeOutPath) 21 | 22 | let receivedBytes = 0 23 | 24 | const progressBar = new cliProgress.SingleBar({ 25 | stream: consoleWriter 26 | ? new Duplex({ 27 | write: (chunk, enconding, next) => { 28 | consoleWriter(chunk.toString()) 29 | next() 30 | }, 31 | read: (size) => { 32 | /* Empty output */ 33 | }, 34 | }) 35 | : process.stdout, 36 | }) 37 | progressBar.start(length, receivedBytes) 38 | 39 | data.on('data', (chunk: { length: number }) => { 40 | receivedBytes += chunk.length 41 | }) 42 | data.pipe(writer) 43 | data.on('error', (err: unknown) => reject(err)) 44 | 45 | const progressInterval = setInterval( 46 | () => progressBar.update(receivedBytes), 47 | 500 48 | ) 49 | 50 | data.on('end', () => { 51 | clearInterval(progressInterval) 52 | progressBar.stop() 53 | resolve() 54 | }) 55 | })() 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/error-handler.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { log } from '..' 5 | 6 | export const errorHandler = (err: Error, isUnhandledRej: boolean): void => { 7 | let cc = readFileSync(resolve(process.cwd(), '.dotbuild', 'command'), 'utf-8') 8 | cc = cc.replace(/(\r\n|\n|\r)/gm, '') 9 | 10 | console.log( 11 | `\n ${chalk.redBright.bold( 12 | 'ERROR' 13 | )} An error occurred while running command ["${cc 14 | .split(' ') 15 | .join('", "')}"]:` 16 | ) 17 | console.log( 18 | `\n\t`, 19 | isUnhandledRej 20 | ? err.toString().replace(/\n/g, '\n\t ') 21 | : err.message.replace(/\n/g, '\n\t ') 22 | ) 23 | if (err.stack || isUnhandledRej) { 24 | const stack: string[] | undefined = err.stack?.split('\n') 25 | 26 | if (!stack) return 27 | 28 | stack.shift() 29 | stack.shift() 30 | console.log( 31 | `\t`, 32 | stack 33 | .join('\n') 34 | .replace(/(\r\n|\n|\r)/gm, '') 35 | .replace(/ {4}at /g, '\n\t • ') 36 | ) 37 | } 38 | 39 | console.log() 40 | log.info('Exiting due to error.') 41 | process.exit(1) 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { closeSync, existsSync, mkdirSync, openSync, writeSync } from 'fs' 2 | import { mkdir, readdir, stat, symlink } from 'fs/promises' 3 | import { join, isAbsolute, dirname, relative } from 'path' 4 | 5 | import { log } from '..' 6 | 7 | export async function walkDirectory(dirName: string): Promise { 8 | const output = [] 9 | 10 | if (!isAbsolute(dirName)) { 11 | log.askForReport() 12 | log.error('Please provide an absolute input to walkDirectory') 13 | } 14 | 15 | try { 16 | const directoryContents = await readdir(dirName) 17 | 18 | for (const file of directoryContents) { 19 | const fullPath = join(dirName, file) 20 | const fStat = await stat(fullPath) 21 | 22 | if (fStat.isDirectory()) { 23 | for (const newFile of await walkDirectory(fullPath)) { 24 | output.push(newFile) 25 | } 26 | } else { 27 | output.push(fullPath) 28 | } 29 | } 30 | } catch (e) { 31 | log.askForReport() 32 | log.error(e) 33 | } 34 | 35 | return output 36 | } 37 | 38 | export type TreeType = Record 39 | 40 | export async function walkDirectoryTree( 41 | dirName: string 42 | ): Promise>>>>>> { 43 | const output: TreeType = {} 44 | 45 | if (!isAbsolute(dirName)) { 46 | log.askForReport() 47 | log.error('Please provide an absolute input to walkDirectory') 48 | } 49 | 50 | try { 51 | const directoryContents = await readdir(dirName) 52 | 53 | const currentOut = [] 54 | 55 | for (const file of directoryContents) { 56 | const fullPath = join(dirName, file) 57 | const fStat = await stat(fullPath) 58 | 59 | if (fStat.isDirectory()) { 60 | output[file] = await walkDirectoryTree(fullPath) 61 | } else { 62 | currentOut.push(fullPath) 63 | } 64 | } 65 | 66 | output['.'] = currentOut 67 | } catch (e) { 68 | log.askForReport() 69 | log.error(e) 70 | } 71 | 72 | return output 73 | } 74 | 75 | export async function ensureDir(dirName: string): Promise { 76 | if (!existsSync(dirName)) { 77 | await mkdirp(dirName) 78 | } 79 | } 80 | 81 | export function mkdirp(dirName: string): Promise { 82 | return mkdir(dirName, { recursive: true }) 83 | } 84 | 85 | export function mkdirpSync(dirName: string): string | undefined { 86 | return mkdirSync(dirName, { recursive: true }) 87 | } 88 | 89 | export function appendToFileSync(fileName: string, content: string): void { 90 | const file = openSync(fileName, 'a') 91 | writeSync(file, content) 92 | closeSync(file) 93 | } 94 | 95 | export async function createSymlink( 96 | srcPath: string, 97 | destPath: string, 98 | type?: string 99 | ): Promise { 100 | if (existsSync(destPath)) return 101 | 102 | const { toDest: src } = symlinkPaths(srcPath, destPath) 103 | 104 | const dir = dirname(destPath) 105 | const exists = existsSync(dir) 106 | if (exists) return await symlink(src, destPath, type) 107 | await mkdirp(dir) 108 | return await symlink(src, destPath, type) 109 | } 110 | 111 | /** 112 | * Adapted from fs-extra 113 | * @param srcPath 114 | * @param destPath 115 | * @returns 116 | */ 117 | export function symlinkPaths( 118 | srcPath: string, 119 | destPath: string 120 | ): { toCwd: string; toDest: string } { 121 | if (isAbsolute(srcPath)) { 122 | if (!existsSync(srcPath)) throw new Error('absolute srcpath does not exist') 123 | 124 | return { 125 | toCwd: srcPath, 126 | toDest: srcPath, 127 | } 128 | } else { 129 | const dstdir = dirname(destPath) 130 | const relativeToDst = join(dstdir, srcPath) 131 | if (existsSync(relativeToDst)) 132 | return { 133 | toCwd: relativeToDst, 134 | toDest: srcPath, 135 | } 136 | else { 137 | if (!existsSync(srcPath)) 138 | throw new Error('relative srcpath does not exist') 139 | return { 140 | toCwd: srcPath, 141 | toDest: relative(dstdir, srcPath), 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/utils/import.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import { lstatSync, readFileSync } from 'fs' 3 | import { ensureSymlink } from 'fs-extra' 4 | import { copyFile } from 'fs/promises' 5 | import { resolve } from 'path' 6 | import rimraf from 'rimraf' 7 | import { appendToFileSync } from '.' 8 | import { config } from '..' 9 | import { ENGINE_DIR, SRC_DIR } from '../constants' 10 | 11 | const getChunked = (location: string) => location.replace(/\\/g, '/').split('/') 12 | 13 | export const copyManual = async ( 14 | name: string, 15 | noIgnore?: boolean 16 | ): Promise => { 17 | // If the file exists and is not a symlink, we want to replace it with a 18 | // symlink to our file, so remove it 19 | if ( 20 | existsSync(resolve(ENGINE_DIR, ...getChunked(name))) && 21 | !lstatSync(resolve(ENGINE_DIR, ...getChunked(name))).isSymbolicLink() 22 | ) { 23 | rimraf.sync(resolve(ENGINE_DIR, ...getChunked(name))) 24 | } 25 | 26 | if ( 27 | process.platform == 'win32' && 28 | !config.buildOptions.windowsUseSymbolicLinks 29 | ) { 30 | // By default, windows users do not have access to the permissions to create 31 | // symbolic links. As a work around, we will just copy the files instead 32 | await copyFile( 33 | resolve(SRC_DIR, ...getChunked(name)), 34 | resolve(ENGINE_DIR, ...getChunked(name)) 35 | ) 36 | } else { 37 | // Create the symlink 38 | await ensureSymlink( 39 | resolve(SRC_DIR, ...getChunked(name)), 40 | resolve(ENGINE_DIR, ...getChunked(name)) 41 | ) 42 | } 43 | 44 | if (!noIgnore) { 45 | const gitignore = readFileSync(resolve(ENGINE_DIR, '.gitignore'), 'utf-8') 46 | 47 | if (!gitignore.includes(getChunked(name).join('/'))) 48 | appendToFileSync( 49 | resolve(ENGINE_DIR, '.gitignore'), 50 | `\n${getChunked(name).join('/')}` 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './delay' 2 | export * from './dispatch' 3 | export * from './error-handler' 4 | export * from './import' 5 | export * from './version' 6 | export * from './write-metadata' 7 | export * from './config' 8 | export * from './stringTemplate' 9 | export * from './fs' 10 | -------------------------------------------------------------------------------- /src/utils/stringTemplate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows for the usage of template strings inside from a file 3 | */ 4 | export function stringTemplate( 5 | template: string, 6 | variables: { [key: string]: string | number } 7 | ): string { 8 | let temp = template 9 | 10 | for (const variable in variables) { 11 | // Replace only replaces the first instance of a string. We want to 12 | // replace all instances 13 | while (temp.includes(`\${${variable}}`)) { 14 | temp = temp.replace(`\${${variable}}`, variables[variable].toString()) 15 | } 16 | } 17 | 18 | return temp 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/version.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { SupportedProducts } from './config' 3 | 4 | const firefoxTargets = JSON.parse(`{ 5 | "${SupportedProducts.Firefox}": "LATEST_FIREFOX_VERSION", 6 | "${SupportedProducts.FirefoxBeta}": "LATEST_FIREFOX_DEVEL_VERSION", 7 | "${SupportedProducts.FirefoxDev}": "FIREFOX_DEVEDITION", 8 | "${SupportedProducts.FirefoxESR}": "FIREFOX_ESR", 9 | "${SupportedProducts.FirefoxESRNext}": "FIREFOX_ESR_NEXT", 10 | "${SupportedProducts.FirefoxNightly}": "FIREFOX_NIGHTLY" 11 | }`) 12 | 13 | export const getLatestFF = async ( 14 | product: SupportedProducts = SupportedProducts.Firefox 15 | ): Promise => { 16 | const { data } = await axios.get( 17 | 'https://product-details.mozilla.org/1.0/firefox_versions.json' 18 | ) 19 | 20 | return data[firefoxTargets[product]] 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/write-metadata.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa' 2 | import { writeFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | import { config } from '..' 5 | 6 | export const writeMetadata = async (): Promise => { 7 | const { stdout: sha } = await execa('git', ['rev-parse', 'HEAD']) 8 | const { stdout: branch } = await execa('git', ['branch', '--show-current']) 9 | 10 | writeFileSync( 11 | resolve(process.cwd(), '.dotbuild', 'metadata'), 12 | JSON.stringify({ 13 | sha, 14 | branch, 15 | birth: Date.now(), 16 | versions: config.version, 17 | }) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /template/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.jsm": "javascript", 4 | "moz.build": "python", 5 | "moz.configure": "python", 6 | "app.mozbuild": "python", 7 | "Makefile.in": "makefile", 8 | "mozconfig": "shellscript" 9 | }, 10 | "files.watcherExclude": { 11 | "**/.git/objects/**": true, 12 | "**/.git/subtree-cache/**": true, 13 | "**/node_modules/*/**": true, 14 | "*.tar.xz": true 15 | }, 16 | "javascript.updateImportsOnFileMove.enabled": "always", 17 | "files.exclude": { 18 | "**/node_modules": true, 19 | "**/.dotbuild": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /template/branding.optional/brand.dtd: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /template/branding.optional/brand.ftl: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | ## Firefox and Mozilla Brand 6 | ## 7 | ## Firefox and Mozilla must be treated as a brand. 8 | ## 9 | ## They cannot be: 10 | ## - Transliterated. 11 | ## - Translated. 12 | ## 13 | ## Declension should be avoided where possible, leaving the original 14 | ## brand unaltered in prominent UI positions. 15 | ## 16 | ## For further details, consult: 17 | ## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark 18 | 19 | -brand-shorter-name = ${brandShorterName} 20 | -brand-short-name = ${brandShortName} 21 | -brand-full-name = ${brandFullName} 22 | # This brand name can be used in messages where the product name needs to 23 | # remain unchanged across different versions (Nightly, Beta, etc.). 24 | -brand-product-name = ${brandingGenericName} 25 | -vendor-short-name = ${brandingVendor} 26 | trademarkInfo = { " " } 27 | -------------------------------------------------------------------------------- /template/branding.optional/brand.properties: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | brandShorterName=${brandShorterName} 6 | brandShortName=${brandShortName} 7 | brandFullName=${brandFullName} 8 | vendorShortName=${brandingVendor} 9 | -------------------------------------------------------------------------------- /template/configs/common/mozconfig: -------------------------------------------------------------------------------- 1 | # Browser branding 2 | ac_add_options --enable-update-channel=release 3 | 4 | ac_add_options --with-branding=${brandingDir} 5 | ac_add_options --with-app-name=${binName} 6 | export MOZ_USER_DIR="${name}" 7 | export MOZ_APP_VENDOR="${vendor}" 8 | export MOZ_APP_BASENAME=Dot 9 | export MOZ_APP_PROFILE=${binName} 10 | export MOZ_APP_DISPLAYNAME="${name}" 11 | export MOZ_BRANDING_DIRECTORY=${brandingDir} 12 | export MOZ_OFFICIAL_BRANDING_DIRECTORY=${brandingDir} 13 | export MOZ_MACBUNDLE_ID=${appId} 14 | export MOZ_DISTRIBUTION_ID=${appId} 15 | 16 | # Uncomment if builds are too resource hungry 17 | # mk_add_options MOZ_MAKE_FLAGS="-j4" 18 | # ac_add_options --enable-linker=gold 19 | 20 | # Misc 21 | export MOZ_STUB_INSTALLER=1 22 | export MOZ_INCLUDE_SOURCE_INFO=1 23 | export MOZ_SOURCE_REPO=https://github.com/dothq/browser-desktop 24 | export MOZ_SOURCE_CHANGESET=${changeset} 25 | -------------------------------------------------------------------------------- /template/configs/linux/build_linux.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "Building Dot Browser for Linux" 4 | echo "" 5 | echo "This will take 10 to 60 minutes to complete." 6 | echo "" 7 | 8 | echo "━━━━━━━━━ Setting up roles... ━━━━━━━━━" 9 | echo "" 10 | 11 | sudo usermod -aG wheel worker 12 | sudo chown -R worker:worker /worker 13 | 14 | echo "" 15 | echo "━━━━━━━━━ Setting up Rust... ━━━━━━━━━" 16 | echo "" 17 | 18 | rustup install stable 19 | rustup default stable 20 | 21 | echo "" 22 | echo "━━━━━━━━━ Installing dependencies... ━━━━━━━━━" 23 | echo "" 24 | 25 | sudo pacman -Syu --noconfirm 26 | 27 | echo "" 28 | echo "━━━━━━━━━ Bootstrapping... ━━━━━━━━━" 29 | echo "" 30 | 31 | ./mach bootstrap --application-choice browser --no-interactive 32 | 33 | echo "" 34 | echo "━━━━━━━━━ Building... ━━━━━━━━━" 35 | echo "" 36 | 37 | MOZCONFIG=/worker/build/mozconfig ./mach build 38 | 39 | echo "" 40 | echo "━━━━━━━━━ Packaging... ━━━━━━━━━" 41 | echo "" 42 | 43 | ./mach package 44 | 45 | echo "" 46 | echo "Dot Browser was successfully built!" 47 | echo "To take your build for a test drive, run: |melon run|" 48 | echo "" -------------------------------------------------------------------------------- /template/configs/linux/linux.dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux:latest 2 | 3 | # Setup environment variables 4 | ENV SHELL=/bin/sh 5 | ENV MACH_USE_SYSTEM_PYTHON=true 6 | ENV BUILD_SCRIPT=/worker/configs/linux/build_linux.sh 7 | 8 | # Mount working directory 9 | RUN mkdir /worker 10 | WORKDIR /worker/build 11 | VOLUME /worker/build 12 | 13 | # Remove password prompt for worker 14 | RUN useradd -m worker 15 | RUN usermod --append --groups wheel worker 16 | RUN echo 'worker ALL=(ALL) NOPASSWD: ALL' >> \ 17 | /etc/sudoers 18 | 19 | # Install dependencies 20 | RUN pacman -Syu --noconfirm 21 | RUN pacman -S --noconfirm base-devel git mercurial python2 python3 make wget tar zip yasm libpulse rustup python-pip 22 | RUN rustup install stable && rustup default stable 23 | RUN cargo install cbindgen 24 | 25 | # Switch to worker user for build 26 | USER worker 27 | 28 | CMD sudo chmod +x $BUILD_SCRIPT && $BUILD_SCRIPT -------------------------------------------------------------------------------- /template/configs/linux/mozconfig: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | 11 | # Disable telemetry and tracking 12 | mk_add_options MOZ_TELEMETRY_REPORTING= 13 | mk_add_options MOZ_DATA_REPORTING= 14 | -------------------------------------------------------------------------------- /template/configs/linux/mozconfig-i686: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | 11 | # Disable telemetry and tracking 12 | mk_add_options MOZ_TELEMETRY_REPORTING= 13 | mk_add_options MOZ_DATA_REPORTING= 14 | 15 | # Support 32-bit builds 16 | ac_add_options --target=i686 17 | -------------------------------------------------------------------------------- /template/configs/macos/build_macos.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "Building Dot Browser for macOS" 4 | echo "" 5 | echo "This will take 10 to 60 minutes to complete." 6 | echo "" 7 | 8 | echo "━━━━━━━━━ Setting up roles... ━━━━━━━━━" 9 | echo "" 10 | 11 | sudo usermod -aG wheel worker 12 | sudo chown -R worker:worker /worker 13 | 14 | echo "━━━━━━━━━ Setting up Rust... ━━━━━━━━━" 15 | echo "" 16 | 17 | rustup install stable 18 | rustup default stable 19 | 20 | echo "━━━━━━━━━ Installing dependencies... ━━━━━━━━━" 21 | echo "" 22 | 23 | sudo pacman -Syu --noconfirm 24 | 25 | echo "━━━━━━━━━ Bootstrapping... ━━━━━━━━━" 26 | echo "" 27 | 28 | ./mach bootstrap --application-choice browser --no-interactive 29 | 30 | echo "━━━━━━━━━ Building... ━━━━━━━━━" 31 | echo "" 32 | 33 | MOZCONFIG=/worker/build/mozconfig ./mach build 34 | 35 | echo "━━━━━━━━━ Packaging... ━━━━━━━━━" 36 | echo "" 37 | 38 | ./mach package 39 | 40 | echo "" 41 | echo "Dot Browser was successfully built!" 42 | echo "To take your build for a test drive, run: |melon run|" 43 | echo "" -------------------------------------------------------------------------------- /template/configs/macos/macos.dockerfile: -------------------------------------------------------------------------------- 1 | FROM sickcodes/docker-osx:latest 2 | 3 | # Setup environment variables 4 | ENV SHELL=/bin/sh 5 | ENV MACH_USE_SYSTEM_PYTHON=true 6 | ENV BUILD_SCRIPT=/worker/configs/macos/build_macos.sh 7 | 8 | # Mount working directory 9 | RUN mkdir /worker 10 | WORKDIR /worker/build 11 | VOLUME /worker/build 12 | 13 | # Make build script executable 14 | RUN chmod +x $BUILD_SCRIPT 15 | 16 | # Switch to worker user for build 17 | USER worker 18 | 19 | CMD $BUILD_SCRIPT -------------------------------------------------------------------------------- /template/configs/macos/mozconfig: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | mk_add_options MOZ_MAKE_FLAGS="-j4" 11 | 12 | # Disable telemetry and tracking 13 | mk_add_options MOZ_TELEMETRY_REPORTING= 14 | mk_add_options MOZ_DATA_REPORTING= 15 | -------------------------------------------------------------------------------- /template/configs/macos/mozconfig-i686: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | mk_add_options MOZ_MAKE_FLAGS="-j4" 11 | 12 | # Disable telemetry and tracking 13 | mk_add_options MOZ_TELEMETRY_REPORTING= 14 | mk_add_options MOZ_DATA_REPORTING= 15 | 16 | # Support 32-bit builds 17 | ac_add_options --target=i686 18 | -------------------------------------------------------------------------------- /template/configs/windows/mozconfig: -------------------------------------------------------------------------------- 1 | ac_add_options --target=x86_64-pc-mingw32 2 | ac_add_options --enable-js-shell 3 | ac_add_options --enable-rust-simd 4 | ac_add_options --enable-crashreporter 5 | 6 | # Disable telemetry and tracking 7 | mk_add_options MOZ_TELEMETRY_REPORTING= 8 | mk_add_options MOZ_DATA_REPORTING= -------------------------------------------------------------------------------- /template/configs/windows/mozconfig-i686: -------------------------------------------------------------------------------- 1 | # Optimise builds 2 | ac_add_options --enable-application=browser 3 | ac_add_options --enable-hardening 4 | ac_add_options --enable-rust-simd 5 | ac_add_options --enable-release 6 | ac_add_options --enable-optimize 7 | ac_add_options --with-ccache=sccache 8 | ac_add_options --disable-debug 9 | ac_add_options --enable-updater 10 | 11 | # Disable telemetry and tracking 12 | mk_add_options MOZ_TELEMETRY_REPORTING= 13 | mk_add_options MOZ_DATA_REPORTING= 14 | 15 | # Support 32-bit builds 16 | ac_add_options --target=i686 17 | -------------------------------------------------------------------------------- /template/src/README.md: -------------------------------------------------------------------------------- 1 | # Melon build tool 2 | -------------------------------------------------------------------------------- /template/src/browser/confvars-sh.patch.optional: -------------------------------------------------------------------------------- 1 | diff --git a/browser/confvars.sh b/browser/confvars.sh 2 | index 92871c9516f98e065c5240a4a1cc7ead1de8ef9d..f9c515c48b67ee581e1280d587de5cdcf380002f 100755 3 | --- a/browser/confvars.sh 4 | +++ b/browser/confvars.sh 5 | @@ -26,7 +26,7 @@ if test "$OS_ARCH" = "WINNT"; then 6 | fi 7 | fi 8 | 9 | -BROWSER_CHROME_URL=chrome://browser/content/browser.xhtml 10 | +BROWSER_CHROME_URL=chrome://customui/content/browser.html 11 | 12 | # MOZ_APP_DISPLAYNAME will be set by branding/configure.sh 13 | # MOZ_BRANDING_DIRECTORY is the default branding directory used when none is 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/linux/linux.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/template/src/browser/themes.optional/custom/linux/linux.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/macos/macos.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/template/src/browser/themes.optional/custom/macos/macos.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/shared/shared.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/template/src/browser/themes.optional/custom/shared/shared.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/custom/windows/windows.inc.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dothq/melon/775fa2cfe834ef437dc110bcc6b7a235dfe2f572/template/src/browser/themes.optional/custom/windows/windows.inc.css -------------------------------------------------------------------------------- /template/src/browser/themes.optional/linux/browser-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css 2 | index ac03145433960f8abcfad5f285545843f02e7f1e..108dc7b290206be7158e914f2760226cb2c219ea 100644 3 | --- a/browser/themes/linux/browser.css 4 | +++ b/browser/themes/linux/browser.css 5 | @@ -6,6 +6,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/linux/linux.inc.css 10 | + 11 | %include ../shared/browser.inc.css 12 | /** 13 | * We intentionally do not include browser-custom-colors.inc.css, 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/osx/browser-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css 2 | index 01036f1e2e08f2a033c439ed6796668704e39b02..0e07dc2ae161c0dca88c634577c4788755bf2324 100644 3 | --- a/browser/themes/osx/browser.css 4 | +++ b/browser/themes/osx/browser.css 5 | @@ -4,6 +4,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/macos/macos.inc.css 10 | + 11 | %include ../shared/browser.inc.css 12 | %include ../shared/browser-custom-colors.inc.css 13 | 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/shared/browser-inc-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/shared/browser.inc.css b/browser/themes/shared/browser.inc.css 2 | index b345560225ee8619524481c4570540746be9fcba..cafddff0e4febe056fceb35d705f3ef43cc08b1f 100644 3 | --- a/browser/themes/shared/browser.inc.css 4 | +++ b/browser/themes/shared/browser.inc.css 5 | @@ -2,6 +2,8 @@ 6 | * License, v. 2.0. If a copy of the MPL was not distributed with this 7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 8 | 9 | +%include ../custom/shared/shared.inc.css 10 | + 11 | %include downloads/indicator.inc.css 12 | %include addons/extension-controlled.inc.css 13 | 14 | -------------------------------------------------------------------------------- /template/src/browser/themes.optional/windows/browser-css.patch: -------------------------------------------------------------------------------- 1 | diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css 2 | index 2cb1a094bc42d045992bf42ccd4c79e89795d971..c2fab339d4ddb7dd394d73f686c1b6e569d2a1be 100644 3 | --- a/browser/themes/windows/browser.css 4 | +++ b/browser/themes/windows/browser.css 5 | @@ -4,6 +4,8 @@ 6 | 7 | @namespace html url("http://www.w3.org/1999/xhtml"); 8 | 9 | +%include ../custom/windows/windows.inc.css 10 | + 11 | %include ../shared/browser.inc.css 12 | %include ../shared/browser-custom-colors.inc.css 13 | %filter substitution 14 | -------------------------------------------------------------------------------- /template/src/customui.optional/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to the browser 8 | 9 | 10 | 11 | 12 |
    13 |
    14 | 15 |
    16 |
    17 |

    Good, you are setup!

    18 |

    You chose the option to customize the html of the browser. This means you inherit none of the UI or logic firefox has in relation to tabs, etc. I wish you the best of luck!

    19 | 20 | 21 |
    22 |
    23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /template/src/customui.optional/css/browser.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #333333; 3 | color: white; 4 | font-family: sans-serif; 5 | height: 100vh; 6 | margin: 0; 7 | } 8 | 9 | #grid { 10 | display: grid; 11 | grid-template-columns: 1fr 1fr; 12 | height: 100%; 13 | align-content: center; 14 | } 15 | -------------------------------------------------------------------------------- /template/src/customui.optional/jar.mn: -------------------------------------------------------------------------------- 1 | browser.jar: 2 | % content customui %content/customui/ contentaccessible=yes 3 | content/customui/browser.html (browser.html) 4 | content/customui/css/ (css/**/**) 5 | content/customui/scripts/ (scripts/**/**) -------------------------------------------------------------------------------- /template/src/customui.optional/moz.build: -------------------------------------------------------------------------------- 1 | JAR_MANIFESTS += ["jar.mn"] -------------------------------------------------------------------------------- /template/src/customui.optional/scripts/browser.js: -------------------------------------------------------------------------------- 1 | import { launchDevTools } from './devtools.js' 2 | 3 | document.getElementById('launchDevTools').addEventListener('click', launchDevTools) 4 | -------------------------------------------------------------------------------- /template/src/customui.optional/scripts/devtools.js: -------------------------------------------------------------------------------- 1 | const launcher = ChromeUtils.import( 2 | "resource://devtools/client/framework/browser-toolbox/Launcher.jsm" 3 | ).BrowserToolboxLauncher 4 | 5 | export function launchDevTools() { 6 | launcher.init() 7 | } 8 | -------------------------------------------------------------------------------- /template/src/toolkit/toolkit-mozbuild.patch.optional: -------------------------------------------------------------------------------- 1 | diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild 2 | index fd9903bac5b07c655ee77c94f8f795b6773676ad..3127dc35fd8793b91ddd437f30b1917f6eff29ce 100644 3 | --- a/toolkit/toolkit.mozbuild 4 | +++ b/toolkit/toolkit.mozbuild 5 | @@ -202,3 +202,6 @@ if CONFIG['ENABLE_TESTS']: 6 | 7 | if CONFIG['FUZZING']: 8 | DIRS += ['/tools/fuzzing'] 9 | + 10 | +# Custom UI toolkit 11 | +DIRS += ['/customui'] 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./dist" /* Redirect output structure to the directory. */, 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */, 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 50 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 51 | /* Source Map Options */ 52 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 53 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 54 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 55 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | /* Advanced Options */ 60 | "skipLibCheck": true /* Skip type checking of declaration files. */, 61 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 62 | 63 | "resolveJsonModule": true 64 | }, 65 | "exclude": [ 66 | "node_modules/**/*", 67 | "**/firefox-*/**/*", 68 | "gecko", 69 | "**/engine/**/*" 70 | ] 71 | } 72 | --------------------------------------------------------------------------------