├── .gitignore ├── .gitpod.yml ├── .vscode └── settings.json ├── README.md ├── copier.yml └── template ├── .devcontainer ├── Dockerfile.jinja └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── docker.yml.jinja ├── .gitignore ├── .gitpod.yml ├── .ncurc.js ├── .nvmrc.jinja ├── .prettierrc ├── .swcrc ├── .vscode └── settings.json ├── Dockerfile.chrome.jinja ├── Dockerfile.jinja ├── Makefile.jinja ├── README.md.jinja ├── assets └── fonts.conf ├── bin ├── run └── run.cmd ├── entrypoint.sh.jinja ├── jest.config.js ├── k8s └── KUBERNETES.md ├── nodemon.json ├── package.json.jinja ├── scripts ├── vnc.sh └── xvfb.sh ├── src ├── commands │ ├── COMMANDS.md │ └── dev │ │ └── logs.ts └── packlets │ ├── PACKLETS.md │ ├── browser │ ├── args.ts │ └── env.ts │ ├── database │ ├── env.ts │ ├── index.ts │ └── sql.ts │ ├── telemetry │ ├── README.md │ ├── env.ts.jinja │ ├── index.ts │ ├── logger.ts.jinja │ └── metrics.ts │ └── utils │ ├── functions │ ├── index.ts │ ├── lock.ts │ ├── random.ts │ ├── require-file.ts │ └── throw-expression.ts │ ├── index.ts │ ├── schemas │ ├── env.ts │ └── index.ts │ └── transform.js ├── tsconfig.json └── {{_copier_conf.answers_file}}.jinja /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package.json 3 | yarn.lock 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | vscode: 2 | extensions: 3 | - esbenp.prettier-vscode 4 | - dbaeumer.vscode-eslint 5 | - WakaTime.vscode-wakatime 6 | - streetsidesoftware.code-spell-checker 7 | - eamodio.gitlens 8 | - mushan.vscode-paste-image 9 | - lukashass.volar 10 | - wayou.vscode-todo-highlight 11 | - nicoespeon.abracadabra 12 | - https://marketplace.visualstudio.com/_apis/public/gallery/publishers/nick-lvov-dev/vsextensions/typescript-explicit-types/0.0.9/vspackage 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "argocd", 4 | "ARGOCD", 5 | "arkade", 6 | "Hetzner", 7 | "jsonpath", 8 | "KUBECONFIG", 9 | "kubectl", 10 | "kubeseal", 11 | "kustomization", 12 | "masternode", 13 | "noconfirm", 14 | "pacman", 15 | "pipx" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Camille's TypeScript Boilerplate 🥘 3 |

4 | 5 |

6 | This is the TypeScript base code that I use for all my open-source and private projects. 7 |

