├── .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 |
--------------------------------------------------------------------------------