Skip to content

Commit

Permalink
chore: template setup
Browse files Browse the repository at this point in the history
configured with:
 - solid-js
 - elysia
 - bun
 - nvm
 - typescript
 - vitest
 - eslint
 - prettier
 - docker
 - github actions
  • Loading branch information
thedanchez committed Jan 14, 2025
0 parents commit 801b587
Show file tree
Hide file tree
Showing 36 changed files with 2,112 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**/node_modules/
**/dist
**/__tests__
**/coverage/
.git
.env
mockServiceWorker.js
26 changes: 26 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["simple-import-sort", "solid"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:solid/typescript"
],
"env": {
"browser": true,
"node": true,
"es6": true,
"jest": true
},
"parserOptions": {
"sourceType": "module"
},
"rules": {
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_" }],
"no-unused-vars": "off",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
},
"ignorePatterns": ["dist", "node_modules"]
}
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
thedanchez
33 changes: 33 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
quality-checks:
name: Quality Checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.42
- name: Install Dependencies
run: bun install --frozen-lockfile
- name: Type Check
run: bun typecheck
- name: Lint Check
run: bun lint
- name: Format Check
run: bun format
- name: Test Coverage Check (UI)
run: bun test:ui
- name: Test Coverage Check (Server)
run: bun test:server
80 changes: 80 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Deploy

on:
push:
branches:
- main
workflow_dispatch:
inputs:
environment:
description: "Select the environment"
required: true
default: "qa"
type: choice
options:
- qa
- prod
git_hash:
description: "Optional git commit hash or branch (defaults to latest in main)"
required: false
default: ""

jobs:
build:
name: Build and Push Image
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.git_hash || 'main' }}
- name: Set Variables
id: vars
run: |
if [ "${{ github.event.inputs.git_hash }}" == "" ]; then
echo "No git_hash input provided, using latest commit in main branch"
GIT_HASH=${{ github.sha }}
else
echo "Using provided git hash"
GIT_HASH=${{ github.event.inputs.git_hash }}
fi
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_OUTPUT
- name: Build Image
run: |
podman build -t app:${{ steps.vars.outputs.GIT_HASH }} \
--build-arg VERSION=$(echo ${{ steps.vars.outputs.GIT_HASH }} | cut -c1-7) \
.
# NOTE: The last two steps are only for template purposes. Post building the image, you should be pushing it to an
# image registry/repository which could be public facing or internal if you are a private organization.
# When you use this template, make sure to replace these steps with the appropriate ones for your use case.
- name: Save Image
run: |
podman save app:${{ steps.vars.outputs.GIT_HASH }} -o app_${{ steps.vars.outputs.GIT_HASH }}.tar
- name: Upload Image Artifact
uses: actions/upload-artifact@v4
with:
name: app_${{ steps.vars.outputs.GIT_HASH }}
path: app_${{ steps.vars.outputs.GIT_HASH }}.tar
# NOTE: Post building the image, you should be pulling it from where you pushed it to using whatever tool you use to
# deploy your application to your infrastructure (i.e. Kubernetes). This is just a template to show you where the deploy
# job would go.
deploy:
name: Deploy Image
runs-on: ubuntu-latest
needs: build
steps:
- name: Set Variables
id: vars
run: |
if [ "${{ github.event.inputs.git_hash }}" == "" ]; then
echo "No git_hash input provided, using latest commit in main branch"
GIT_HASH=${{ github.sha }}
else
echo "Using provided git hash"
GIT_HASH=${{ github.event.inputs.git_hash }}
fi
echo "GIT_HASH=$GIT_HASH" >> $GITHUB_OUTPUT
- name: Download Image Artifact
uses: actions/download-artifact@v4
with:
name: app_${{ steps.vars.outputs.GIT_HASH }}
44 changes: 44 additions & 0 deletions .github/workflows/sanbox.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Sandbox workflow to build and push ephemeral sandbox deployments on every pull request to the main branch.
# This is useful for testing changes in a production-like environment before merging them into the main branch.
name: Sandbox

on:
pull_request:
branches:
- main

