├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintignore ├── .stylelintrc ├── LICENSE ├── Readme.md ├── package.json ├── postcss.config.cjs ├── src ├── .eslintrc ├── globals.d.ts ├── index.html └── index.tsx ├── tsconfig.json ├── utils └── customTemplate │ ├── index.js │ └── package.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-react", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ], 9 | "@babel/preset-env", 10 | "@babel/preset-typescript" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # define set of browsers you want to support 2 | defaults -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "env": { 4 | "node": true 5 | }, 6 | "root": true, 7 | "parserOptions": { 8 | "ecmaVersion": "latest", 9 | "sourceType": "module" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dist/ 3 | node_modules/ 4 | ngrok.json 5 | *.log 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twa-dev/webpack-boilerplate/24ec3447b344b80600e707e8975e0fb4e6b16557/.prettierrc -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-recommended", "stylelint-config-prettier"], 3 | "rules": { 4 | "selector-class-pattern": "^[a-z][a-zA-Z0-9]+$" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Artur Stambultsian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Boilerplate 2 | 3 | If you want to start a new project and fully control your infrastructure, you can use our Boilerplate. 4 | 5 | ## Quick start 6 | 7 | ```bash 8 | - git clone git@github.com:twa-dev/Boilerplate.git {projectName} 9 | - cd {projectName} 10 | - yarn install 11 | - yarn start 12 | ``` 13 | 14 | ## Documentation 15 | 16 | This project solves all the famous problems you gonna face if you develop TWA. 17 | 18 | ### Development 19 | 20 | `yarn start` starts dev server on 9000 port. It optionally includes ngrok proxy. Ngrok allows you instantly deploy the app 21 | to the internet. It can be useful if you want to see how your application works inside Telegram. 22 | 23 | ### Build 24 | 25 | `yarn build` creates an optimized production-ready bundle inside `dist` directory. 26 | 27 | ### Bundle analyze 28 | 29 | `yarn analyze` shows you what your js-bundle consists of. Use this utility if you have performance issues. 30 | 31 | ### Static analyze 32 | 33 | - `yarn test:eslint` and `yarn test:stylelint` runs linters; 34 | - `yarn tsc` runs type check; 35 | - `yarn prettier` runs prettier (it will check your formatting but won't change anything) 36 | 37 | If you work in a team it's important to keep a consistent code style. Include these commands in your CI process to prevent code base inconsistency. 38 | 39 | ## Technologies 40 | 41 | Boilerplate uses Webpack, CSS Modules, TypeScript, React, [@twa-dev/sdk](https://github.com/twa-dev/sdk), [@twa-dev/mark42](https://github.com/twa-dev/Mark42), eslint, stylelint, prettier and ngrok. 42 | 43 | All these tools help you to deliver your features fast and confidently. 44 | 45 | ### SVG 46 | 47 | There are two different ways to import SVG: as a component and as an asset. If you want to use SVG as a component, 48 | you have to add "component." before an extension. Example: 49 | 50 | ```tsx 51 | import diamond from "./diamond.svg"; 52 | import Diamond from "./diamond.component.svg"; 53 | 54 | 55 | 56 | ``` 57 | 58 | ### CSS Modules 59 | 60 | All `*.module.css` files are treated as isolated [CSS Modules](https://github.com/css-modules/css-modules). It means that you don't have to worry about class name collisions. 61 | Moreover, this CSS is typed, so it's impossible to use unexisting class names in `.tsx`. Boilerplate uses named exports for CSS to improve TS checks. 62 | 63 | ### Ngrok 64 | 65 | [Ngrok](https://ngrok.com/) is a powerful tool for creating tunnel between your computer and the internet. It's 66 | already built in `yarn start` command. To enable Ngrok you need to create `ngrok.json` in the root of your project. 67 | 68 | **Important:** you have to buy Ngrok subscription if you want to register fixed subdomain names. 69 | 70 | Example of `ngrok.json`: 71 | 72 | ```json 73 | { 74 | "authtoken": "abcd", 75 | "subdomain": "{subdomain}", 76 | "region": "{region}" 77 | } 78 | ``` 79 | 80 | Now if you run `yarn start`, your local version of TWA will be available on _https://{subdomain}.{region}.ngrok.io/_. That's it. All you need to do is to [set](https://core.telegram.org/bots/webapps#launching-web-apps-from-the-menu-button) this URL as Menu Button URL. 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twa-dev/boilerplate", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "@twa-dev/mark42": "^0.0.37", 8 | "@twa-dev/sdk": "^6.7.1", 9 | "autoprefixer": "^10.4.12", 10 | "cross-env": "^7.0.3", 11 | "open": "^8.4.0", 12 | "postcss": "^8.4.17", 13 | "postcss-loader": "^7.0.1", 14 | "@babel/core": "^7.18.10", 15 | "@babel/preset-env": "^7.18.10", 16 | "@babel/preset-react": "^7.18.6", 17 | "@babel/preset-typescript": "^7.18.6", 18 | "@types/react": "^18.0.17", 19 | "@types/react-dom": "^18.0.6", 20 | "@typescript-eslint/eslint-plugin": "^5.36.2", 21 | "@typescript-eslint/parser": "^5.36.2", 22 | "babel-loader": "^8.2.5", 23 | "css-loader": "^6.7.1", 24 | "eslint": "^8.22.0", 25 | "eslint-config-prettier": "^8.5.0", 26 | "eslint-plugin-react": "^7.31.8", 27 | "eslint-webpack-plugin": "^3.2.0", 28 | "html-inline-script-webpack-plugin": "^3.1.0", 29 | "html-webpack-plugin": "^5.5.0", 30 | "mini-css-extract-plugin": "^2.6.1", 31 | "ngrok": "^4.3.3", 32 | "prettier": "^2.7.1", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0", 35 | "style-loader": "^3.3.1", 36 | "stylelint": "^14.11.0", 37 | "stylelint-config-prettier": "^9.0.3", 38 | "stylelint-config-recommended": "^9.0.0", 39 | "stylelint-webpack-plugin": "^3.3.0", 40 | "svg-react-loader": "^0.4.6", 41 | "svgo-loader": "^3.0.1", 42 | "typescript": "^4.8.3", 43 | "typescript-plugin-css-modules": "^3.4.0", 44 | "webpack": "^5.74.0", 45 | "webpack-bundle-analyzer": "^4.6.1", 46 | "webpack-cli": "^4.10.0", 47 | "webpack-dev-server": "^4.10.0" 48 | }, 49 | "scripts": { 50 | "start": "cross-env NODE_ENV=development webpack serve", 51 | "build": "webpack", 52 | "analyze": "webpack --analyze", 53 | "test:eslint": "eslint --ext .ts,.tsx,.js \"./\"", 54 | "test:stylelint": "stylelint \"src/**/*.css\"", 55 | "test:tsc": "tsc", 56 | "test:prettier": "prettier --check" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const autoprefixer = require("autoprefixer"); 2 | 3 | module.exports = { 4 | plugins: [autoprefixer], 5 | }; 6 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@typescript-eslint/recommended", 4 | "plugin:react/recommended", 5 | "prettier" 6 | ], 7 | "parser": "@typescript-eslint/parser", 8 | "settings": { 9 | "react": { 10 | "version": "detect" 11 | } 12 | }, 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | } 17 | }, 18 | "env": { 19 | "node": false, 20 | "browser": true 21 | }, 22 | "rules": { 23 | "react/react-in-jsx-scope": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "*.jpg" { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module "*.gif" { 12 | const value: string; 13 | export default value; 14 | } 15 | 16 | declare module "*.svg" { 17 | const value: string; 18 | export default value; 19 | } 20 | 21 | declare module "*.ttf" { 22 | const value: string; 23 | export default value; 24 | } 25 | 26 | declare module "*.woff" { 27 | const value: string; 28 | export default value; 29 | } 30 | 31 | declare module "*.woff2" { 32 | const value: string; 33 | export default value; 34 | } 35 | 36 | declare module "*.json" { 37 | const value: Record; 38 | export default value; 39 | } 40 | 41 | declare module "*.module.css"; 42 | 43 | declare module "*.component.svg" { 44 | import { FC, SVGProps } from "react"; 45 | const ReactComponent: FC>; 46 | export default ReactComponent; 47 | } 48 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { AppearanceProvider } from "@twa-dev/mark42"; 3 | 4 | const root = createRoot(document.getElementById("root") as HTMLElement); 5 | 6 | root.render(); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es2015", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "strict": true, 7 | "module": "esnext", 8 | "moduleResolution": "Node", 9 | "isolatedModules": true, 10 | "jsx": "react-jsx", 11 | "noEmit": true, 12 | "plugins": [ 13 | { 14 | "name": "typescript-plugin-css-modules", 15 | "options": { 16 | "customTemplate": "./utils/customTemplate/index.js" 17 | } 18 | } 19 | ] 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /utils/customTemplate/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (dts, { classes }) => { 2 | return Object.keys(classes) 3 | .map((key) => `export const ${key}: string`) 4 | .join("\n"); 5 | }; 6 | -------------------------------------------------------------------------------- /utils/customTemplate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import HTMLWebpackPlugin from "html-webpack-plugin"; 2 | import StylelintWebpackPlugin from "stylelint-webpack-plugin"; 3 | import EslintWebpackPlugin from "eslint-webpack-plugin"; 4 | import MiniCssExtractPlugin from "mini-css-extract-plugin"; 5 | import HtmlInlineScriptPlugin from "html-inline-script-webpack-plugin"; 6 | import path from "path"; 7 | import ngrok from "ngrok"; 8 | import { fileURLToPath } from "url"; 9 | import { createRequire } from "module"; 10 | import { readFile } from "fs/promises"; 11 | import open from "open"; 12 | 13 | const require = createRequire(import.meta.url); 14 | const { name } = require("./package.json"); 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = path.dirname(__filename); 17 | const isDev = process.env.NODE_ENV === "development"; 18 | 19 | const config = { 20 | mode: isDev ? "development" : "production", 21 | entry: "./src/index.tsx", 22 | output: { 23 | publicPath: "", 24 | filename: isDev ? "[name].js" : "[contenthash:6].[name].js", 25 | assetModuleFilename: "images/[contenthash:6].[ext]", 26 | path: path.join(__dirname, "dist"), 27 | clean: true, 28 | }, 29 | module: { 30 | strictExportPresence: true, 31 | rules: [ 32 | { 33 | test: /\.m?js/, 34 | resolve: { 35 | fullySpecified: false, 36 | }, 37 | }, 38 | { 39 | test: /\.(ts|tsx)$/, 40 | use: "babel-loader", 41 | exclude: /node_modules/, 42 | }, 43 | { 44 | test: /\.module.css$/, 45 | include: /src/, 46 | use: [isDev ? "style-loader" : MiniCssExtractPlugin.loader].concat([ 47 | { 48 | loader: "css-loader", 49 | options: { 50 | esModule: true, 51 | modules: { 52 | namedExport: true, 53 | localIdentName: "[hash:base64:4]", 54 | }, 55 | }, 56 | }, 57 | "postcss-loader", 58 | ]), 59 | }, 60 | { 61 | test: /\.css$/, 62 | exclude: /src/, 63 | use: [isDev ? "style-loader" : MiniCssExtractPlugin.loader].concat([ 64 | "css-loader", 65 | ]), 66 | }, 67 | { 68 | test: /\.component.svg$/, 69 | use: ["svg-react-loader"].concat(isDev ? [] : ["svgo-loader"]), 70 | }, 71 | { 72 | test: /\.(png|jpg|gif|woff|woff2|ttf)$/i, 73 | type: "asset", 74 | }, 75 | { 76 | test: /\.svg$/i, 77 | use: isDev ? [] : ["svgo-loader"], 78 | type: "asset", 79 | }, 80 | ], 81 | }, 82 | optimization: { 83 | splitChunks: { 84 | cacheGroups: { 85 | vendors: { 86 | chunks: "all", 87 | name: "vendors", 88 | test: /node_modules/, 89 | }, 90 | }, 91 | }, 92 | }, 93 | plugins: [ 94 | new StylelintWebpackPlugin(), 95 | new EslintWebpackPlugin({ 96 | extensions: ["ts", "tsx"], 97 | }), 98 | new HTMLWebpackPlugin({ 99 | template: "./src/index.html", 100 | title: name, 101 | }), 102 | new HtmlInlineScriptPlugin({ 103 | scriptMatchPattern: [/telegram-web-apps/], 104 | }), 105 | ], 106 | resolve: { 107 | extensions: [".ts", ".tsx", ".js"], 108 | alias: { 109 | react: path.resolve(__dirname, "./node_modules/react"), 110 | "react-dom": path.resolve(__dirname, "./node_modules/react-dom"), 111 | }, 112 | }, 113 | devServer: { 114 | allowedHosts: "all", 115 | port: 9000, 116 | client: { 117 | overlay: { 118 | errors: true, 119 | warnings: false, 120 | }, 121 | }, 122 | onListening: async function (devServer) { 123 | const port = devServer.server.address().port; 124 | try { 125 | const ngrokConfig = JSON.parse( 126 | await readFile(new URL("./ngrok.json", import.meta.url)) 127 | ); 128 | const url = await ngrok.connect({ 129 | host_header: "rewrite", 130 | proto: "http", 131 | addr: port, 132 | ...ngrokConfig, 133 | }); 134 | await open(url); 135 | } catch (e) { 136 | await open(`http://localhost:${port}`); 137 | } 138 | }, 139 | }, 140 | }; 141 | 142 | if (!isDev) { 143 | config.plugins.push( 144 | new MiniCssExtractPlugin({ 145 | filename: isDev ? "[name].js" : "[contenthash:6].[name].css", 146 | }) 147 | ); 148 | } 149 | 150 | export default config; 151 | --------------------------------------------------------------------------------