Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
node_modules
.git
.github
.husky
# Dependencies
node_modules/

# Source Control
.git/
.github/

# Tooling
.husky/
.versions
.wrangler/
.dev.vars

# Documentation & Config
CHANGELOG.md
LICENSE.md
README.md
GEMINI.md
Dockerfile
docker-compose.yml
wrangler.toml
package-lock.json

# Misc
.DS_Store
dist/
coverage/
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"

- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
32 changes: 12 additions & 20 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false # Don't cancel deployments in progress

jobs:
deploy:
name: deploy
Expand All @@ -14,23 +18,11 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: get node version
run: echo "NODE_VERSION=$(node -p "JSON.parse(fs.readFileSync('./.versions','utf8')).node")" >> $GITHUB_OUTPUT
id: node_version
- name: setup node
uses: actions/setup-node@v4
with:
node-version: ${{ steps.node_version.outputs.NODE_VERSION }}
cache: 'npm'
- name: install dependencies
env:
HUSKY: 0
run: npm install
- name: run semantic-release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: deploy to cloudflare workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}

- name: build and deploy
run: |
docker compose run --rm \
-e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \
-e CLOUDFLARE_API_TOKEN=${{ secrets.CF_API_TOKEN }} \
app \
bash -c "npx semantic-release && npx wrangler deploy"
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: test

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

# Cancel in-progress runs for the same branch/PR when a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: test
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: build and test
run: docker compose run --rm app npm test
18 changes: 17 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
# Dependencies
node_modules/
.wrangler

# Cloudflare Workers
.wrangler/
.dev.vars

# Gemini CLI
GEMINI.md

# OS files
.DS_Store
Thumbs.db

# Misc
dist/
coverage/
.npm
npm-debug.log*
10 changes: 6 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# Use Node.js 20 as the base image for stability and compatibility
FROM node:20-slim

# Install git for semantic-release and other tools
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*

# Set the working directory inside the container
WORKDIR /app

# Copy package configuration files first to optimize layer caching
COPY package.json ./
COPY package.json package-lock.json ./

# Install project dependencies quietly and skip post-install scripts (e.g., husky)
RUN npm install --no-audit --no-fund --quiet && \
npm install -g wrangler@4.69.0 && \
npm cache clean --force

# Copy the rest of the application code
Expand All @@ -18,5 +20,5 @@ COPY . .
# Expose the default Wrangler development port
EXPOSE 8787

# Use 'wrangler dev' to run the development server, binding to all interfaces
CMD ["wrangler", "dev", "--ip", "0.0.0.0", "--port", "8787"]
# Use 'npx wrangler dev' to run the development server, binding to all interfaces
CMD ["npx", "wrangler", "dev", "--ip", "0.0.0.0", "--port", "8787"]
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ curl -s "https://enchinito-api.xmarcos.workers.dev/enchinito/Sudo%20make%20me%20
# {
# "input": "Sudo make me a sandwich",
# "output": "Sidi miki mi i sindwich",
# "version": "0.1.0"
# "version": "2.1.0"
# }
```

Expand All @@ -26,36 +26,38 @@ curl -s "https://enchinito-api.xmarcos.workers.dev/enchinito/Sudo%20make%20me%20
# <data>
# <input>Sudo make me a sandwich</input>
# <output>Sidi miki mi i sindwich</output>
# <version>0.1.0</version>
# <version>2.1.0</version>
# </data>
```

## Development
## Local Development

Local development is powered by Docker.
All development tasks are powered by Docker to ensure environment parity.

### Start Development Server
```bash
docker compose up
```

→ <http://localhost:8787>