jobs:
build:
name: Build and Push Image
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Build Image
run: |
podman build -t app:sandbox-${{ github.event.pull_request.number }} \
--build-arg VERSION=sandbox-${{ github.event.pull_request.number }} \
.
# NOTE: The last two steps are only for template purposes. Post building the image, you should be pushing it to an
# image registry/repository which could be public facing or internal if you are a private organization.
# When you use this template, make sure to replace these steps with the appropriate ones for your use case.
- name: Save Image
run: |
podman save app:sandbox-${{ github.event.pull_request.number }} -o app_sandbox_${{ github.event.pull_request.number }}.tar
- name: Upload Image Artifact
uses: actions/upload-artifact@v4
with:
name: app_sandbox_${{ github.event.pull_request.number }}
path: app_sandbox_${{ github.event.pull_request.number }}.tar
# NOTE: Post building the image, you should be pulling it from where you pushed it to using whatever tool you use to
# deploy your application to your infrastructure (i.e. Kubernetes). This is just a template to show you where the deploy
# job would go.
deploy:
name: Deploy Image
runs-on: ubuntu-latest
needs: build
steps:
- name: Download Image Artifact
uses: actions/download-artifact@v4
with:
name: app_sandbox_${{ github.event.pull_request.number }}
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage
dist
node_modules

