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