├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── bin └── protonPack ├── circle.yml ├── cli ├── compile.js ├── config.js ├── configApp.js ├── helpers │ ├── cli.js │ ├── file.js │ └── log.js ├── initProtonApp.js └── server.js ├── package-lock.json ├── package.json ├── template ├── .editorconfig ├── .eslintrc.json ├── .prettierrc ├── Readme.md ├── _gitignore ├── assets │ ├── logoConfig.js │ └── protonmail.svg ├── auth │ ├── .htaccess │ ├── app.ejs │ └── app │ │ ├── App.tsx │ │ ├── PrivateApp.tsx │ │ ├── PublicApp.tsx │ │ ├── app.scss │ │ ├── components │ │ └── layout │ │ │ ├── PrivateHeader.tsx │ │ │ ├── PrivateLayout.tsx │ │ │ └── PublicLayout.tsx │ │ ├── containers │ │ ├── About.tsx │ │ └── Home.tsx │ │ └── index.tsx ├── circle.yml ├── default │ ├── app.ejs │ └── app │ │ ├── App.js │ │ ├── app.scss │ │ └── index.js ├── package.json ├── po │ └── lang.json └── tsconfig.json ├── webpack.config.js └── webpack ├── alias.js ├── assets.loader.js ├── constants.js ├── css.loader.js ├── helpers ├── babel.js ├── files.js ├── openpgp.js ├── regex.js └── source.js ├── js.loader.js ├── optimization.js ├── paths.js ├── plugins.js └── sri-strip-plugin └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "commonjs": true, 8 | "jasmine": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "isNaN": true, 13 | "require": true, 14 | "angular": true, 15 | "noUiSlider": true, 16 | "Awesomplete": true, 17 | "Brick": true, 18 | "MailParser": true, 19 | "moment": true, 20 | "openpgp": true, 21 | "jQuery": true, 22 | "Mousetrap": true, 23 | "_rAF": true, 24 | "base32": true, 25 | "$": true, 26 | "Cypress": true, 27 | "cy": true, 28 | "after": true, 29 | "before": true 30 | }, 31 | "rules": { 32 | "object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": true }], 33 | "arrow-parens": ["error", "always"], 34 | "comma-dangle": ["error", "never"], 35 | "no-shadow": ["off", { "hoist": "never", "builtinGlobals": true }], 36 | "array-bracket-spacing": ["off", "never"], 37 | "object-property-newline": "off", 38 | "no-sequences": "off", 39 | "no-param-reassign": ["error", { "props": false }], 40 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 41 | "padded-blocks": ["off", "always"], 42 | "arrow-body-style": ["off", "as-needed"], 43 | "no-use-before-define": ["error", { "functions": false, "classes": true }], 44 | "new-cap": ["error", { "properties": true, "capIsNewExceptionPattern": "^Awesomplete.", "newIsCapExceptions": ["vCard"] }], 45 | "no-mixed-operators": ["error", {"groups": [["&", "|", "^", "~", "<<", ">>", ">>>"], ["&&", "||"]]}], 46 | "no-return-assign": "off", 47 | "max-len": ["error", { "ignoreComments": true, "code": 120, "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true }], 48 | "consistent-return": "off", 49 | "default-case": "off", 50 | "no-plusplus": "off", 51 | "no-bitwise": "off", 52 | "no-debugger": "off", 53 | "prefer-template": "off", 54 | "class-methods-use-this": "off", 55 | "func-names": ["off", "never"], 56 | "prefer-destructuring": "off", 57 | "function-paren-newline": "off", 58 | "prefer-promise-reject-errors": "off", 59 | "import/prefer-default-export": "off", 60 | "no-console": "off", 61 | "object-curly-newline": "off", 62 | "space-before-function-paren": "off", 63 | "global-require": "off", 64 | "indent": "off", 65 | "import/no-unresolved": [2, {"commonjs": true, "amd": true}], 66 | "import/no-dynamic-require": 0, 67 | "import/no-extraneous-dependencies": 1, 68 | "import/named": 2, 69 | "import/namespace": 2, 70 | "import/default": 2, 71 | "import/export": 2, 72 | "operator-linebreak": "off", 73 | "implicit-arrow-linebreak": "off", 74 | "no-await-in-loop": "off", 75 | "no-restricted-globals": ["error", "event"], 76 | "no-restricted-syntax": ["error", "WithStatement"] 77 | 78 | }, 79 | "overrides": [ 80 | { 81 | "files": ["*Modal.js"], 82 | "rules": { 83 | "object-shorthand": "off" 84 | } 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | dist 26 | .eslintcache 27 | .idea 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "trailingComma": "none", 6 | "tabWidth": 4, 7 | "proseWrap": "never" 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proton Pack 2 | 3 | Create your application with openpgp and our webpack config. 4 | 5 | ## How to install ? 6 | 7 | ```sh 8 | $ npm i -D github:ProtonMail/proton-pack.git#semver:^1.0.0 9 | ``` 10 | 11 | ## Create a new app with openpgp etc. 12 | 13 | 1. `$ npx proton-pack init` _If you want to use our boilerplate_ 14 | 2. `$ npm start` :popcorn: App available on :8080 15 | 16 | ### Dev env 17 | 18 | As for the WebClient you need to have `appConfig.json` (_previously_ `env.json`)
19 | A new key exists inside this file now, to add more config 20 | ```jsonc 21 | { 22 | "appConfig": { 23 | "clientId": "WebMail", // use to identify the application by the API 24 | "appName": "protonmail", // use to identify the application by the proton react components 25 | "urlI18n": "", // [mandatory if not protonmail] Url for i18n, ex: settings for protonmail-settings 26 | "clientType": "", // Custom client type 27 | "version": "", // Custom version 28 | } 29 | } 30 | ``` 31 | ## Commands 32 | 33 | - `$ proton-pack help` 34 | 35 | - `$ proton-pack init ` 36 | - type: default (default) basic app 37 | - type: auth basic app with login + private routes 38 | 39 | > _Create a basic app from our boilerplate with openpgp_ 40 | 41 | - `$ proton-pack extract-i18n` 42 | 43 | > _Extract translations for the app_ 44 | 45 | - `$ proton-pack compile` 46 | - `$ proton-pack dev-server` 47 | 48 | > _Run a dev server available on `8080` by default. You can customize the port via_ `NODE_ENV_PORT` 49 | 50 | You can also pass custom flags: 51 | - `--port=`: to use a custom port 52 | - `--publicPath=/settings/`: to serve the app on /settings/ 53 | - `--api=|`: key one of the ones from the appConfig (ex: .dev.api -> key= dev) or URL ex: https://mail.protonmail.com/api 54 | 55 | - `$ proton-pack compile` 56 | 57 | > _Build your app_ 58 | 59 | 60 | - `$ proton-pack print-config` 61 | 62 | > _Print as JSON the current config_ 63 | 64 | ## How to configure 65 | 66 | Create a file `proton.config.js` at the root of your app. 67 | 68 | - It takes and object as argument `webpackConfig` 69 | - It must return the config 70 | 71 | **It's a standard webpack config, nothing custom. It contains our config.** 72 | 73 | Ex: _to have a verbose dev server_ 74 | ```js 75 | module.exports = (webpackConfig) => { 76 | webpackConfig.devServer.stats = 'normal'; 77 | return webpackConfig; 78 | } 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /bin/protonPack: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs').promises; 4 | const path = require('path'); 5 | const chalk = require('chalk'); 6 | const dedent = require('dedent'); 7 | const argv = require('minimist')(process.argv.slice(2), { 8 | string: ['appMode', 'featureFlags', 'api'], 9 | boolean: ['sri'], 10 | default: { 11 | sri: true, 12 | api: 'proxy' 13 | } 14 | }); 15 | const localIp = require('my-local-ip'); 16 | 17 | const configBuilder = require('../cli/config'); 18 | const configAppBuilder = require('../cli/configApp'); 19 | const { json, debug, success, error } = require('../cli/helpers/log'); 20 | const { hasDirectory } = require('../cli/helpers/file'); 21 | const { getPort, getPublicPath, findPort } = require('../webpack/helpers/source'); 22 | 23 | const is = (command) => argv._.includes(command); 24 | 25 | const getSingleArgument = (argument) => { 26 | // Many flags creates an array, take the last one. 27 | if (Array.isArray(argument)) { 28 | const { length, [length - 1]: last, first } = argument; 29 | return last || first; 30 | } 31 | return argument; 32 | }; 33 | 34 | const getFeatureFlags = () => { 35 | return getSingleArgument(argv.featureFlags) || ''; 36 | }; 37 | 38 | const getAppMode = () => { 39 | return getSingleArgument(argv.appMode) || 'bundle'; 40 | }; 41 | 42 | async function main() { 43 | 44 | const { 45 | config: appConfig, 46 | path: appConfigPath, 47 | apiUrl, 48 | json: jsonConfig 49 | } = configAppBuilder(argv); 50 | 51 | const APP_MODE = getAppMode(); 52 | const FEATURE_FLAGS = getFeatureFlags(); 53 | const WRITE_SRI = argv.sri; 54 | const publicPath = getPublicPath(argv); 55 | 56 | // For any task BUT init we need to create a custom config for the app 57 | if (!is('init') && !is('help') && !is('print-config')) { 58 | debug(appConfig, 'app config'); 59 | await fs.writeFile(appConfigPath, appConfig); 60 | success(`generated ${appConfigPath}`); 61 | await hasDirectory(path.join('node_modules', 'proton-translations'), true); 62 | } 63 | 64 | if (is('print-config')) { 65 | return json(jsonConfig); 66 | } 67 | 68 | if (is('compile') || is('extract-i18n')) { 69 | const compile = require('../cli/compile'); 70 | const CONFIG = configBuilder({ 71 | publicPath, 72 | appMode: APP_MODE, 73 | featureFlags: FEATURE_FLAGS, 74 | writeSRI: WRITE_SRI, 75 | ...is('extract-i18n') && { flow: 'i18n' } 76 | }); 77 | return compile(CONFIG); 78 | } 79 | 80 | // Run the dev server, but first find an available port 81 | if (is('dev-server')) { 82 | const server = require('../cli/server'); 83 | const port = await findPort(getPort(argv)); 84 | 85 | const log = (ip = 'localhost') => chalk.yellow(`http://${ip}:${port}${publicPath}`); 86 | console.log(dedent` 87 | ➙ Dev server: ${log()} 88 | ➙ Dev server: ${log(localIp())} 89 | ➙ API: ${chalk.yellow(apiUrl)} 90 | \n 91 | `); 92 | const CONFIG = configBuilder({ port, publicPath, appMode: APP_MODE, featureFlags: FEATURE_FLAGS }); 93 | 94 | const run = server(CONFIG); 95 | run.listen(port); 96 | } 97 | 98 | // Init the application with the boilerplate 99 | if (is('init')) { 100 | const initProtonApp = require('../cli/initProtonApp'); 101 | initProtonApp(argv._[1]); 102 | } 103 | 104 | if (is('help')) { 105 | console.log(dedent` 106 | Usage: $ proton-pack 107 | Available commands: 108 | - ${chalk.blue('init')} ${chalk.blue('')} 109 | Create a basic app with OpenPGP. 110 | - type: default (default) basic app 111 | - type: auth basic app with login + private routes 112 | 113 | - ${chalk.blue('dev-server')} 114 | Dev server, default port on ${chalk.bold('8080')} 115 | - --api=: Change the API, based on your env.json 116 | - --port=: to use a custom port 117 | - --publicPath=/settings/: to serve the app on /settings/ 118 | 119 | - ${chalk.blue('compile')} 120 | - --api=: Change the API, based on your env.json 121 | +: Use MainAPI as a base for the env but use ExtendedAPI as the URL -> dev+proxy 122 | - --publicPath=/settings/: to build the app based on /settings/ 123 | Build the app 124 | 125 | - ${chalk.blue('extract-i18n')} 126 | Extract translations from a project (we use ttag) 127 | 128 | - ${chalk.blue('print-config')} 129 | Print as JSON the current config 130 | `); 131 | } 132 | } 133 | 134 | main().catch(error); 135 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:lts 6 | steps: 7 | - checkout 8 | - run: npm ci 9 | - run: npm run lint 10 | -------------------------------------------------------------------------------- /cli/compile.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 3 | const { success, error } = require('./helpers/log'); 4 | 5 | function main(config) { 6 | const compiler = webpack(config); 7 | 8 | new ProgressBarPlugin({ 9 | format: ' build [:bar] :percent (:elapsed seconds)', 10 | clear: false, 11 | width: 60 12 | }).apply(compiler); 13 | 14 | compiler.run((err, stats) => { 15 | if (err) { 16 | error(err); 17 | } 18 | 19 | success( 20 | stats.toString({ 21 | chunks: false, 22 | colors: true 23 | }) 24 | ); 25 | }); 26 | 27 | return compiler; 28 | } 29 | 30 | module.exports = main; 31 | -------------------------------------------------------------------------------- /cli/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { success, error } = require('./helpers/log'); 3 | 4 | /** 5 | * Load the config for webpack 6 | * We will try to load the proton.config.js in the user's app dir 7 | * if there is one. 8 | * We will use it to extend our config 9 | * @param {Object} cfg Our own configuration 10 | * @return {Object} 11 | */ 12 | const loadUserConfig = (cfg) => { 13 | try { 14 | const fromUser = require(path.join(process.cwd(), 'proton.config.js')); 15 | 16 | if (typeof fromUser !== 'function') { 17 | const msg = [ 18 | '[ProtonPack] Error', 19 | 'The custom config from proton.config.js must export a function.', 20 | 'This function takes one argument which is the webpack config.', 21 | '' 22 | ].join('\n'); 23 | console.error(msg); 24 | process.exit(1); 25 | } 26 | 27 | const config = fromUser(cfg); 28 | success('Found proton.config.js, extend the config'); 29 | return config; 30 | } catch (e) { 31 | if (e.code === 'MODULE_NOT_FOUND') { 32 | return cfg; 33 | } 34 | error(e); 35 | return cfg; 36 | } 37 | }; 38 | 39 | /** 40 | * format the config based on some options 41 | * - port: for the dev server 42 | * @param {Object} options 43 | * @return {Object} Webpack's config 44 | */ 45 | function main(options) { 46 | const defaultConfig = require('../webpack.config'); 47 | const cfg = defaultConfig(options); 48 | return loadUserConfig(cfg); 49 | } 50 | 51 | module.exports = main; 52 | -------------------------------------------------------------------------------- /cli/configApp.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const dedent = require('dedent'); 3 | const argv = require('minimist')(process.argv.slice(2)); 4 | 5 | const { sync } = require('./helpers/cli'); 6 | const { warn, error } = require('./helpers/log'); 7 | const { getPublicPath } = require('../webpack/helpers/source'); 8 | 9 | const isSilent = argv._.includes('help') || argv._.includes('init') || argv._.includes('print-config'); 10 | 11 | const readJSON = (file) => { 12 | const fileName = `${file}.json`; 13 | 14 | if (file === 'env' && !isSilent) { 15 | warn('[DEPREACTION NOTICE] Please rename your file env.json to appConfig.json'); 16 | } 17 | 18 | try { 19 | return require(path.join(process.cwd(), fileName)); 20 | } catch (e) { 21 | !isSilent && warn(`Missing file ${fileName}`); 22 | if (/SyntaxError/.test(e.stack)) { 23 | error(e); 24 | } 25 | } 26 | }; 27 | 28 | /** 29 | * Get the hash of the current commit we build against 30 | * @return {String} fill hash of the commit 31 | */ 32 | const getBuildCommit = () => { 33 | try { 34 | const { stdout = '' } = sync('git rev-parse HEAD'); 35 | return stdout.trim(); 36 | } catch (e) { 37 | return ''; 38 | } 39 | }; 40 | 41 | /** 42 | * Extract the config of a project 43 | * - env: from env.json for sentry, and some custom config for the app 44 | * appConfig: { 45 | * name: 'Web', 46 | * etc. 47 | * } 48 | * - pkg: from package.json for sentry 49 | * @return {Object} { env: Object, pkg: Object } 50 | */ 51 | const CONFIG_ENV = (() => { 52 | const pkg = require(path.join(process.cwd(), 'package.json')); 53 | // @todo load value from the env as it's done for proton-i19n 54 | return { 55 | env: readJSON('appConfig') || readJSON('env') || {}, 56 | pkg 57 | }; 58 | })(); 59 | 60 | /** 61 | * Read the configuration for translations, it's a file generated by the CI 62 | * : 63 | */ 64 | const LOCALES = (() => { 65 | try { 66 | return require(path.join(process.cwd(), 'node_modules', 'proton-translations', 'config', 'locales.json')); 67 | } catch (e) { 68 | if (!process.argv.includes('print-config')) { 69 | warn('No po/locales.json available yet'); 70 | } 71 | return {}; 72 | } 73 | })(); 74 | 75 | const ENV_CONFIG = Object.keys(CONFIG_ENV.env).reduce( 76 | (acc, key) => { 77 | if (key === 'appConfig') { 78 | acc.app = CONFIG_ENV.env[key]; 79 | return acc; 80 | } 81 | const { api, secure } = CONFIG_ENV.env[key]; 82 | api && (acc.api[key] = api); 83 | secure && (acc.secure[key] = secure); 84 | return acc; 85 | }, 86 | { api: {}, secure: {}, pkg: CONFIG_ENV.pkg, app: {} } 87 | ); 88 | 89 | const API_TARGETS = { 90 | prod: 'https://mail.protonmail.com/api', 91 | local: 'https://protonmail.dev/api', 92 | localhost: 'https://localhost/api', 93 | build: '/api', 94 | ...ENV_CONFIG.api 95 | }; 96 | 97 | /** 98 | * Yargs creates an array if you gives many flags 99 | * Ensure to take only the last one 100 | * @param {String|Array} api 101 | * @return {Object} 102 | */ 103 | const getApi = (api) => { 104 | const parse = (api) => { 105 | if (!Array.isArray(api)) { 106 | return api || 'proxy'; 107 | } 108 | 109 | const { length, [length - 1]: latest } = api.filter(Boolean); 110 | return latest || 'proxy'; 111 | }; 112 | 113 | const value = parse(api); 114 | 115 | // We can do --api=https://mail.protonmail.com/api and it's only for dev, so we can stop here 116 | if (value.includes('https') || value.includes('/api')) { 117 | return { value, url: value }; 118 | } 119 | 120 | // Because we can "extend" via + -> --api dev+proxy = dev env but with /api as API url 121 | const urlList = value.split('+'); 122 | const url = urlList.reduce((apiUrl, apiKey) => API_TARGETS[apiKey] || apiUrl, API_TARGETS.prod); 123 | 124 | return { value, url, first: urlList[0] }; 125 | }; 126 | 127 | function main({ api = 'dev' }) { 128 | const { url: apiUrl } = getApi(api); 129 | const json = { 130 | clientId: ENV_CONFIG.app.clientId || 'WebMail', 131 | appName: ENV_CONFIG.app.appName || ENV_CONFIG.pkg.name || 'protonmail', 132 | version: ENV_CONFIG.app.version || ENV_CONFIG.pkg.version || '3.16.20', 133 | locales: LOCALES, 134 | apiUrl 135 | }; 136 | 137 | const COMMIT_RELEASE = getBuildCommit(); 138 | 139 | const isProduction = process.env.NODE_ENV === 'production'; 140 | const SENTRY_DSN = isProduction ? ENV_CONFIG.app.sentry : ''; 141 | 142 | const PUBLIC_APP_PATH = getPublicPath(argv); 143 | 144 | const config = dedent` 145 | export const CLIENT_ID = '${json.clientId}'; 146 | export const CLIENT_TYPE = ${ENV_CONFIG.app.clientType || 1}; 147 | export const CLIENT_SECRET = '${ENV_CONFIG.app.clientSecret || ''}'; 148 | export const APP_VERSION = '${json.version}'; 149 | export const APP_NAME = '${json.appName}'; 150 | export const API_URL = '${apiUrl}'; 151 | export const LOCALES = ${JSON.stringify(LOCALES)}; 152 | export const API_VERSION = '3'; 153 | export const DATE_VERSION = '${new Date().toGMTString()}'; 154 | export const CHANGELOG_PATH = '${PUBLIC_APP_PATH}assets/changelog.tpl.html'; 155 | export const VERSION_PATH = '${PUBLIC_APP_PATH}assets/version.json'; 156 | export const COMMIT_RELEASE = '${COMMIT_RELEASE}'; 157 | export const SENTRY_DSN = '${SENTRY_DSN}'; 158 | `; 159 | 160 | return { 161 | config, 162 | apiUrl, 163 | json, 164 | path: path.join(process.cwd(), 'src', 'app', 'config.ts') 165 | }; 166 | } 167 | 168 | module.exports = main; 169 | -------------------------------------------------------------------------------- /cli/helpers/cli.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const execa = require('execa'); 3 | const { debug } = require('./log'); 4 | 5 | const sync = (cli) => execa.sync(cli, { shell: true }); 6 | 7 | const bash = (cli, args = [], stdio) => { 8 | debug({ cli, args, stdio }, 'shell command'); 9 | return execa(cli, args, { shell: '/bin/bash', stdio }); 10 | }; 11 | 12 | const script = (cli, args = [], stdio) => { 13 | const cmd = path.resolve(__dirname, '..', '..', 'scripts', cli); 14 | return bash(cmd, args, stdio); 15 | }; 16 | 17 | module.exports = { bash, script, sync }; 18 | -------------------------------------------------------------------------------- /cli/helpers/file.js: -------------------------------------------------------------------------------- 1 | const { promises: fs, constants: FS_CONSTANTS } = require('fs'); 2 | const path = require('path'); 3 | 4 | const { warn } = require('./log'); 5 | 6 | /** 7 | * Check if a directory exists, else we create it 8 | * @param {String} filePath Path to a file (or directory if isDir is true) 9 | * @param {Boolean} isDir The path is not for a file but a hasDirectory 10 | * @return {void} 11 | */ 12 | async function hasDirectory(filePath, isDir) { 13 | const dir = isDir ? filePath : path.dirname(filePath); 14 | try { 15 | await fs.access(dir, FS_CONSTANTS.F_OK | FS_CONSTANTS.W_OK); 16 | } catch (e) { 17 | warn(`Cannot find/write the directory ${dir}, we're going to create it`); 18 | await fs.mkdir(dir); 19 | } 20 | } 21 | 22 | module.exports = { hasDirectory }; 23 | -------------------------------------------------------------------------------- /cli/helpers/log.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | 4 | const SCOPE = '[proton-pack]'; 5 | 6 | const warn = (msg, info) => { 7 | console.log(`${chalk.bgMagenta(SCOPE)} ${chalk.bold(msg)}.`); 8 | if (info) { 9 | console.log(''); 10 | console.log(` ${info}`); 11 | console.log(''); 12 | console.log(''); 13 | } 14 | }; 15 | 16 | const success = (msg, { time, space = false } = {}) => { 17 | const txt = `${chalk.bgGreen(SCOPE)} ${chalk.bold('✓ ' + msg)}`; 18 | const message = [txt, time && `(${time})`].filter(Boolean).join(''); 19 | space && console.log(); 20 | console.log(message); 21 | }; 22 | 23 | const json = (data, noSpace) => { 24 | !noSpace && console.log(); 25 | console.log(JSON.stringify(data, null, 2).trim()); 26 | console.log(); 27 | }; 28 | 29 | const error = (e) => { 30 | console.log(chalk.bgRed(SCOPE), chalk.red(e.message)); 31 | console.log(); 32 | console.log(); 33 | console.error(e); 34 | process.exit(1); 35 | }; 36 | 37 | function debug(item, message = 'debug') { 38 | if (!(argv.v || argv.verbose)) { 39 | return; 40 | } 41 | if (Array.isArray(item) || typeof item === 'object') { 42 | console.log(`${SCOPE} ${message}`); 43 | return json(item, true); 44 | } 45 | 46 | console.log(`${SCOPE} ${message}\n`, item); 47 | } 48 | 49 | module.exports = { 50 | success, 51 | error, 52 | json, 53 | debug, 54 | warn 55 | }; 56 | -------------------------------------------------------------------------------- /cli/initProtonApp.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs').promises; 3 | const chalk = require('chalk'); 4 | const dedent = require('dedent'); 5 | 6 | const { success } = require('./helpers/log'); 7 | const { bash } = require('./helpers/cli'); 8 | 9 | const TEMPLATE = path.resolve(__dirname, '..', 'template'); 10 | const PATH_APP_PKG = path.join(process.cwd(), 'package.json'); 11 | 12 | /** 13 | * Copy the template boilerplate into the root app 14 | * - type: default (default) a boilerplate with everything but the auth 15 | * - type: auth a boilerplate + private routes 16 | * @param {String} type type of boilerplate you want to setup 17 | */ 18 | async function main(type = 'default') { 19 | // Make a copy of the whole src repo 20 | await bash(`cp -r ${TEMPLATE}/${type} src`); 21 | // Copy assets 22 | await bash(`cp -r ${TEMPLATE}/assets src/assets`); 23 | // Copy basic i18n setup 24 | await bash(`cp -r ${TEMPLATE}/po po`); 25 | // Copy hidden config files 26 | await bash(`cp -r ${TEMPLATE}/.{editorconfig,eslintrc.json,prettierrc} .`); 27 | 28 | if (type === 'auth') { 29 | // Copy tsconfig 30 | await bash(`cp -r ${TEMPLATE}/tsconfig.json .`); 31 | } 32 | 33 | // Copy config tmp 34 | await bash(`cp -r ${TEMPLATE}/circle.yml .`); 35 | // Copy custom gitignore as during the npm install .gitignore is removed... wtf 36 | await bash(`cp -r ${TEMPLATE}/_gitignore .gitignore`); 37 | await bash(`cp ${TEMPLATE}/Readme.md Readme.md`); 38 | await bash('echo {} > appConfig.json'); 39 | 40 | const pkgTpl = require('../template/package.json'); 41 | const pkgApp = require(PATH_APP_PKG); 42 | 43 | // Extend the config with the boilerplate's one 44 | const pkg = { 45 | ...pkgApp, 46 | ...pkgTpl, 47 | devDependencies: { 48 | ...pkgApp.devDependencies, 49 | ...pkgTpl.devDependencies 50 | } 51 | }; 52 | 53 | // Prout 54 | await fs.writeFile(PATH_APP_PKG, JSON.stringify(pkg, null, 4)); 55 | 56 | console.log(dedent` 57 | 🎉 ${chalk.green('Your app is ready')} 58 | 59 | Here is what's available for this setup: 60 | - EditorConfig 61 | - Eslint 62 | - Prettier 63 | - Circle.ci config (lint js + i18n) 64 | - Husky + lint-staged 65 | - React 66 | - Deploy ready, you will need to create an empty branch: deploy-x 67 | - npm scripts 68 | - ${chalk.yellow('start')}: dev server 69 | - ${chalk.yellow('deploy')}: deploy 70 | - ${chalk.yellow('pretty')}: run prettier 71 | - ${chalk.yellow('i18n:getlatest')}: upgrade translations inside your app 72 | - Hook postversion for pushing git tag 73 | 74 | ➙ Now you can run ${chalk.yellow('npm i')} 75 | ➙ Once it's done: ${chalk.yellow('npm start')} 76 | `); 77 | console.log(); 78 | success('Setup done, do not forget about the appConfig.json'); 79 | } 80 | 81 | module.exports = main; 82 | -------------------------------------------------------------------------------- /cli/server.js: -------------------------------------------------------------------------------- 1 | const WebpackDevServer = require('webpack-dev-server'); 2 | const webpack = require('webpack'); 3 | 4 | function main(config) { 5 | const compiler = webpack(config); 6 | const server = new WebpackDevServer(compiler, config.devServer); 7 | return server; 8 | } 9 | 10 | module.exports = main; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proton-pack", 3 | "author": "Proton Technologies AG", 4 | "description": "Setup boilerplate for webpack", 5 | "version": "3.8.9", 6 | "bin": { 7 | "proton-pack": "./bin/protonPack" 8 | }, 9 | "module": "src/index.js", 10 | "main": "src/index.js", 11 | "engines": { 12 | "node": ">= 12.13.1" 13 | }, 14 | "dependencies": { 15 | "@babel/cli": "^7.13.10", 16 | "@babel/core": "^7.13.10", 17 | "@babel/plugin-proposal-class-properties": "^7.13.0", 18 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", 19 | "@babel/plugin-proposal-object-rest-spread": "^7.13.8", 20 | "@babel/plugin-proposal-optional-chaining": "^7.13.8", 21 | "@babel/plugin-proposal-private-methods": "^7.13.0", 22 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 23 | "@babel/plugin-transform-regenerator": "^7.12.13", 24 | "@babel/plugin-transform-runtime": "^7.13.10", 25 | "@babel/preset-env": "^7.13.10", 26 | "@babel/preset-react": "^7.12.13", 27 | "@babel/preset-typescript": "^7.13.0", 28 | "@babel/runtime": "^7.13.10", 29 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.1", 30 | "babel-loader": "^8.1.0", 31 | "babel-plugin-istanbul": "^6.0.0", 32 | "babel-plugin-lodash": "^3.3.4", 33 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 34 | "babel-plugin-ttag": "^1.7.30", 35 | "copy-webpack-plugin": "^6.4.1", 36 | "core-js": "^3.12.1", 37 | "cross-env": "^7.0.2", 38 | "css-loader": "^5.1.2", 39 | "dedent": "^0.7.0", 40 | "del-cli": "^3.0.1", 41 | "execa": "^5.0.0", 42 | "favicons-webpack-plugin": "^3.0.1", 43 | "file-loader": "^6.0.0", 44 | "html-webpack-plugin": "^4.5.2", 45 | "imagemin-mozjpeg": "^9.0.0", 46 | "imagemin-webpack-plugin": "^2.4.0", 47 | "markdown-it-attrs": "^3.0.3", 48 | "markdown-it-link-attributes": "^2.1.0", 49 | "mini-css-extract-plugin": "^0.12.0", 50 | "minimist": "^1.2.5", 51 | "my-local-ip": "^1.0.0", 52 | "node-progress": "^0.1.0", 53 | "optimize-css-assets-webpack-plugin": "^5.0.3", 54 | "po-gettext-loader": "^1.0.0", 55 | "postcss-loader": "^3.0.0", 56 | "postcss-preset-env": "^6.7.0", 57 | "progress-bar-webpack-plugin": "^2.1.0", 58 | "raw-loader": "^4.0.0", 59 | "react-refresh": "^0.9.0", 60 | "regenerator-runtime": "^0.13.5", 61 | "sass": "^1.34.1", 62 | "sass-loader": "^10.1.1", 63 | "script-ext-html-webpack-plugin": "^2.1.3", 64 | "script-loader": "^0.7.2", 65 | "sharp": "^0.27.2", 66 | "source-map-loader": "^1.1.3", 67 | "string-replace-loader": "^3.0.1", 68 | "strip-ansi": "^6.0.0", 69 | "style-loader": "^2.0.0", 70 | "svg-inline-loader": "^0.8.2", 71 | "terser-webpack-plugin": "^4.2.3", 72 | "ttag": "^1.7.24", 73 | "ttag-cli": "^1.9.1", 74 | "url-loader": "^4.1.0", 75 | "webpack": "^4.43.0", 76 | "webpack-cli": "^3.3.11", 77 | "webpack-dev-server": "^3.11.2", 78 | "webpack-subresource-integrity": "^1.5.2", 79 | "write-webpack-plugin": "^1.1.0" 80 | }, 81 | "devDependencies": { 82 | "babel-eslint": "^10.1.0", 83 | "eslint": "^7.21.0", 84 | "eslint-config-airbnb-base": "^14.2.1", 85 | "eslint-plugin-import": "^2.22.1", 86 | "husky": "^5.1.3", 87 | "lint-staged": "^10.5.4", 88 | "prettier": "^2.1.2" 89 | }, 90 | "scripts": { 91 | "lint": "eslint $(find cli bin -type f) --quiet --cache", 92 | "pretty": "prettier --write cli bin", 93 | "sync:lock": "npm run follow:lock && git add package-lock.json && git commit -m 'Sync package-lock.json' && git push && npm run unfollow:lock", 94 | "follow:lock": "git update-index --no-assume-unchanged package-lock.json", 95 | "unfollow:lock": "git update-index --assume-unchanged package-lock.json", 96 | "preversion": "npm run follow:lock", 97 | "postversion": "npm run unfollow:lock && git push --tags && git push" 98 | }, 99 | "license": "MIT", 100 | "lint-staged": { 101 | "*.js": [ 102 | "prettier --write", 103 | "git add" 104 | ] 105 | }, 106 | "husky": { 107 | "hooks": { 108 | "pre-commit": "lint-staged" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 4 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /template/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["react", "react-hooks", "import", "@typescript-eslint"], 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "jsx": true 9 | } 10 | }, 11 | "env": { 12 | "browser": true, 13 | "commonjs": true, 14 | "es6": true, 15 | "jest": true, 16 | "node": true 17 | }, 18 | "settings": { 19 | "react": { 20 | "version": "detect" 21 | }, 22 | "import/resolver": { 23 | "node": { 24 | "extensions": [".js", ".ts", ".tsx"] 25 | } 26 | } 27 | }, 28 | "extends": ["plugin:@typescript-eslint/recommended", "eslint:recommended", "plugin:react/recommended"], 29 | "rules": { 30 | "react/display-name": "off", 31 | "react/prop-types": "warn", 32 | "react-hooks/rules-of-hooks": "error", 33 | "import/no-unresolved": [2, { "commonjs": true, "amd": true }], 34 | "import/named": 2, 35 | "import/namespace": 2, 36 | "import/default": 2, 37 | "import/export": 2, 38 | "@typescript-eslint/explicit-function-return-type": "off", 39 | "@typescript-eslint/no-explicit-any": "off", 40 | "@typescript-eslint/indent": "off" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "proseWrap": "never" 7 | } 8 | -------------------------------------------------------------------------------- /template/Readme.md: -------------------------------------------------------------------------------- 1 | You will need: 2 | - bash 3 | - node (mini latest LTS) 4 | - npm (latest too, it's better) 5 | 6 | ## How to dev 1 7 | 8 | 1. Clone this repository 9 | 2. Run `$ npm i` 10 | 3. `$ npm start` 11 | 12 | It will give you the URL where it's available. 13 | 14 | > You can login via `/login` 15 | 16 | ## Sync translations [App to crowdin] 17 | 18 | You can sync them via `$ npm run i18n:upgrade`, it will: 19 | - Extract translations 20 | - Push them to crowndin 21 | - Create a commit with them on the repo 22 | 23 | 24 | ## How to deploy 25 | 26 | - `$ npm run deploy -- --branch= --api=` 27 | _Deploy the app as /$config_ 28 | 29 | `$config`: See package.json config.publicPathFlag 30 | 31 | - `$ npm run deploy:standalone -- --branch= --api=` 32 | _Deploy the app as deploy + /login_ 33 | 34 | Based on [proton-bundler](https://github.com/ProtonMail/proton-bundler) 35 | 36 | ## Sync translations [Crowdin to our App] 37 | 38 | To get latest translations available on crowdin, you can run `$ npm run i18n:getlatest`. 39 | It will: 40 | - Get list of translations available (default same as proton-i18n crowdin --list --type --limit=95) 41 | - Upgrade our translations with ones from crowdin 42 | - Store a cache of translations available in the app 43 | - Export translations as JSON 44 | - Commit everything 45 | 46 | > :warning: If you want to get only a **custom** list of translations, configure it inside `po/i18n.txt` and run `$ npm run i18n:getlatest -- --custom` 47 | -------------------------------------------------------------------------------- /template/_gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | dist 26 | .eslintcache 27 | .idea 28 | src/app/config.js 29 | appConfig.json 30 | env.json 31 | .env 32 | po/i18n.txt 33 | -------------------------------------------------------------------------------- /template/assets/logoConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | logo: 'src/assets/protonmail.svg', 3 | favicons: { 4 | appName: 'ProtonMail', 5 | appDescription: 6 | "ProtonMail is the world's largest secure email service, developed by CERN and MIT scientists.We are open source and protected by Swiss privacy law", 7 | developerName: 'Proton Technologies AG', 8 | developerURL: 'https://github.com/ProtonMail/proton-mail', 9 | background: '#262a33', 10 | // eslint-disable-next-line @typescript-eslint/camelcase 11 | theme_color: '#262a33' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /template/assets/protonmail.svg: -------------------------------------------------------------------------------- 1 | Artboard 2 copy 2 -------------------------------------------------------------------------------- /template/auth/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | RewriteCond %{HTTPS} !=on 4 | RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] 5 | 6 | # Redirect nothing to app 7 | RewriteRule ^$ /index.html [NC,L] 8 | 9 | # Hide .git stuff 10 | RewriteRule ^.*?\.git.* /index.html [NC,L] 11 | 12 | RewriteCond %{REQUEST_FILENAME} -s [OR] 13 | RewriteCond %{REQUEST_FILENAME} -l [OR] 14 | RewriteCond %{REQUEST_FILENAME} -d 15 | RewriteRule ^.*$ - [NC,L] 16 | 17 | RewriteRule ^(.*) /index.html [NC,L] 18 | 19 | # Error pages 20 | ErrorDocument 403 /assets/errors/403.html 21 | 22 | 23 | FileETag None 24 | Header unset ETag 25 | Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" 26 | Header set Pragma "no-cache" 27 | Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT" 28 | 29 | 30 | 31 | AddType application/font-woff2 .woff2 32 | 33 | 34 | 35 | AddOutputFilter INCLUDES;DEFLATE svg 36 | 37 | -------------------------------------------------------------------------------- /template/auth/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /template/auth/app/App.tsx: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader/root'; 2 | import React from 'react'; 3 | import { ProtonApp, useAuthentication, useInstance } from 'react-components'; 4 | import createSecureSessionStorage from 'proton-shared/lib/createSecureSessionStorage'; 5 | import { MAILBOX_PASSWORD_KEY, UID_KEY } from 'proton-shared/lib/constants'; 6 | 7 | import * as config from './config'; 8 | import PrivateApp from './PrivateApp'; 9 | import PublicApp from './PublicApp'; 10 | 11 | import './app.scss'; 12 | 13 | const Setup = () => { 14 | const { UID, login, logout } = useAuthentication(); 15 | if (UID) { 16 | return ; 17 | } 18 | return ; 19 | }; 20 | 21 | const App = () => { 22 | const storage = useInstance(() => createSecureSessionStorage([MAILBOX_PASSWORD_KEY, UID_KEY])); 23 | return ( 24 |
25 | 26 | 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default hot(App); 33 | -------------------------------------------------------------------------------- /template/auth/app/PrivateApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import { ErrorBoundary, StandardPrivateApp } from 'react-components'; 4 | import { UserModel, MailSettingsModel, UserSettingsModel } from 'proton-shared/lib/models'; 5 | 6 | import Home from './containers/Home'; 7 | import About from './containers/About'; 8 | import PrivateLayout from './components/layout/PrivateLayout'; 9 | 10 | const NotFoundContainer = () =>

Not found

; 11 | 12 | interface Props { 13 | onLogout: () => void; 14 | } 15 | 16 | const PrivateApp = ({ onLogout }: Props) => { 17 | return ( 18 | 23 | 24 | ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | )} 34 | /> 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default PrivateApp; 41 | -------------------------------------------------------------------------------- /template/auth/app/PublicApp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useLayoutEffect } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import { Loader, ModalsChildren, LoginForm } from 'react-components'; 4 | import { loadOpenPGP } from 'proton-shared/lib/openpgp'; 5 | 6 | import PublicLayout from './components/layout/PublicLayout'; 7 | 8 | interface Props { 9 | onLogin: (config: any) => void; 10 | } 11 | 12 | const PublicApp = ({ onLogin }: Props) => { 13 | const [loading, setLoading] = useState(true); 14 | const [error, setError] = useState(false); 15 | 16 | useLayoutEffect(() => { 17 | (async () => { 18 | await Promise.all([loadOpenPGP()]); 19 | })() 20 | .then(() => setLoading(false)) 21 | .catch(() => setError(true)); 22 | }, []); 23 | 24 | if (error) { 25 | return <>OpenPGP failed to load. Handle better.; 26 | } 27 | 28 | if (loading) { 29 | return ; 30 | } 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 | 38 | } /> 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default PublicApp; 47 | -------------------------------------------------------------------------------- /template/auth/app/app.scss: -------------------------------------------------------------------------------- 1 | @import '~react-components/styles/index'; 2 | -------------------------------------------------------------------------------- /template/auth/app/components/layout/PrivateHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { UserDropdown, Hamburger } from 'react-components'; 3 | 4 | interface Props { 5 | expanded: boolean; 6 | onToggleExpand: () => void; 7 | } 8 | 9 | // TODO: add logo to MainLogo in react-components, and remove the placeholder 10 | const Header = ({ expanded, onToggleExpand }: Props) => { 11 | return ( 12 |
13 | 14 | {/* */} 15 | Placeholder app logo 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /template/auth/app/components/layout/PrivateLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import { c } from 'ttag'; 3 | import { Sidebar, Icons, MainAreaContext, useToggle } from 'react-components'; 4 | import { withRouter, RouteComponentProps, match } from 'react-router-dom'; 5 | import Header from './PrivateHeader'; 6 | 7 | const getSidebar = () => { 8 | return [ 9 | { 10 | text: c('Link').t`Home`, 11 | link: '/', 12 | isActive: (m: match) => !!m && m.isExact 13 | }, 14 | { 15 | text: c('Link').t`About`, 16 | link: '/about' 17 | } 18 | ]; 19 | }; 20 | 21 | interface Props extends RouteComponentProps { 22 | children: React.ReactNode; 23 | } 24 | 25 | const PrivateLayout = ({ children, location }: Props) => { 26 | const mainAreaRef = useRef(null); 27 | const { state: isHeaderExpanded, toggle: toggleHeaderExpanded, set: setHeaderExpanded } = useToggle(); 28 | 29 | useEffect(() => { 30 | setHeaderExpanded(false); 31 | if (mainAreaRef.current) { 32 | mainAreaRef.current.scrollTop = 0; 33 | } 34 | }, [location.pathname]); 35 | 36 | return ( 37 | <> 38 |
39 |
40 | 41 |
42 | {children} 43 |
44 |
45 | 46 | 47 | ); 48 | }; 49 | 50 | export default withRouter(PrivateLayout); 51 | -------------------------------------------------------------------------------- /template/auth/app/components/layout/PublicLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { Icons, MainAreaContext } from 'react-components'; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | // TODO: fix ref 9 | const PublicLayout = ({ children }: Props) => { 10 | const mainAreaRef = useRef(); 11 | 12 | return ( 13 | <> 14 |
15 |
16 | {children} 17 |
18 |
19 | 20 | 21 | ); 22 | }; 23 | 24 | export default PublicLayout; 25 | -------------------------------------------------------------------------------- /template/auth/app/containers/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { SubTitle, Title, Bordered, PrimaryButton, Icon } from 'react-components'; 4 | 5 | function About() { 6 | return ( 7 |
8 | 9 | {c('Context').t`Pour la petite histoire`} 10 | Il était une fois... 11 | 12 |

13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore 14 | et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 15 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 16 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 17 | culpa qui officia deserunt mollit anim id est laborum. 18 |

19 | 20 | 21 | 22 | Click me 23 | 24 | 25 | 26 |
27 | 32 | 37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export default About; 44 | -------------------------------------------------------------------------------- /template/auth/app/containers/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useUser } from 'react-components'; 3 | 4 | function Home() { 5 | const [user] = useUser(); 6 | 7 | return ( 8 |
9 |

Welcome {user.Name}

10 |

11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et 12 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 13 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 14 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia 15 | deserunt mollit anim id est laborum. 16 |

17 |
18 | ); 19 | } 20 | 21 | export default Home; 22 | -------------------------------------------------------------------------------- /template/auth/app/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import 'core-js/stable'; 4 | import 'regenerator-runtime/runtime'; 5 | import 'yetch/polyfill'; 6 | 7 | import App from './App'; 8 | 9 | ReactDOM.render(, document.querySelector('.app-root')); 10 | -------------------------------------------------------------------------------- /template/circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.9.0-stretch-browsers 6 | steps: 7 | - checkout 8 | - run: npm i 9 | - run: npm run check-types 10 | - run: npx proton-pack 11 | - run: npm run lint 12 | - run: npm run i18n:validate 13 | - run: npm run i18n:validate:context 14 | -------------------------------------------------------------------------------- /template/default/app.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /template/default/app/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { c } from 'ttag'; 3 | import { Title, SubTitle, Bordered, PrimaryButton, Icon, Icons } from 'react-components'; 4 | 5 | export default () => { 6 | return ( 7 | <> 8 |
9 | Proton boilerplate 10 |
11 |
12 | 13 | {c('Context').t`Pour la petite histoire`} 14 | Il était une fois... 15 | 16 |

17 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut 18 | labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco 19 | laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in 20 | voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 21 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 22 |

23 | 24 | 25 | 26 | Click me 27 | 28 | 29 | 30 |
31 | 32 | 37 |
38 | 39 |

40 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut 41 | labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco 42 | laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in 43 | voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 44 | non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 45 |

46 |
47 | 48 |
49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /template/default/app/app.scss: -------------------------------------------------------------------------------- 1 | @import '~react-components/styles/index'; 2 | -------------------------------------------------------------------------------- /template/default/app/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import 'core-js/stable'; 4 | import 'regenerator-runtime/runtime'; 5 | import 'yetch/polyfill'; 6 | 7 | import App from './App'; 8 | 9 | ReactDOM.render(, document.querySelector('.app-root')); 10 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "publicPathFlag": "--publicPath=/settings/" 4 | }, 5 | "dependencies": { 6 | "abortcontroller-polyfill": "^1.3.0", 7 | "design-system": "github:ProtonMail/design-system.git#master", 8 | "pmcrypto": "github:ProtonMail/pmcrypto.git#semver:^6.1.0", 9 | "proton-shared": "github:ProtonMail/proton-shared.git#master", 10 | "proton-pack": "github:ProtonMail/proton-pack.git#semver:^2.0.1", 11 | "react": "^16.8.6", 12 | "react-components": "github:ProtonMail/react-components.git#master", 13 | "react-dom": "^16.8.6", 14 | "react-router-dom": "^4.3.1", 15 | "ttag": "^1.7.14", 16 | "yetch": "^1.1.0" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^24.0.22", 20 | "@types/react": "^16.9.5", 21 | "@types/react-dom": "^16.9.1", 22 | "@types/react-router-dom": "^5.1.0", 23 | "@typescript-eslint/eslint-plugin": "^2.3.3", 24 | "@typescript-eslint/parser": "^2.3.3", 25 | "husky": "^2.3.0", 26 | "lint-staged": "^8.1.7", 27 | "prettier": "^1.17.1", 28 | "proton-bundler": "github:ProtonMail/proton-bundler#semver:^1.7.7", 29 | "proton-i18n": "github:ProtonMail/proton-i18n#semver:^1.6.12", 30 | "typescript": "^3.6.4" 31 | }, 32 | "scripts": { 33 | "start": "proton-pack dev-server", 34 | "lint": "eslint src --ext .js,.ts,.tsx --cache", 35 | "pretty": "prettier --write $(find src/app -type f -name '*.js' -o -name '*.ts' -o -name '*.tsx')", 36 | "preversion": "git update-index --no-assume-unchanged package-lock.json", 37 | "postversion": "git update-index --assume-unchanged package-lock.json && git push --tags", 38 | "i18n:validate": "proton-i18n validate lint-functions", 39 | "i18n:validate:context": "proton-i18n extract && proton-i18n validate", 40 | "i18n:upgrade": "proton-i18n extract && proton-i18n crowdin -u && proton-i18n commit update", 41 | "i18n:getlatest": "proton-i18n upgrade", 42 | "deploy": "proton-bundler", 43 | "deploy:standalone": "proton-bundler --appMode standalone", 44 | "build": "cross-env NODE_ENV=production proton-pack compile $npm_package_config_publicPathFlag", 45 | "build:standalone": "cross-env NODE_ENV=production proton-pack compile --appMode=standalone", 46 | "check-types": "npm run tsc" 47 | }, 48 | "lint-staged": { 49 | "(*.js|*.ts|*.tsx)": [ 50 | "prettier --write", 51 | "git add" 52 | ] 53 | }, 54 | "husky": { 55 | "hooks": { 56 | "pre-commit": "lint-staged" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /template/po/lang.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "proton-shared/tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { getSource, firstExisting } = require('./webpack/helpers/source'); 2 | const { getJsLoaders } = require('./webpack/js.loader'); 3 | const getCssLoaders = require('./webpack/css.loader'); 4 | const getAssetsLoaders = require('./webpack/assets.loader'); 5 | const getAlias = require('./webpack/alias'); 6 | const getPlugins = require('./webpack/plugins'); 7 | const getOptimizations = require('./webpack/optimization'); 8 | const { outputPath } = require('./webpack/paths'); 9 | 10 | function main({ port, publicPath, flow, appMode, featureFlags, writeSRI = true }) { 11 | const isProduction = process.env.NODE_ENV === 'production'; 12 | const isTtag = flow === 'i18n'; 13 | 14 | const options = { 15 | isProduction, 16 | isTtag, 17 | publicPath: publicPath || '/', 18 | appMode, 19 | featureFlags, 20 | writeSRI 21 | }; 22 | 23 | const config = { 24 | stats: 'minimal', 25 | mode: isProduction ? 'production' : 'development', 26 | bail: isProduction, 27 | devtool: false, 28 | watchOptions: { 29 | ignored: [/node_modules/, 'i18n/*.json', /\*\.(gif|jpeg|jpg|ico|png)/] 30 | }, 31 | resolve: { 32 | extensions: ['.js', '.tsx', '.ts'], 33 | alias: getAlias() 34 | }, 35 | entry: { 36 | // The order is important. The supported.js file sets a global variable that is used by unsupported.js to detect if the main bundle could be parsed. 37 | index: [ 38 | firstExisting(['./src/app/index.tsx', './src/app/index.js']), 39 | getSource('./node_modules/proton-shared/lib/browser/supported.js') 40 | ], 41 | unsupported: [getSource('./node_modules/proton-shared/lib/browser/unsupported.js')] 42 | }, 43 | output: { 44 | path: outputPath, 45 | filename: isProduction ? '[name].[chunkhash:8].js' : '[name].js', 46 | publicPath, 47 | chunkFilename: isProduction ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js', 48 | crossOriginLoading: 'anonymous' 49 | }, 50 | module: { 51 | // Make missing exports an error instead of warning 52 | strictExportPresence: true, 53 | rules: [...getJsLoaders(options), ...getCssLoaders(options), ...getAssetsLoaders(options)] 54 | }, 55 | plugins: getPlugins(options), 56 | optimization: getOptimizations(options), 57 | devServer: { 58 | hot: !isProduction, 59 | inline: true, 60 | compress: true, 61 | host: '0.0.0.0', 62 | historyApiFallback: { 63 | index: publicPath 64 | }, 65 | disableHostCheck: true, 66 | contentBase: outputPath, 67 | publicPath, 68 | stats: 'minimal' 69 | } 70 | }; 71 | 72 | if (isTtag) { 73 | delete config.devServer; 74 | delete config.optimization; 75 | } 76 | 77 | return config; 78 | } 79 | 80 | module.exports = main; 81 | -------------------------------------------------------------------------------- /webpack/alias.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const getAlias = () => { 4 | const standard = [ 5 | 'react', 6 | 'react-dom', 7 | 'react-router', 8 | 'react-router-dom', 9 | 'react-refresh', 10 | 'pmcrypto', 11 | 'design-system', 12 | 'react-components', 13 | 'ttag', 14 | 'date-fns', 15 | 'proton-translations', 16 | '@babel/runtime' 17 | // Ensure that the correct package is used when symlinking 18 | ].reduce((acc, key) => ({ ...acc, [key]: path.resolve(`./node_modules/${key}`) }), {}); 19 | 20 | return { 21 | ...standard, 22 | // Custom alias as we're building for the web (mimemessage) 23 | iconv: 'iconv-lite' 24 | }; 25 | }; 26 | 27 | module.exports = getAlias; 28 | -------------------------------------------------------------------------------- /webpack/assets.loader.js: -------------------------------------------------------------------------------- 1 | const LIMIT = 10000; 2 | const DESIGN_SYSTEM_ICONS_SVG = 'sprite-icons.svg|mime-icons.svg|file-icons.svg'; 3 | const DESIGN_SYSTEM_CSS_SVG = 'sprite-for-css-only.svg'; 4 | 5 | module.exports = () => [ 6 | { 7 | /** 8 | * oneOf allows to take the first match instead of all matches, .e.g 9 | * 10 | * without one of: 11 | * 12 | * sprite-icons.svg -> [svg-inline-loader, url-loader, file-loader] 13 | * img-1.svg -> [url loader, file loader] 14 | * design-system-icon.svg -> file loader 15 | * 16 | * with one of: 17 | * 18 | * sprite-icons.svg -> svg-inline-loader 19 | * img-1.svg -> url loader 20 | * design-system-icon.svg -> file loader 21 | */ 22 | oneOf: [ 23 | { 24 | test: new RegExp(`${DESIGN_SYSTEM_ICONS_SVG}$`), 25 | use: [ 26 | { 27 | loader: 'svg-inline-loader' 28 | } 29 | ] 30 | }, 31 | { 32 | test: /\.(bmp|png|jpg|jpeg|gif|svg)$/, 33 | loader: 'url-loader', 34 | exclude: new RegExp(`${DESIGN_SYSTEM_CSS_SVG}`), 35 | options: { 36 | limit: LIMIT, 37 | name: 'assets/[name].[hash:8].[ext]' 38 | } 39 | }, 40 | { 41 | test: /\.(bmp|png|jpg|jpeg|gif|svg|woff|woff2|eot|ttf|otf)$/, 42 | use: [ 43 | { 44 | loader: 'file-loader', 45 | options: { 46 | name: 'assets/[name].[hash:8].[ext]' 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | test: /\.md$/, 53 | use: [ 54 | { 55 | loader: 'raw-loader', 56 | options: { 57 | esModule: false 58 | } 59 | } 60 | ] 61 | } 62 | ] 63 | } 64 | ]; 65 | -------------------------------------------------------------------------------- /webpack/constants.js: -------------------------------------------------------------------------------- 1 | const { getSource } = require('./helpers/source'); 2 | 3 | const bindNodeModulesPrefix = (name) => getSource(`node_modules/${name}`); 4 | 5 | const OPENPGP_FILES = Object.fromEntries( 6 | Object.entries({ 7 | main: 'openpgp/dist/lightweight/openpgp.min.js', 8 | elliptic: 'openpgp/dist/lightweight/elliptic.min.js', 9 | worker: 'openpgp/dist/lightweight/openpgp.worker.min.js', 10 | compat: 'openpgp/dist/compat/openpgp.min.js' 11 | }).map(([k, v]) => [k, bindNodeModulesPrefix(v)]) 12 | ); 13 | 14 | const BABEL_INCLUDE_NODE_MODULES = [ 15 | 'asmcrypto.js', 16 | 'pmcrypto', 17 | 'proton-pack', 18 | 'proton-shared', 19 | 'mutex-browser', 20 | 'interval-tree', 21 | 'get-random-values', 22 | 'sieve.js', 23 | 'pm-srp', 24 | 'react-components', 25 | 'idb' 26 | ]; 27 | const BABEL_EXCLUDE_FILES = ['mailparser.js']; 28 | 29 | module.exports = { 30 | OPENPGP_FILES, 31 | BABEL_EXCLUDE_FILES, 32 | BABEL_INCLUDE_NODE_MODULES 33 | }; 34 | -------------------------------------------------------------------------------- /webpack/css.loader.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 2 | const postcssPresetEnv = require('postcss-preset-env'); 3 | const fs = require('fs'); 4 | const { getSource } = require('./helpers/source'); 5 | 6 | const DESIGN_SYSTEM_THEME = /.*theme\.scss$/; 7 | 8 | const SASS_VARIABLES_FILEPATH = getSource('src/app/variables.scss'); 9 | const SASS_VARIABLES = fs.existsSync(SASS_VARIABLES_FILEPATH) ? fs.readFileSync(SASS_VARIABLES_FILEPATH) : ''; 10 | // Set up the variables to the design system so that files are resolved properly. 11 | const PREPEND_SASS = ` 12 | $path-images: "~design-system/assets/img/shared/"; 13 | ${SASS_VARIABLES} 14 | `; 15 | 16 | const handleUrlResolve = (url) => { 17 | // Transparent image, included through write 18 | if (url.includes('host.png')) { 19 | return false; 20 | } 21 | return true; 22 | }; 23 | 24 | const getSassLoaders = (isProduction) => { 25 | const postcssPlugins = isProduction 26 | ? [ 27 | postcssPresetEnv({ 28 | autoprefixer: { 29 | flexbox: 'no-2009' 30 | }, 31 | stage: 3 32 | }) 33 | ] 34 | : []; 35 | 36 | return [ 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | url: handleUrlResolve 41 | } 42 | }, 43 | // To get rid of "You did not set any plugins, parser, or stringifier. Right now, PostCSS does nothing." 44 | postcssPlugins.length 45 | ? { 46 | loader: 'postcss-loader', 47 | options: { 48 | ident: 'postcss', 49 | plugins: postcssPlugins, 50 | sourceMap: isProduction 51 | } 52 | } 53 | : undefined, 54 | { 55 | loader: 'sass-loader', 56 | options: { 57 | additionalData: PREPEND_SASS 58 | } 59 | } 60 | ].filter(Boolean); 61 | }; 62 | 63 | module.exports = ({ isProduction }) => { 64 | const sassLoaders = getSassLoaders(isProduction); 65 | const miniLoader = { 66 | loader: MiniCssExtractPlugin.loader, 67 | options: { 68 | hmr: !isProduction 69 | } 70 | }; 71 | return [ 72 | { 73 | test: /\.css$/, 74 | use: [ 75 | miniLoader, 76 | { 77 | loader: 'css-loader', 78 | options: { 79 | importLoaders: 1, 80 | url: handleUrlResolve 81 | } 82 | } 83 | ], 84 | sideEffects: true 85 | }, 86 | { 87 | test: /\.scss$/, 88 | exclude: DESIGN_SYSTEM_THEME, 89 | use: [miniLoader, ...sassLoaders], 90 | sideEffects: true 91 | }, 92 | { 93 | test: DESIGN_SYSTEM_THEME, 94 | // Prevent loading the theme in