> [Conventional Commits](https://www.conventionalcommits.org/en/about/) are enforced using a hook but there is no `prepare-commit-msg` _wizard_. You can do `npm run commit` if you need that.

## Deploy
### Run Tests
```bash
docker compose run --rm app npm test
```

→ <https://enchinito-api.xmarcos.workers.dev/>
### Deploy
```bash
CLOUDFLARE_API_TOKEN=your_token_here docker compose run --rm app npx wrangler deploy
```

### Tail Production Logs
```bash
# Using Docker
CLOUDFLARE_API_TOKEN=your_token_here docker compose run --rm app wrangler deploy
docker compose run --rm app npx wrangler tail
```

# Or using native wrangler
wrangler deploy
---

# tail prod logs
wrangler tail
```
> [Conventional Commits](https://www.conventionalcommits.org/en/about/) are enforced using a hook. You can run `docker compose run --rm app npm run commit` if you need a wizard.

## But why?

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"private": true,
"scripts": {
"commit": "commit",
"prepare": "is-ci || husky install"
"dev": "wrangler dev",
"prepare": "is-ci || husky install",
"test": "vitest run"
},
"dependencies": {
"@xmarcos/enchinito": "^0.4.2",
Expand All @@ -22,10 +24,11 @@
"husky": "^9.0.11",
"is-ci": "^3.0.1",
"semantic-release": "^23.0.2",
"vitest": "^1.4.0",
"wrangler": "4.69.0"
},
"engines": {
"node": ">=16"
"node": ">=20"
},
"release": {
"branches": [
Expand Down
112 changes: 112 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, expect } from 'vitest';
import { router } from './index';
import pkg from '../package.json';

describe('enchinito-api', () => {
const version = pkg.version;

it('should return JSON by default', async () => {
const request = new Request('https://example.com/enchinito/Hello%20World');
// @ts-ignore
const response = await router.handle(request, { request });
const data = await response.json();

expect(response.headers.get('Content-Type')).toContain('application/json');
expect(data).toEqual({
input: 'Hello World',
output: 'Hilli Wirld',
version: version
});
});

it('should return plain text when requested', async () => {
const request = new Request('https://example.com/enchinito/Hello%20World', {
headers: { 'Accept': 'text/plain' }
});
// @ts-ignore
const response = await router.handle(request, { request });
const text = await response.text();

expect(response.headers.get('Content-Type')).toContain('text/plain');
expect(text).toBe('Hilli Wirld\n');
});

it('should return XML when requested', async () => {
const request = new Request('https://example.com/enchinito/Hello%20World', {
headers: { 'Accept': 'application/xml' }
});
// @ts-ignore
const response = await router.handle(request, { request });
const text = await response.text();

expect(response.headers.get('Content-Type')).toContain('application/xml');
expect(text).toContain('<input>Hello World</input>');
expect(text).toContain('<output>Hilli Wirld</output>');
expect(text).toContain(`<version>${version}</version>`);
});

it('should return 404/info for unknown routes', async () => {
const request = new Request('https://example.com/unknown');
// @ts-ignore
const response = await router.handle(request);
const data = await response.json();

expect(data).toEqual({
goto: '/enchinito/:input',
version: version
});
});

it('should return favicon', async () => {
const request = new Request('https://example.com/favicon.ico');
// @ts-ignore
const response = await router.handle(request);

expect(response.headers.get('Content-Type')).toBe('image/svg+xml');
});

it('should be case-insensitive for Accept headers', async () => {
const request = new Request('https://example.com/enchinito/Test', {
headers: { 'Accept': 'APPLICATION/XML' }
});
// @ts-ignore
const response = await router.handle(request, { request });
expect(response.headers.get('Content-Type')).toContain('application/xml');
});

it('should handle special characters and spaces', async () => {
const request = new Request('https://example.com/enchinito/Hello%20%26%20Welcome%21');
// @ts-ignore
const response = await router.handle(request, { request });
const data = await response.json();
expect(data.input).toBe('Hello & Welcome!');
expect(data.output).toBe('Hilli & Wilcimi!');
});

it('should handle text with no vowels', async () => {
const request = new Request('https://example.com/enchinito/Fly%20Sky');
// @ts-ignore
const response = await router.handle(request, { request });
const data = await response.json();
expect(data.output).toBe('Fly Sky');
});

it('should preserve vowel casing (if library supports it)', async () => {
const request = new Request('https://example.com/enchinito/AEIOU%20aeiou');
// @ts-ignore
const response = await router.handle(request, { request });
const data = await response.json();
// Assuming enchinito maps A->I, a->i
expect(data.output).toBe('IIIII iiiii');
});

it('should work with different HTTP methods', async () => {
const methods = ['GET', 'POST', 'PUT', 'DELETE'];
for (const method of methods) {
const request = new Request('https://example.com/enchinito/test', { method });
// @ts-ignore
const response = await router.handle(request, { request });
expect(response.status).toBe(200);
}
});
});
4 changes: 1 addition & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,4 @@ router.all('*', (req) => {
});
});

addEventListener('fetch', (event) => {
event.respondWith(router.handle(event.request, event));
});
export { router };
5 changes: 5 additions & 0 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { router } from './index';

addEventListener('fetch', (event) => {
event.respondWith(router.handle(event.request, event));
});
2 changes: 1 addition & 1 deletion wrangler.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name = "enchinito-api"
account_id = "d4a1d752981f8b6317a65495079aa476"
main = "src/index.ts"
main = "src/worker.ts"
compatibility_date = "2022-09-15"