8 | 9 | --- 10 | 11 | I aim to update this boilerplate when I integrate new tools in my workflow. 12 | 13 | If you are looking for my older boilerplate template, you can find it [here](https://github.com/clouedoc/typescript-boilerplate-old). 14 | 15 | ## Features 16 | 17 | - Parse your environment variables with [Zod](https://github.com/colinhacks/zod) 18 | - Collect and extract metrics from your logs using [Loki](https://github.com/grafana/loki/) 19 | - Update your packages with [ncu](https://github.com/raineorshine/npm-check-updates) 20 | - Build a beautiful CLI UX with [oclif](https://github.com/oclif/oclif) CLI 21 | - Build reusable [Docker](https://www.docker.com/) images 22 | - Puppeteer-ready image integrated 23 | - ... and a lightweight Node-based one is included too. 24 | - containerized development thanks to DevContainers 25 | - CI with [GitHub Actions](https://docs.github.com/en/actions) 26 | - CD with Kubernetes and ArgoCD 27 | - Package patching thanks to [patch-package](https://github.com/ds300/patch-package) 28 | - Witty ESLint configuration to keep your code clean 29 | - Stay in the loop thanks to smart template updates 30 | 31 | ## Usage 32 | 33 | ```bash 34 | pip install pipx && pipx ensurepath && . ~/.bashrc # pipx installation 35 | pipx install copier # copier installation 36 | 37 | copier gh:clouedoc/typescript-boilerplate-evolved your_new_project 38 | 39 | cd your_new_project 40 | yarn install && yarn build 41 | yarn dev # all good, start programming now! 42 | ``` 43 | 44 | ### Fetching updates 45 | 46 | To apply the latest version of this boilerplate to your project, run the following commands: 47 | 48 | ```bash 49 | git status # should be clean 50 | copier update # this will fetch the latest release 51 | # If you want to stay on the edge: 52 | # copier update --vcs-ref=HEAD 53 | ``` 54 | 55 | **Do not manually edit the .copier-answers.yml file. This will confuse Copier's update algorithm.** 56 | 57 | You may find `*.ref` files that you need to resolve manually. 58 | 59 | ## License 60 | 61 | This code is released under the MIT License 62 | 63 | ## Contributing 64 | 65 | Contributions are what make the open source community such an amazing place to be, learn, inspire, and create. Any contributions you make are greatly appreciated! 66 | 67 | ## References 68 | 69 | - [Jinja templates reference](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures) 70 | - [Copier documentation](https://copier.readthedocs.io/en/stable/) 71 | -------------------------------------------------------------------------------- /copier.yml: -------------------------------------------------------------------------------- 1 | # config 2 | _subdirectory: template 3 | 4 | # questions 5 | project_name: 6 | type: str 7 | help: What is your project name? 8 | placeholder: "awesome-project" 9 | 10 | project_slug: 11 | type: str 12 | help: What is your project's slug? 13 | default: "{{ project_name.lower().replace(' ', '-').replace('_', '-') }}" 14 | 15 | full_name: 16 | type: str 17 | help: What is your full name? 18 | placeholder: Camille Louédoc-Eyries 19 | 20 | github_username: 21 | type: str 22 | help: What is your GitHub username? 23 | placeholder: clouedoc 24 | 25 | project_short_description: 26 | type: str 27 | help: What is your project about? 28 | placeholder: My awesome project does X with Y. 29 | 30 | package_private: 31 | type: bool 32 | help: Do you wish to make your package private? 33 | default: yes 34 | 35 | binary_name: 36 | type: str 37 | help: The name of your CLI command 38 | default: "{{ project_slug }}" 39 | 40 | project_emoji: 41 | type: str 42 | help: Pick an emoji which describes best your project. 43 | placeholder: ✨ 44 | 45 | email: 46 | type: str 47 | help: What is your email? 48 | placeholder: camille.eyries@gmail.com 49 | 50 | node_version: 51 | type: str 52 | help: Choose a Node version 53 | choices: 54 | - [Node 16, "16"] 55 | - [Node 18, "18"] 56 | 57 | project_url: 58 | type: str 59 | help: Your repository URL 60 | default: "https://github.com/{{ github_username }}/{{ project_slug }}" 61 | 62 | project_url_git: 63 | type: str 64 | help: Your project's git URL 65 | default: "{{project_url}}.git" 66 | 67 | docker_image_name: 68 | type: str 69 | help: Your Docker image name 70 | default: "ghcr.io/{{github_username}}/{{project_slug}}" 71 | 72 | use_doppler: 73 | type: bool 74 | help: Do you want to use Doppler (tool to manage environment variables in the cloud)? 75 | default: false 76 | 77 | project_license: 78 | type: str 79 | help: Pick a license for your project. 80 | default: MIT 81 | -------------------------------------------------------------------------------- /template/.devcontainer/Dockerfile.jinja: -------------------------------------------------------------------------------- 1 | FROM {{ docker_image_name }}-chrome:main 2 | 3 | USER root 4 | 5 | RUN apt update 6 | RUN apt install -y zsh ssh tig sudo tmux 7 | RUN npm install -g sort-package-json 8 | RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 9 | 10 | RUN echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers 11 | RUN useradd -m -s /bin/zsh -u 1000 -U dev && groupadd wheel && usermod -aG wheel dev 12 | 13 | RUN rm -rf /app 14 | 15 | USER dev 16 | 17 | # add $(yarn global bin) to the path (zshrc) 18 | RUN echo "export PATH=\$PATH:$(yarn global bin)" >> ~/.zshrc 19 | 20 | RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 21 | 22 | ENV NODE_ENV=development 23 | 24 | RUN git config --global --add safe.directory /workspaces/ 25 | RUN git config --global --add safe.directory /workspaces/{{project_slug}} 26 | 27 | COPY .wakatime.cfg /home/dev 28 | -------------------------------------------------------------------------------- /template/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { "dockerfile": "./Dockerfile", "context": ".." }, 3 | "postCreateCommand": "yarn install && yarn link", 4 | "postStartCommand": "tmux new -d '. ./scripts/xvfb.sh && . ./scripts/vnc.sh && sleep 360d'", 5 | "postAttachCommand": "yarn dev", 6 | "shutdownAction": "stopContainer", 7 | "settings": { 8 | "terminal.integrated.defaultProfile.linux": "zsh", 9 | // copy git configuration 10 | "remote.containers.gitCredentialHelperConfigLocation": "global", 11 | "remote.containers.copyGitConfig": true 12 | }, 13 | "forwardPorts": [5900], 14 | 15 | "extensions": [ 16 | "esbenp.prettier-vscode", 17 | "dbaeumer.vscode-eslint", 18 | "WakaTime.vscode-wakatime", 19 | "streetsidesoftware.code-spell-checker", 20 | "streetsidesoftware.code-spell-checker-french", 21 | "eamodio.gitlens", 22 | "mushan.vscode-paste-image", 23 | "lukashass.volar", 24 | "wayou.vscode-todo-highlight", 25 | "nicoespeon.abracadabra", 26 | "nick-lvov-dev.typescript-explicit-types", 27 | "GitHub.copilot", 28 | "GitHub.copilot-labs" 29 | ], 30 | "containerEnv": { 31 | "DISPLAY": ":99" 32 | }, 33 | "runArgs": ["--privileged", "--shm-size=1gb"] // for Chrome to work without needing extra arguments 34 | } 35 | -------------------------------------------------------------------------------- /template/.dockerignore: -------------------------------------------------------------------------------- 1 | # Build Files 2 | node_modules/ 3 | build/ 4 | lib/ 5 | 6 | # Editor preferences 7 | .editorconfig 8 | .dockerignore 9 | .git/ 10 | .no-git/ 11 | .vscode/ 12 | 13 | *.log 14 | 15 | Dockerfile 16 | .dockerignore 17 | Makefile -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.ts] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /template/.eslintignore: -------------------------------------------------------------------------------- 1 | npm node_modules 2 | build 3 | jest.config.js 4 | .eslintrc.js -------------------------------------------------------------------------------- /template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This is a workaround for https://github.com/eslint/eslint/issues/3458 2 | require('@rushstack/eslint-config/patch/modern-module-resolution'); 3 | 4 | var isDev = process.env.NODE_ENV === 'development'; 5 | 6 | /** 7 | * @type {import("eslint").Linter.Config} 8 | */ 9 | const config = { 10 | root: true, 11 | extends: [ 12 | '@rushstack/eslint-config/profile/node', 13 | '@rushstack/eslint-config/mixins/friendly-locals', 14 | '@rushstack/eslint-config/mixins/packlets', 15 | ], // <---- put your profile string here 16 | env: { 17 | es6: true, 18 | }, 19 | ignorePatterns: [], 20 | rules: { 21 | // This rule reduces performance by 84%, so only enabled during CI. 22 | '@typescript-eslint/no-floating-promises': isDev ? 'off' : 'error', 23 | '@typescript-eslint/no-invalid-this': 'error', 24 | 'no-console': 'warn', 25 | }, 26 | overrides: [ 27 | { 28 | files: ['**/*.{ts,js}'], 29 | /** 30 | * TypeScript configuration 31 | */ 32 | parser: '@typescript-eslint/parser', 33 | parserOptions: { 34 | tsconfigRootDir: __dirname, 35 | project: './tsconfig.json', 36 | }, 37 | }, 38 | { 39 | files: ['src/packlets/scripts/*', '*.spec.ts'], 40 | rules: { 41 | '@rushstack/packlets/mechanics': 0, 42 | }, 43 | }, 44 | { 45 | files: ['**/schemas/*.ts', '**/env.ts'], 46 | rules: { 47 | '@typescript-eslint/typedef': 0, 48 | }, 49 | }, 50 | { 51 | files: ['src/commands/**'], 52 | rules: { 53 | '@typescript-eslint/typedef': 0, 54 | '@typescript-eslint/explicit-member-accessibility': 0, 55 | }, 56 | }, 57 | ], 58 | }; 59 | 60 | module.exports = config; 61 | -------------------------------------------------------------------------------- /template/.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docker/**' 7 | - '.github/**' 8 | - 'k8s/**' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | run: 13 | name: Build & lint 14 | runs-on: ubuntu-20.04 15 | env: 16 | NODE_ENV: development 17 | CI: true 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 # checkout full history 23 | - name: Setup NodeJS 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version-file: '.nvmrc' 27 | cache: 'yarn' 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | - name: Build 31 | run: yarn build 32 | - name: Lint 33 | run: yarn lint 34 | - name: Test 35 | run: yarn test --passWithNoTests 36 | - name: Check circular dependencies 37 | run: yarn detect-circular-dependencies 38 | -------------------------------------------------------------------------------- /template/.github/workflows/docker.yml.jinja: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'k8s/**' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-matrix: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: # build Dockerfile and Dockerfile.chrome under the names {{project_slug}} and {{project_slug}}-chrome 14 | include: 15 | - dockerfile: Dockerfile 16 | suffix: '' 17 | - dockerfile: Dockerfile.chrome 18 | suffix: -chrome 19 | name: {{ project_slug }}{{ '${{ matrix.suffix }}' }} 20 | env: 21 | IMAGE_NAME: {{docker_image_name}}{{ '${{ matrix.suffix }}' }} 22 | steps: 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v2 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | - name: Login to GitHub Container registry 28 | uses: docker/login-action@v2 29 | with: 30 | registry: ghcr.io 31 | username: {{ '${{ github.actor }}' }} 32 | password: {{ '${{ secrets.GITHUB_TOKEN }}' }} 33 | - name: Checkout 34 | uses: actions/checkout@v3 35 | - name: Docker meta 36 | id: meta 37 | uses: docker/metadata-action@v4 38 | with: 39 | images: | 40 | {{ '${{ env.IMAGE_NAME }}' }} 41 | tags: | 42 | type=schedule 43 | type=ref,event=branch 44 | type=ref,event=pr 45 | type=semver,pattern={{version}} 46 | type=semver,pattern={{major}}.{{minor}} 47 | type=semver,pattern={{major}} 48 | type=sha 49 | - name: Build and push 50 | uses: docker/build-push-action@v3 51 | with: 52 | push: true 53 | file: {{ '${{ matrix.dockerfile }}' }} 54 | tags: {{ '${{ steps.meta.outputs.tags }}' }} 55 | labels: {{ '${{ steps.meta.outputs.labels }}' }} 56 | cache-from: type=registry,ref={{ '${{ env.IMAGE_NAME }}' }}:buildcache 57 | cache-to: type=registry,ref={{ '${{ env.IMAGE_NAME }}' }}:buildcache,mode=max 58 | # uncomment the following if using Kubernetes 59 | # - uses: tale/kubectl-action@v1 60 | # with: 61 | # base64-kube-config: {{ '${{ secrets.KUBE_CONFIG }}' }} 62 | # kubectl-version: v1.24.3 63 | # - name: deploy {{project_slug}}-chrome 64 | # if: {{ '${{ matrix.suffix }}' }} == '-chrome' 65 | # run: | 66 | # kubectl rollout restart deploy my-awesome-chrome-service -n my-namespace 67 | # - name: deploy {{project_slug}} 68 | # if: {{ '${{ matrix.suffix }}' }} == '' 69 | # run: | 70 | # kubectl rollout restart deploy my-awesome-nonchrome-service -n my-namespace 71 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | # Secrets 2 | .env 3 | 4 | # Dev files 5 | node_modules 6 | lib 7 | 8 | # OSX 9 | .DS_Store 10 | 11 | # Yarn 12 | package-lock.json 13 | yarn-error.log 14 | 15 | .wakatime.cfg -------------------------------------------------------------------------------- /template/.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn install 3 | && yarn build 4 | && yarn link 5 | && echo "export PATH=$PATH:$(yarn global bin)" >> ~/.bashrc 6 | command: yarn dev 7 | 8 | vscode: 9 | extensions: 10 | - esbenp.prettier-vscode 11 | - dbaeumer.vscode-eslint 12 | - WakaTime.vscode-wakatime 13 | - streetsidesoftware.code-spell-checker 14 | - eamodio.gitlens 15 | - mushan.vscode-paste-image 16 | - lukashass.volar 17 | - wayou.vscode-todo-highlight 18 | - nicoespeon.abracadabra 19 | - https://marketplace.visualstudio.com/_apis/public/gallery/publishers/nick-lvov-dev/vsextensions/typescript-explicit-types/0.0.9/vspackage 20 | -------------------------------------------------------------------------------- /template/.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | upgrade: true, 3 | reject: ['puppeteer', '@types/puppeteer', '@types/node', 'got', 'execa'], 4 | loglevel: 'error', 5 | errorLevel: 1, 6 | }; 7 | -------------------------------------------------------------------------------- /template/.nvmrc.jinja: -------------------------------------------------------------------------------- 1 | {{node_version}} 2 | -------------------------------------------------------------------------------- /template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "useTabs": false, 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /template/.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "jsc": { 4 | "parser": { 5 | "syntax": "typescript" 6 | }, 7 | "target": "es2020" 8 | }, 9 | "module": { 10 | "type": "commonjs" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /template/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/.git/objects/**": true, 4 | "**/.git/subtree-cache/**": true, 5 | "**/node_modules/*/**": true, 6 | "**/.hg/store/**": true, 7 | "**/lib/**": true 8 | }, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | "files.exclude": { 11 | "**/.git": true, 12 | "**/.svn": true, 13 | "**/.hg": true, 14 | "**/CVS": true, 15 | "**/.DS_Store": true, 16 | "**/Thumbs.db": true, 17 | "**/lib": true, 18 | "**/mode_modules": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /template/Dockerfile.chrome.jinja: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 as base 2 | 3 | COPY ./assets/fonts.conf /etc/fonts/local.conf 4 | RUN apt-get -qq update && apt-get -qq dist-upgrade 5 | RUN apt-get -y -qq install software-properties-common && \ 6 | apt-add-repository "deb http://archive.canonical.com/ubuntu $(lsb_release -sc) partner" 7 | RUN echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections 8 | RUN apt-get -y -qq --no-install-recommends install \ 9 | dumb-init \ 10 | git \ 11 | ffmpeg \ 12 | fonts-liberation \ 13 | msttcorefonts \ 14 | fonts-roboto \ 15 | fonts-ubuntu \ 16 | fonts-noto-color-emoji \ 17 | fonts-noto-cjk \ 18 | fonts-ipafont-gothic \ 19 | fonts-wqy-zenhei \ 20 | fonts-kacst \ 21 | fonts-freefont-ttf \ 22 | fonts-thai-tlwg \ 23 | fonts-indic \ 24 | fontconfig \ 25 | libappindicator3-1 \ 26 | pdftk \ 27 | unzip \ 28 | locales \ 29 | gconf-service \ 30 | libasound2 \ 31 | libatk-bridge2.0-0 \ 32 | libatk1.0-0 \ 33 | libc6 \ 34 | libcairo2 \ 35 | libcups2 \ 36 | libdbus-1-3 \ 37 | libexpat1 \ 38 | libfontconfig1 \ 39 | libgbm1 \ 40 | libgcc1 \ 41 | libgconf-2-4 \ 42 | libgdk-pixbuf2.0-0 \ 43 | libglib2.0-0 \ 44 | libgtk-3-0 \ 45 | libnspr4 \ 46 | libpango-1.0-0 \ 47 | libpangocairo-1.0-0 \ 48 | libstdc++6 \ 49 | libx11-6 \ 50 | libx11-xcb1 \ 51 | libxcb1 \ 52 | libxcomposite1 \ 53 | libxcursor1 \ 54 | libxdamage1 \ 55 | libxext6 \ 56 | libxfixes3 \ 57 | libxi6 \ 58 | libxrandr2 \ 59 | libxrender1 \ 60 | libxss1 \ 61 | libxtst6 \ 62 | libgbm-dev \ 63 | ca-certificates \ 64 | libappindicator1 \ 65 | libnss3 \ 66 | lsb-release \ 67 | xdg-utils \ 68 | wget \ 69 | xvfb \ 70 | curl \ 71 | build-essential 72 | 73 | # Install NodeJS 74 | RUN curl --silent --location https://deb.nodesource.com/setup_{{ node_version }}.x | bash - && \ 75 | apt-get -qq install nodejs && \ 76 | npm install -g npm@latest && \ 77 | npm install -g yarn 78 | 79 | ARG BLESS_USER_ID=999 80 | ENV APP_DIR=/app 81 | ENV CHROME_PATH=/usr/bin/google-chrome 82 | ENV LANG="C.UTF-8" 83 | ENV NODE_ENV=production 84 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 85 | 86 | LABEL org.opencontainers.image.source {{ project_url }} 87 | 88 | RUN groupadd -r blessuser && useradd --uid ${BLESS_USER_ID} -r -g blessuser -G audio,video blessuser && \ 89 | mkdir -p /home/blessuser/Downloads && \ 90 | chown -R blessuser:blessuser /home/blessuser 91 | 92 | # ==================== BUILDER ====================== 93 | 94 | FROM base as builder 95 | 96 | WORKDIR ${APP_DIR} 97 | ENV NODE_ENV=development 98 | # App build 99 | COPY package.json yarn.lock ./ 100 | RUN yarn install 101 | COPY ./tsconfig.json . 102 | COPY ./src ./src 103 | COPY .swcrc . 104 | RUN yarn build 105 | RUN npm prune --production 106 | 107 | # ================ RUNNER ================== 108 | FROM base as runner 109 | 110 | RUN apt-get update && apt-get install -y x11vnc fluxbox 111 | 112 | # --- Chrome installation 113 | RUN if [ "$(dpkg --print-architecture)" != "amd64" ]; then echo "Chrome Stable is only available for amd64" && exit 1; fi 114 | RUN cd /tmp &&\ 115 | wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb &&\ 116 | dpkg -i google-chrome-stable_current_amd64.deb; 117 | # Cleanup 118 | RUN fc-cache -f -v && \ 119 | apt-get -qq clean && \ 120 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 121 | 122 | WORKDIR $APP_DIR 123 | RUN chown -R blessuser:blessuser $APP_DIR 124 | USER blessuser 125 | 126 | {% if use_doppler %} 127 | # Install Doppler CLI 128 | RUN wget -q -t3 'https://packages.doppler.com/public/cli/rsa.8004D9FF50437357.key' -O /etc/apk/keys/cli@doppler-8004D9FF50437357.rsa.pub && \ 129 | echo 'https://packages.doppler.com/public/cli/alpine/any-version/main' | tee -a /etc/apk/repositories && \ 130 | apk add doppler 131 | {% endif %} 132 | 133 | COPY --from=builder --chown=blessuser:blessuser ${APP_DIR}/lib/ ./lib 134 | COPY --from=builder --chown=blessuser:blessuser ${APP_DIR}/node_modules/ ./node_modules 135 | COPY --chown=blessuser:blessuser package.json . 136 | COPY --chown=blessuser:blessuser oclif.manifest.json . 137 | COPY --chown=blessuser:blessuser bin ./bin 138 | COPY --chown=blessuser:blessuser scripts ./scripts 139 | 140 | RUN yarn link && echo "export PATH=$PATH:/home/blessuser/.yarn/bin" >> /home/blessuser/.bashrc 141 | 142 | {% if use_doppler %} 143 | ENTRYPOINT ["doppler", "run", "--", "/bin/sh"] 144 | {% else %} 145 | ENTRYPOINT ["/bin/sh"] 146 | {% endif %} -------------------------------------------------------------------------------- /template/Dockerfile.jinja: -------------------------------------------------------------------------------- 1 | FROM node:{{ node_version }}-alpine as builder 2 | 3 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 4 | 5 | WORKDIR /app 6 | 7 | COPY package.json yarn.lock /app/ 8 | RUN yarn install 9 | 10 | COPY . /app/ 11 | RUN yarn build 12 | 13 | FROM node:{{ node_version }}-alpine as runner 14 | 15 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true 16 | 17 | {% if use_doppler %} 18 | # Install Doppler CLI 19 | RUN wget -q -t3 'https://packages.doppler.com/public/cli/rsa.8004D9FF50437357.key' -O /etc/apk/keys/cli@doppler-8004D9FF50437357.rsa.pub && \ 20 | echo 'https://packages.doppler.com/public/cli/alpine/any-version/main' | tee -a /etc/apk/repositories && \ 21 | apk add doppler 22 | {% endif %} 23 | 24 | COPY --from=builder /app/ /app/ 25 | 26 | WORKDIR /app/ 27 | 28 | # Link binary name 29 | RUN yarn link 30 | RUN npm prune --production 31 | 32 | {% if use_doppler %} 33 | ENTRYPOINT [ "doppler", "run", "--", "./bin/run" ] 34 | {% else %} 35 | ENTRYPOINT [ "./bin/run" ] 36 | {% endif %} 37 | -------------------------------------------------------------------------------- /template/Makefile.jinja: -------------------------------------------------------------------------------- 1 | .PHONY: docker docker-shell run-docker db-backup 2 | 3 | docker: 4 | DOCKER_BUILDKIT=1 docker build -t {{ docker_image_name }} . 5 | 6 | docker-shell: docker 7 | docker run --rm --entrypoint sh -it {{ docker_image_name }} /bin/bash 8 | 9 | docker-push: 10 | docker push {{ docker_image_name }} 11 | 12 | docker-run: docker 13 | docker run --rm -it {{ docker_image_name }} 14 | 15 | # Backup the database to backup.sql 16 | db-backup: 17 | pg_dump $POSTGRES_URL --verbose --format=p --file backup.sql -------------------------------------------------------------------------------- /template/README.md.jinja: -------------------------------------------------------------------------------- 1 |

2 | {{ project_name }} {{ project_emoji }} 3 |

4 | 5 |

6 | {{ project_short_description }} 7 |

8 | 9 | ## Installation 10 | 11 | ```bash 12 | git clone {{ project_url }}.git 13 | cd {{ project_slug }} 14 | yarn install && yarn link 15 | 16 | # to be able to call the CLI, you must add the result of `yarn global bin` to your $PATH 17 | # e.g. echo "export PATH=$PATH:$(yarn global bin)" >> ~/.zshrc 18 | {{ binary_name }} # see CLI usage documentation below 19 | ``` 20 | 21 | ## Development 22 | 23 | ```bash 24 | git clone {{ project_url_git }}.git 25 | cd {{ project_slug }} 26 | yarn install 27 | 28 | # Here are the available scripts: 29 | yarn ncu # check for package updates 30 | yarn sort # sort package.json 31 | yarn detect-circular-dependencies # detect circular deps with Madge 32 | yarn lint # run ESLint 33 | yarn prettier # prettify all files 34 | yarn test # run Jest tests 35 | yarn dev # run Nodemon (build everything!) 36 | ``` 37 | 38 | ## CLI 39 | 40 | ### Usage 41 | 42 | 43 | 44 | 45 | ## Commands 46 | 47 | 48 | 49 | 50 | ## License 51 | 52 | This project is published under the {{ project_license }} license. 53 | 54 | ## Contributing 55 | 56 | Contributions are what make the open source community such an amazing place to be, learn, inspire, and create. Any contributions you make are greatly appreciated! 57 | -------------------------------------------------------------------------------- /template/assets/fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | serif 7 | Ubuntu Condensed 8 | 9 | 10 | sans-serif 11 | Ubuntu 12 | 13 | 14 | sans 15 | Ubuntu 16 | 17 | 18 | monospace 19 | Ubuntu Monospace 20 | 21 | -------------------------------------------------------------------------------- /template/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command') 4 | .run() 5 | .then(require('@oclif/command/flush')) 6 | .catch(require('@oclif/errors/handle')); 7 | -------------------------------------------------------------------------------- /template/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --no-deprecation "%~dp0\run" %* -------------------------------------------------------------------------------- /template/entrypoint.sh.jinja: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # When docker restarts, this file is still there, 5 | # so we need to kill it just in case 6 | [ -f /tmp/.X99-lock ] && rm -f /tmp/.X99-lock 7 | 8 | _kill_procs() { 9 | kill -TERM $node 10 | kill -TERM $xvfb 11 | } 12 | 13 | # Relay quit commands to processes 14 | trap _kill_procs SIGTERM SIGINT 15 | 16 | Xvfb :99 -screen 0 1024x768x16 -nolisten tcp -nolisten unix & 17 | xvfb=$! 18 | 19 | export DISPLAY=:99 20 | 21 | # TODO: add docker entrypoint command here 22 | dumb-init -- {{ binary_name }} docker $@ & 23 | node=$! 24 | 25 | wait $node 26 | wait $xvfb 27 | -------------------------------------------------------------------------------- /template/jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("jest").Config} 3 | */ 4 | module.exports = { 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.(t|j)sx?$': '@swc/jest', 8 | }, 9 | transformIgnorePatterns: [ 10 | // https://stackoverflow.com/a/60730519/4564097 11 | 'node_modules/(?!chalk)', 12 | ], 13 | testTimeout: 30 * 1000, 14 | testPathIgnorePatterns: ['/lib', '/node_modules/'], 15 | watchPathIgnorePatterns: [ 16 | '/lib/', 17 | 'README.md', 18 | 'oclif.manifest.json', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /template/k8s/KUBERNETES.md: -------------------------------------------------------------------------------- 1 | # Kubernetes 2 | 3 | **NOTE: THIS IS A WORK IN PROGRESS** 4 | 5 | This guide will cover the installation and maintenance of a Kubernetes cluster running on bare-metal Hetzner servers. 6 | 7 | ## Provisioning servers 8 | 9 | The first step is to provision the servers that will be used for the Kubernetes cluster. You must pick servers that are fit for your workload. 10 | 11 | ### Server requirements 12 | 13 | - at least 4GB of memory (k3s adds a bit of overhead) 14 | - at least 2 CPU cores 15 | - Arch Linux (other distributions can work, but this guide will cover arch linux) 16 | 17 | ### Providers 18 | 19 | - [Hetzner Auctions](https://www.hetzner.com/sb) 20 | 21 | ## Installing k3s 22 | 23 | ### Installing k3s on the master node 24 | 25 | SSH into the master node and run the following command: 26 | 27 | ```bash 28 | # Install dependencies 29 | hostnamectl hostname watchtower 30 | pacman -Syu --noconfirm 31 | pacman -S wireguard-tools mosh git zsh 32 | 33 | # Set up a cozy terminal environment 34 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" 35 | curl -sLS https://dl.get-arkade.dev | sh 36 | arkade get helm 37 | cp ~/.arkade/bin/helm /usr/local/bin/ 38 | arkade get kubectl 39 | mv ~/.arkade/bin/kubectl /usr/local/bin/ 40 | arkade get flux 41 | mv /root/.arkade/bin/flux /usr/local/bin/ 42 | source <(kubectl completion zsh) && \ 43 | echo 'source <(kubectl completion zsh)' >> ~/.zshrc 44 | . <(flux completion zsh) 45 | echo ". <(flux completion zsh)" >> ~/.zshrc 46 | 47 | # Install k3s 48 | curl -sfL https://get.k3s.io | sh -s - --flannel-backend wireguard # note: using the wireguard backend to encrypt traffic between nodes 49 | export KUBECONFIG=/etc/rancher/k3s/k3s.yaml 50 | echo "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml" >> ~/.zshrc 51 | ``` 52 | 53 | ### Installing k3s on the worker nodes 54 | 55 | SSH into the worker nodes and run the following command: 56 | 57 | ```bash 58 | # Install dependencies 59 | hostnamectl hostname worker # note: replace worker with the name of the worker node that you want. You can get fancy and input names from powerful characters of your favorite anime. 60 | pacman -Syu --noconfirm 61 | pacman -S --noconfirm wireguard-tools mosh 62 | 63 | # Install k3s 64 | export K3S_TOKEN= # note: replace with the token from the master node 65 | export K3S_URL=https://masternode.com:6443 # note: replace masternode.com with the hostname of the master node 66 | curl -sfL https://get.k3s.io | sh - 67 | ``` 68 | 69 | ## GitOps 70 | 71 | In order to easily deploy our application to the cluster, we will use GitOps. 72 | This guide will cover the basics of ArgoCD. 73 | 74 | ### Installing ArgoCD on the cluster 75 | 76 | ```bash 77 | kubectl create namespace argocd 78 | kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml 79 | # Manual step: Edit the argocd-server deployment to add the --insecure flag to the argocd-server command (required to disable HTTPS - more info here: https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/#traefik-v22) 80 | ``` 81 | 82 | ### Accessing the ArgoCD UI 83 | 84 | ```bash 85 | export ARGOCD_PASSWORD=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo) 86 | echo "ArgoCD username: admin" 87 | echo "ArgoCD password: $ARGOCD_PASSWORD" 88 | 89 | kubectl port-forward svc/argocd-server -n argocd 8080:80 90 | # open http://localhost:8080 and login with the credentials above 91 | ``` 92 | 93 | ### Adding your app repository to ArgoCD 94 | 95 | Go to [`/settings/repos`](https://localhost:8080/settings/repos) and add your repository. 96 | 97 | ### Adding your GitHub PAT to pull Docker images 98 | 99 | ```bash 100 | brew install kubeseal # install the kubeseal CLI 101 | kubectl create secret docker-registry ghcr-clouedoc --dry-run --docker-server=ghcr.io --docker-username=clouedoc --docker-password= --docker-email=clouedoc@tutanota.com -o jso 102 | n > secret.json 103 | kubeseal --format yaml < secret.json > k8s/ghcr-clouedoc.yaml 104 | rm secret.json 105 | # update kustomization.yaml to include ghcr-clouedoc.yaml 106 | ``` 107 | -------------------------------------------------------------------------------- /template/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "verbose": false, 4 | "ignore": ["*.md", ".git", "node_modules"], 5 | "watch": ["src", "package.json"], 6 | "ext": "js,ts,json", 7 | "execMap": { 8 | "ts": "tsc" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /template/package.json.jinja: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ project_slug }}", 3 | "version": "1.0.0", 4 | {% if package_private %}"private": true,{% endif %} 5 | "description": "{{ project_short_description }}", 6 | "license": "{{ project_license }}", 7 | "repository": "{{ project_url }}", 8 | "author": "{{ full_name }} <{{ email }}>", 9 | "bin": { 10 | "{{ binary_name }}": "./bin/run" 11 | }, 12 | "files": [ 13 | "lib", 14 | "oclif.manifest.json", 15 | "bin" 16 | ], 17 | "scripts": { 18 | "build": "rm -rf lib; swc --out-dir ./lib ./src && tsc --noEmit", 19 | "detect-circular-dependencies": "madge --circular --extensions ts ./src", 20 | "dev": "nodemon --exec 'yarn build && yarn lint && yarn detect-circular-dependencies && yarn oclif:build && yarn sort'", 21 | "postinstall": "patch-package", 22 | "lint": "eslint ./src --fix --max-warnings 0", 23 | "oclif:build": "oclif manifest && oclif readme", 24 | "prettier": "prettier --config .prettierrc --write --loglevel error .", 25 | "sort": "npx sort-package-json", 26 | "test": "jest", 27 | "ncu": "npx npm-check-updates" 28 | }, 29 | "dependencies": { 30 | "got": "^11", 31 | "async-lock": "^1.3.2", 32 | "delay": "^5.0.0", 33 | "execa": "5", 34 | "@oclif/command": "^1.8.16", 35 | "@oclif/plugin-help": "^5.1.12", 36 | "@oclif/plugin-not-found": "^2.3.1", 37 | "postgres": "^3.2.4", 38 | "prom-client": "^14.1.0", 39 | "express": "^4.18.1", 40 | "express-basic-auth": "^1.2.1", 41 | "fast-safe-stringify": "^2.1.1", 42 | "winston": "^3.7.2", 43 | "winston-console-format": "^1.0.8", 44 | "winston-loki": "^6.0.5", 45 | "zod": "^3.17.3" 46 | }, 47 | "devDependencies": { 48 | "@types/async-lock": "^1.1.5", 49 | "@rushstack/eslint-config": "^2.6.0", 50 | "@types/express": "^4.17.14", 51 | "@swc/cli": "^0.1.57", 52 | "@swc/core": "^1.2.203", 53 | "@swc/jest": "^0.2.21", 54 | "@types/jest": "^28.1.1", 55 | "@types/node": "^16.0.0", 56 | "@typescript-eslint/eslint-plugin": "^5.28.0", 57 | "@typescript-eslint/parser": "^5.28.0", 58 | "@typescript-eslint/typescript-estree": "^5.28.0", 59 | "eslint": "^8.17.0", 60 | "jest": "^28.1.1", 61 | "madge": "^5.0.1", 62 | "nodemon": "^2.0.16", 63 | "oclif": "^3.0.1", 64 | "patch-package": "^6.4.7", 65 | "prettier": "^2.7.0", 66 | "typescript": "^4.7.3" 67 | }, 68 | "engines": { 69 | "node": ">={{ node_version }}.0.0" 70 | }, 71 | "oclif": { 72 | "commands": "./lib/commands", 73 | "bin": "{{ binary_name }}", 74 | "plugins": [ 75 | "@oclif/plugin-help", 76 | "@oclif/plugin-not-found" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /template/scripts/vnc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Start VNC server 4 | x11vnc -display "$DISPLAY" -bg -forever -nopw -quiet -xkb -------------------------------------------------------------------------------- /template/scripts/xvfb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export DISPLAY=:99 4 | Xvfb $DISPLAY -screen 0 1024x768x16 -nolisten tcp -nolisten unix & 5 | fluxbox & 6 | -------------------------------------------------------------------------------- /template/src/commands/COMMANDS.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Put your Oclif commands here. 4 | 5 | ## Adding a command 6 | 7 | ```bash 8 | yarn oclif command hello 9 | # adds a command named "hello" 10 | ``` 11 | -------------------------------------------------------------------------------- /template/src/commands/dev/logs.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core'; 2 | import { logger } from '../../packlets/telemetry'; 3 | 4 | export default class DevLogs extends Command { 5 | static description = 'Send test logs to Loki'; 6 | 7 | static examples = ['<%= config.bin %> <%= command.id %>']; 8 | 9 | public async run(): Promise { 10 | setInterval(() => { 11 | logger.info('Ceci est un log de test'); 12 | logger.debug('coucou les amis!!'); 13 | }, 1000); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /template/src/packlets/PACKLETS.md: -------------------------------------------------------------------------------- 1 | # Packlets 2 | 3 | Put your Rush packlets here. 4 | -------------------------------------------------------------------------------- /template/src/packlets/browser/args.ts: -------------------------------------------------------------------------------- 1 | import { platform } from 'os'; 2 | import { browserEnv } from './env'; 3 | 4 | export const CHROME_ARGS: string[] = [ 5 | '--enable-logging', 6 | '--v1=1', 7 | '--no-first-run', 8 | ]; 9 | 10 | export const CHROME_IGNORE_DEFAULT_ARGS: string[] = [ 11 | '--disable-ipc-flooding-protection', 12 | ]; 13 | 14 | if (platform() === 'darwin' || browserEnv.CONTAINER_DEV_SHM_ENABLED) { 15 | CHROME_IGNORE_DEFAULT_ARGS.push('--disable-dev-shm-usage'); 16 | } 17 | 18 | if (platform() === 'linux' && !browserEnv.CONTAINER_PRIVILEGED) { 19 | CHROME_ARGS.push('--no-sandbox'); 20 | } 21 | -------------------------------------------------------------------------------- /template/src/packlets/browser/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const browserEnv = z 4 | .object({ 5 | CHROME_PATH: z.string().optional(), 6 | 7 | CAPTCHA_PROVIDER_ID: z.string().default('2captcha'), 8 | /** 9 | * The 2captcha API key 10 | */ 11 | CAPTCHA_PROVIDER_KEY: z 12 | .string() 13 | .optional() 14 | .default('52dcb47b063c981a87fdcb24414f0929'), 15 | 16 | CONTAINER_PRIVILEGED: z 17 | .string() 18 | .transform((v) => v === 'true') 19 | .optional(), 20 | CONTAINER_DEV_SHM_ENABLED: z 21 | .string() 22 | .transform((v) => v === 'true') 23 | .optional(), 24 | }) 25 | .parse(process.env); 26 | -------------------------------------------------------------------------------- /template/src/packlets/database/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const dbEnv = z 4 | .object({ 5 | POSTGRES_URL: z 6 | .string() 7 | .default('postgres://postgres:postgres@localhost:5432/postgres'), 8 | }) 9 | .parse(process.env); 10 | -------------------------------------------------------------------------------- /template/src/packlets/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sql'; 2 | -------------------------------------------------------------------------------- /template/src/packlets/database/sql.ts: -------------------------------------------------------------------------------- 1 | import postgres from 'postgres'; 2 | import { dbEnv } from './env'; 3 | 4 | export const sql: postgres.Sql<{}> = postgres(dbEnv.POSTGRES_URL, { 5 | prepare: false, // note: this is needed when using pgBouncer in 'transaction' mode 6 | types: { 7 | bigint: postgres.BigInt, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /template/src/packlets/telemetry/README.md: -------------------------------------------------------------------------------- 1 | # telemetry 2 | 3 | This Rush Stack packlet contains everything you need to do basic [telemetry](https://en.wikipedia.org/wiki/Telemetry) with [Loki](https://github.com/grafana/loki). 4 | 5 | ## Usage 6 | 7 | ### Logging 8 | 9 | We are using a simple 10 | 11 | ```ts 12 | import { logger } from '../telemetry'; 13 | 14 | logger.info('Hello there!'); 15 | logger.info('You can log metrics here', { metricName: 1337 }); 16 | ``` 17 | 18 | #### Sending logs to Loki 19 | 20 | To send logs to Loki, set the following environment variables. 21 | 22 | | Name | Description | Example | Default | 23 | | ----------------- | ------------------------------------------------------- | ---------------------------- | ------- | 24 | | `LOKI_URL` | The URL of your Loki instance | https://loki.yourcompany.com | _Empty_ | 25 | | `LOKI_BASIC_AUTH` | Basic authentication credentials for your Loki instance | username:password | _Empty_ | 26 | 27 | ### Traces and metrics 28 | 29 | _To be added in a future release_ 30 | 31 | ## What is Telemetry? 32 | 33 | Telemetry is a software engineering techniques which places emphasis on collecting information 34 | about the internal state of your software. 35 | 36 | There are three main ways to do that: 37 | 38 | - logging 39 | - tracing 40 | - collecting metrics 41 | 42 | ## FAQ 43 | 44 | ### This packages doesn't send metrics 45 | 46 | Logs that are sent to Loki can easily be [converted into metrics](https://grafana.com/blog/2021/03/23/how-i-fell-in-love-with-logs-thanks-to-grafana-loki/). 47 | 48 | Metrics are useful when you are getting serious about your infrastructure, but are a burden to maintain when starting out a project. 49 | 50 | ### This package doesn't send traces 51 | 52 | This is to be added. 53 | 54 | However, be informed that traces are mostly useful to debug distributed systems. (not that useful when starting out) 55 | 56 | ### I want to implement metrics and traces. How should I go about it? 57 | 58 | I highly recommend instrumenting this app using [OpenTelemetry](https://opentelemetry.io/) combined with an [OpenTelemetry-Collector server](https://github.com/open-telemetry/opentelemetry-collector). 59 | -------------------------------------------------------------------------------- /template/src/packlets/telemetry/env.ts.jinja: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/typedef 4 | export const telemetryEnv = z 5 | .object({ 6 | SERVICE_NAME: z.string().default('{{ project_slug }}'), 7 | LOKI_URL: z.string().optional(), 8 | LOKI_BASIC_AUTH: z.string().optional(), 9 | 10 | /** 11 | * Username for the /metrics endpoint. 12 | */ 13 | METRICS_USERNAME: z.string().default('noloki'), 14 | /** 15 | * Password for the /metrics endpoint. 16 | */ 17 | METRICS_PASSWORD: z.string().default('azertyuiop'), 18 | /** 19 | * Port for the /metrics endpoint. 20 | */ 21 | METRICS_PORT: z.string().transform(Number).default('9100'), 22 | }) 23 | .parse(process.env); 24 | -------------------------------------------------------------------------------- /template/src/packlets/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | export { logger } from './logger'; 2 | -------------------------------------------------------------------------------- /template/src/packlets/telemetry/logger.ts.jinja: -------------------------------------------------------------------------------- 1 | import stringify from 'fast-safe-stringify'; 2 | import os from 'os'; 3 | import winston, { createLogger, format, Logger, transports } from 'winston'; 4 | import { consoleFormat } from 'winston-console-format'; 5 | import LokiTransport from 'winston-loki'; 6 | import { telemetryEnv } from './env'; 7 | 8 | export const logger: Logger = createLogger({ 9 | level: 'silly', 10 | format: format.combine( 11 | format.timestamp(), 12 | format.ms(), 13 | format.errors({ stack: true }), 14 | format.splat(), 15 | format.json(), 16 | ), 17 | defaultMeta: { service: telemetryEnv.SERVICE_NAME }, 18 | transports: [ 19 | new transports.Console({ 20 | format: format.combine( 21 | format.colorize({ all: true }), 22 | format.padLevels(), 23 | consoleFormat({ 24 | showMeta: true, 25 | metaStrip: ['timestamp', 'service'], 26 | inspectOptions: { 27 | depth: Infinity, 28 | colors: true, 29 | maxArrayLength: Infinity, 30 | breakLength: 120, 31 | compact: Infinity, 32 | }, 33 | }), 34 | ), 35 | }), 36 | ], 37 | }); 38 | 39 | function tryJSONStringify(obj: Record): string | undefined { 40 | try { 41 | return JSON.stringify(obj); 42 | } catch (_) { 43 | return; 44 | } 45 | } 46 | 47 | export function formatError(error: Error): Record { 48 | const enumeratedErrorObject: Record = {}; 49 | Object.getOwnPropertyNames(error).forEach((key: string) => { 50 | enumeratedErrorObject[key] = (error as unknown as Record)[ 51 | key 52 | ]; 53 | }); 54 | 55 | // remove circular dependencies so that Winston can process the error 56 | const serialized: string = 57 | tryJSONStringify(enumeratedErrorObject) || stringify(enumeratedErrorObject); 58 | 59 | return JSON.parse(serialized); 60 | } 61 | 62 | /** 63 | * Enumerable properties show up in for...in loops 64 | * but the Error object properties are set not to be Enumerable. 65 | * While calling JSON.stringify(err), most of it's properties don't show 66 | * because JSON.stringify internally uses something like for...in or Object.keys(err) 67 | * Bellow we replace the Error with new object which all it's properties are enumerable. 68 | */ 69 | const errorObjectFormat: winston.Logform.FormatWrap = winston.format((info) => { 70 | // Don't want to Object.keys() on the info object to find Error instances, 71 | // because this function will run before every logging 72 | // So we assume that the error will be under 'error' or 'err' key 73 | if (info.err instanceof Error) { 74 | info.error = info.err; 75 | delete info.err; 76 | } 77 | 78 | if (info.error instanceof Error) { 79 | info.error = formatError(info.error); 80 | } 81 | return info; 82 | }); 83 | 84 | if (telemetryEnv.LOKI_URL) { 85 | logger.add( 86 | new LokiTransport({ 87 | format: winston.format.combine( 88 | errorObjectFormat(), 89 | winston.format.json(), 90 | ), 91 | host: telemetryEnv.LOKI_URL, 92 | basicAuth: telemetryEnv.LOKI_BASIC_AUTH, 93 | labels: { 94 | service: telemetryEnv.SERVICE_NAME, 95 | hostname: os.hostname(), 96 | }, 97 | json: true, 98 | replaceTimestamp: true, 99 | }), 100 | ); 101 | logger.debug('Added loki transport'); 102 | } 103 | -------------------------------------------------------------------------------- /template/src/packlets/telemetry/metrics.ts: -------------------------------------------------------------------------------- 1 | import execa from 'execa'; 2 | import express, { Express } from 'express'; 3 | import basicAuth from 'express-basic-auth'; 4 | import os from 'os'; 5 | import { collectDefaultMetrics, register } from 'prom-client'; 6 | import { telemetryEnv } from './env'; 7 | import { logger } from './logger'; 8 | 9 | const api: Express = express(); 10 | api.use( 11 | basicAuth({ 12 | users: { [telemetryEnv.METRICS_USERNAME]: telemetryEnv.METRICS_PASSWORD }, 13 | }), 14 | ); 15 | 16 | api.get('/metrics', async (req, res) => { 17 | try { 18 | res.set('Content-Type', register.contentType); 19 | res.end(await register.metrics()); 20 | } catch (ex) { 21 | res.status(500).end(ex); 22 | } 23 | }); 24 | 25 | let prepared: boolean = false; 26 | export function preparePrometheus(): void { 27 | if (prepared) { 28 | return; 29 | } 30 | 31 | let gitCommitHash: string = 'unknown'; 32 | try { 33 | gitCommitHash = 34 | execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 8) ?? 'unknown'; 35 | } catch (err) { 36 | // logger.warn('Could not get git commit hash', { err }); 37 | } 38 | 39 | register.setDefaultLabels({ 40 | service: telemetryEnv.SERVICE_NAME, 41 | hostname: os.hostname(), 42 | commit: gitCommitHash, 43 | instance: os.hostname() + ':' + process.pid, 44 | }); 45 | collectDefaultMetrics(); 46 | api.listen(telemetryEnv.METRICS_PORT); 47 | logger.debug('Prepared Prometheus client'); 48 | prepared = true; 49 | } 50 | -------------------------------------------------------------------------------- /template/src/packlets/utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lock'; 2 | export * from './random'; 3 | export * from './require-file'; 4 | export * from './throw-expression'; 5 | -------------------------------------------------------------------------------- /template/src/packlets/utils/functions/lock.ts: -------------------------------------------------------------------------------- 1 | import AsyncLock from 'async-lock'; 2 | 3 | export const lock: AsyncLock = new AsyncLock(); 4 | -------------------------------------------------------------------------------- /template/src/packlets/utils/functions/random.ts: -------------------------------------------------------------------------------- 1 | import delay from 'delay'; 2 | 3 | export interface IStrongRange { 4 | min: number; 5 | max: number; 6 | } 7 | 8 | /** 9 | * @returns a random integer between min and max 10 | */ 11 | export function randomInteger({ min, max }: IStrongRange): number { 12 | min = Math.ceil(min); 13 | max = Math.floor(max); 14 | // eslint-disable-next-line @typescript-eslint/no-magic-numbers 15 | return Math.floor(Math.random() * (max - min + 1)) + min; 16 | } 17 | 18 | /** 19 | * @returns a random float between the given min and max 20 | */ 21 | export function randomFloat({ min, max }: IStrongRange): number { 22 | // eslint-disable-next-line @typescript-eslint/no-magic-numbers 23 | return Math.random() * (max - min) + min; 24 | } 25 | 26 | /** 27 | * @param percentage the chance percentage that the function will return true 28 | */ 29 | export function chance(percentage: number): boolean { 30 | const number: number = randomInteger({ min: 0, max: 100 }); 31 | return number < percentage; 32 | } 33 | 34 | /** 35 | * Wait for an amount of time specified between min and max. 36 | * @param range the range to wait for, in milliseconds 37 | */ 38 | export async function randomWait(range: IStrongRange): Promise { 39 | return delay(randomInteger(range)); 40 | } 41 | -------------------------------------------------------------------------------- /template/src/packlets/utils/functions/require-file.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | /** 4 | * Make require work with the given extension (it will return a string) 5 | * @param extension extension INCLUDING dot (e.g. '.txt') 6 | */ 7 | export function enableFileRequirement(extension: string): void { 8 | if (!require.extensions[extension]) { 9 | require.extensions[extension] = function ( 10 | module: NodeJS.Module, 11 | filename: string, 12 | ) { 13 | module.exports = fs.readFileSync(filename, 'utf8'); 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /template/src/packlets/utils/functions/throw-expression.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An expression that will throw the given error message. 3 | * Useful when you want to assert that a value is undefined. 4 | * 5 | * Example: 6 | * 7 | * ``` 8 | * // will throw because the function returns null 9 | * returnsNull("argument") ?? throwExpression("the function returned undefined !") 10 | * 11 | * // will NOT throw because the function returns neither null nor undefined 12 | * returnsSomething("hi") ?? throwExpression("the function returned undefined !") 13 | * ``` 14 | * 15 | * [Stackoverflow source](https://stackoverflow.com/a/65666402/4564097) 16 | */ 17 | export function throwExpression(errorMessage: string): never { 18 | throw new Error(errorMessage); 19 | } 20 | -------------------------------------------------------------------------------- /template/src/packlets/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './schemas'; 2 | export * from './functions'; 3 | -------------------------------------------------------------------------------- /template/src/packlets/utils/schemas/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const env = z 4 | .object({ 5 | // add your environment variables here 6 | }) 7 | .parse(process.env); 8 | -------------------------------------------------------------------------------- /template/src/packlets/utils/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env'; 2 | -------------------------------------------------------------------------------- /template/src/packlets/utils/transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse normal files. 3 | */ 4 | module.exports = { 5 | process(src) { 6 | return { code: `module.exports = \`${src}\`;` }; 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "esnext", 6 | "lib": [ 7 | "dom", 8 | "esnext" 9 | ], 10 | "allowSyntheticDefaultImports": true, 11 | "removeComments": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "inlineSourceMap": true, 15 | "noEmitOnError": false, 16 | "strictNullChecks": true, 17 | "noImplicitReturns": true, 18 | "noUnusedLocals": false, 19 | "noUnusedParameters": false, 20 | "declaration": true, 21 | "skipLibCheck": true, 22 | "declarationMap": true, 23 | "esModuleInterop": true, 24 | "outDir": "./lib", 25 | "rootDir": "./src", 26 | "resolveJsonModule": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "forceConsistentCasingInFileNames": true, 29 | "allowJs": true, 30 | "useUnknownInCatchVariables": false 31 | }, 32 | "exclude": [ 33 | "**/.*/*", 34 | "./lib/**" 35 | ], 36 | "include": [ 37 | "./src/**/*", 38 | "./node_modules/puppeteer-extra/**/*.d.ts", 39 | "./node_modules/puppeteer-extra-*/**/*.d.ts" 40 | ] 41 | } -------------------------------------------------------------------------------- /template/{{_copier_conf.answers_file}}.jinja: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier 2 | {{_copier_answers|to_nice_yaml}} --------------------------------------------------------------------------------