.DS_Store
.env
.env.local
/*.tgz
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.11.0
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
coverage
dist
node_modules
mockServiceWorker.js
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"printWidth": 100
}
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "always"
},
"editor.formatOnSave": true,
"editor.rulers": [100],
"files.autoSave": "onFocusChange",
"files.insertFinalNewline": true
}
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM oven/bun:1.1.42-alpine

WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

ARG VERSION
ENV VITE_VERSION=$VERSION

COPY public ./public
RUN echo "{ \"version\": \"${VERSION}\" }" > ./public/version.json

COPY index.html ./
COPY tsconfig.json ./
COPY vite.config.ts ./
COPY src ./src
COPY backend ./backend

RUN bun run build

EXPOSE 8000

CMD ["bun", "start:server"]
118 changes: 118 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Template: SolidJS + Elysia

Template for [SolidJS](https://www.solidjs.com/) SPA with [Elysia](https://elysiajs.com/at-glance.html) BFF (Backend-for-Frontend) layer. Elysia was chosen because it is performant, uses TypeScript thus creating language symmetry on both ends of the stack and, provides end-to-end type safety between the server (BFF) and the client (SPA) via [Eden Treaty](https://elysiajs.com/eden/treaty/overview.html). Furthermore, Elysia has a healthy and growing plugin ecosystem that provides first-party support for popular tools like [Swagger](https://elysiajs.com/plugins/swagger.html) for API documentation and [OpenTelemetry](https://elysiajs.com/plugins/opentelemetry.html) that provides observability, metrics and tracing to identify bottlenecks and general server performance which are important when productionizing applications.

With all of the above said, this additional BFF layer serves the following purposes:

- It is the primary source of data for the SolidJS frontend (`/api`). It will do the work of interfacing with external data sources for the sake of the frontend.
- It is responsible for serving the production built frontend assets generated from `bun run build`.
- Building on first point, issues related to CORS are gone as the frontend and backend BFF share the same origin URL.
- Can enrich or massage data for the frontend if/when desired.

Other things configured include:

- TypeScript (for both SPA and BFF)
- [Solid Query](https://tanstack.com/query/latest/docs/framework/solid/overview#motivation) (for data fetching / caching against BFF layer)
- [MSW](https://mswjs.io/docs/getting-started) + [Solid Testing Library](https://github.com/solidjs/solid-testing-library) + Vitest (for SPA testing)
- Bun Test (for BFF testing)
- ESLint / Prettier
- Dockerfile (for containerized deployments)
- GitHub Actions
- `ci.yaml`: Workflow for running code quality checks (linting, formatting, typechecks, tests).
- `sandbox.yaml`: Workflow for building and pushing ephemeral deployments on every pull request to `main`. (_Developer Note: make sure to create a separate cleanup sandbox workflow that tears down the ephemeral deployment once the pull request has been merged to `main`_)
- `deploy.yaml`: Workflow for building and pushing deployments on merge to `main`. Has a manual `workflow_dispatch` configured to support manual pushes as well. You can imagine the workflow being set where, on every merge to `main` you continuously deploy your app to some `qa` or `staging` environment. When you are satisfied with the changes, you can then manually trigger the workflow to push either the latest in `main` or git hash to your production environment.
- _Developer Notes: For the `sandbox.yaml` and `deploy.yaml` workflows, there are placeholder jobs that store the production image artifacts of the application. It is expected that when using this template, you remove those placeholder jobs and update the workflows to reflect your deployment use-case (e.g. push images to an image repository of some kind followed by deploying the image to Kubernetes or elsewhere)._

### Comments

Ideally, we would use `bun` for everything (frontend + backend) but there are gaps, specifically for the frontend, in the following two areas which force us to use Vite + Vitest: **bundling** and **testing**.

On the bundling side, Bun does not support bundling `index.html` nor injecting any bundled JS asset into the `index.html` file. We can probably work around this via doing some manual scripting but that is not worth doing for what is already a solved problem using other tools.

This is why we use Vite for bundling the frontend as there is the official `vite-plugin-solid` which does all the work under the hood for helping Vite to work with Solid. Furthermore, that same Vite plugin is what helps us to write tests against the SolidJS frontend using Vitest.

On the day when Bun provides an official plugin for Solid where it can bundle the web app and allow for writing tests against it, we can then deprecate Vite/Vitest and rely on Bun as the primary toolchain for the entire repository.

## Getting Started

Some pre-requisites before install dependencies:

- Install Node Version Manager (NVM)
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
```
- Install Bun
```bash
curl -fsSL https://bun.sh/install | bash
```

### Installing Dependencies

```bash
nvm use
bun install
```

### Local Development Build

```bash
# Note: UI dev server running on port 3000 proxies API requests to port 8000
bun start # http://localhost:3000
bun start:server --watch # http://localhost:8000

bun start:msw # start UI with mock server
```

### Local Production Build

```bash
bun run build # Build production frontend static assets
bun start:server # Backend serves assets at http://localhost:8000

docker-compose up -d # Or alternatively using docker-compose
```

### Linting & Formatting

```bash
bun lint # checks source for lint violations
bun format # checks source for format violations

bun lint:fix # fixes lint violations
bun format:fix # fixes format violations
```

### Unit Testing

> We use [Solid Testing Library](https://github.com/solidjs/solid-testing-library) for integration style unit tests
```bash
bun test:ui
bun test:server # or `bun test`

bun test:ui --coverage # with test coverage
bun test:server --coverage # with test coverage
```

### Docker / Podman Image

The provided `Dockerfile` includes all the things needed to build the SPA and BFF to support a containerized deployment model.
Recommended to use `podman` as it is daemon-less and does not require root privileges which offers better security, especially in shared environments.

```bash
# Using Podman (recommended)
podman build -t [tag_name] .

# Using Docker
docker build -t [tag_name] .
```

### Contributing

The only requirements when contributing are:

- You keep a clean git history in your branch
- rebasing `main` instead of making merge commits.
- Using proper commit message formats that adhere to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
- Additionally, squashing (via rebase) commits that are not [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
- CI checks pass before merging into `main`
13 changes: 13 additions & 0 deletions backend/__tests__/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { treaty } from "@elysiajs/eden";
import { describe, expect, it } from "vitest";

import { app } from "../server";

const api = treaty(app);

describe("Elysia", () => {
it("return a response", async () => {
const { data } = await api.api.hello.get();
expect(data).toEqual({ message: "Hello World API!" });
});
});
5 changes: 5 additions & 0 deletions backend/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Elysia } from "elysia";

const api = new Elysia({ prefix: "/api" }).get("/hello", () => ({ message: "Hello World API!" }));

export default api;
Loading

0 comments on commit 801b587

Please sign in to comment.