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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ LICENSE
# Test artifacts
test-screenshots/
comparison/
.lighthouse/

# Build output
dist/
Expand Down
19 changes: 8 additions & 11 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,23 @@ name: "Check"
on:
pull_request:
push:
branches: [main]
branches: [ main ]

jobs:
check:
name: "Check"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
node-version: '18'

- name: Install PNPM
run: npm i -g pnpm
bun-version: 'latest'

- name: Install dependencies
run: pnpm install
run: bun install --frozen-lockfile

- name: Format
run: pnpm run check
- name: ESLint check
run: bun run check
19 changes: 8 additions & 11 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,23 @@ name: "Lint"
on:
pull_request:
push:
branches: [main]
branches: [ main ]

jobs:
lint:
name: "Lint"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
node-version: '18'

- name: Install PNPM
run: npm i -g pnpm
bun-version: 'latest'

- name: Install dependencies
run: pnpm install
run: bun install --frozen-lockfile

- name: Format
run: pnpm run lint
- name: Biome lint
run: bun run lint
50 changes: 50 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: "Test"

on:
pull_request:
push:
branches: [ main ]

jobs:
tests:
name: "Unit and E2E Tests"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 'latest'

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run unit tests
run: bun test:unit

- name: Start container
run: docker compose up -d

- name: Wait for container to be ready
run: |
for i in {1..30}; do
if curl -f http://localhost:3000/v1/health > /dev/null 2>&1; then
echo "Container is ready!"
exit 0
fi
echo "Waiting for container... ($i/30)"
sleep 1
done
echo "Container failed to start"
echo "Container logs:"
docker compose logs
exit 1

- name: Run e2e tests
run: bun test:e2e

- name: Print logs
if: failure()
run: docker compose logs
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,5 @@ dist
# Test artifacts
test-screenshots/
comparison/
.lighthouse/
lighthouse/
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ RUN apk upgrade --no-cache --available && \
tini && \
apk add --no-cache font-wqy-zenhei --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community && \
# remove unnecessary chromium files to save space
rm -rf /usr/lib/chromium/chrome_crashpad_handler \
/usr/lib/chromium/chrome_200_percent.pak \
rm -rf /usr/lib/chromium/chrome_200_percent.pak \
/usr/lib/chromium/chrome_100_percent.pak \
/usr/lib/chromium/xdg-mime \
/usr/lib/chromium/xdg-settings \
Expand All @@ -31,11 +30,14 @@ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 \
NODE_ENV=production

WORKDIR /app
USER chrome

COPY package.json ./
COPY --from=base /app/node_modules ./node_modules
COPY src/ ./src/

RUN chown -R chrome:chrome /app

USER chrome

ENTRYPOINT ["tini", "--"]
CMD ["bun", "run", "src/server.ts"]
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
"dev": "bun --watch src/server.ts",
"build": "tsc",
"type-check": "tsc --noEmit",
"format": "biome check --write ./src",
"lint": "biome check ./src",
"check": "eslint ./src",
"test": "echo \"Error: no test specified\" && exit 1"
"format": "biome check --write ./src ./tests",
"lint": "biome check ./src ./tests",
"check": "eslint ./src ./tests",
"test": "bun test",
"test:e2e": "bun test tests/e2e",
"test:unit": "bun test tests/unit"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.20.0",
"packageManager": "bun@1.3.2",
"dependencies": {
"lighthouse": "^12.2.1",
"playwright-core": "^1.52.0",
Expand Down
2 changes: 1 addition & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from "./browser.js";
export * from "./lighthouse.js";

export const port = Number(process.env.PORT) || 3000;
export const port = process.env.PORT ? Number(process.env.PORT) : 3000;
9 changes: 6 additions & 3 deletions src/routes/reports.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { BrowserContextOptions } from "playwright-core";
import type { BrowserContext, BrowserContextOptions } from "playwright-core";
import { playAudit } from "playwright-lighthouse";
import { browser, defaultContext, lighthouseConfigs } from "../config";
import { lighthouseSchema } from "../schemas";

export async function handleReportsRequest(req: Request): Promise<Response> {
let context: BrowserContext | undefined;

try {
const json = await req.json();
const body = lighthouseSchema.parse(json);
Expand All @@ -19,7 +21,7 @@ export async function handleReportsRequest(req: Request): Promise<Response> {
if (body.locale) contextOptions.locale = body.locale;
if (body.timezoneId) contextOptions.timezoneId = body.timezoneId;

const context = await browser.newContext(contextOptions);
context = await browser.newContext(contextOptions);

// Grant permissions if specified
if (body.permissions && body.permissions.length > 0) {
Expand Down Expand Up @@ -72,7 +74,6 @@ export async function handleReportsRequest(req: Request): Promise<Response> {
thresholds,
});

await context.close();
const report = Array.isArray(results.report)
? results.report.join("")
: results.report;
Expand All @@ -85,5 +86,7 @@ export async function handleReportsRequest(req: Request): Promise<Response> {
status: 400,
headers: { "Content-Type": "application/json" },
});
} finally {
await context?.close();
}
}
9 changes: 6 additions & 3 deletions src/routes/screenshots.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
BrowserContext,
BrowserContextOptions,
PageScreenshotOptions,
} from "playwright-core";
Expand All @@ -8,6 +9,8 @@ import { screenshotSchema } from "../schemas";
export async function handleScreenshotsRequest(
req: Request,
): Promise<Response> {
let context: BrowserContext | undefined;

try {
const json = await req.json();
const body = screenshotSchema.parse(json);
Expand All @@ -28,7 +31,7 @@ export async function handleScreenshotsRequest(
if (body.timezoneId) contextOptions.timezoneId = body.timezoneId;
if (body.geolocation) contextOptions.geolocation = body.geolocation;

const context = await browser.newContext(contextOptions);
context = await browser.newContext(contextOptions);

// Grant permissions if specified
if (body.permissions && body.permissions.length > 0) {
Expand Down Expand Up @@ -78,8 +81,6 @@ export async function handleScreenshotsRequest(

const screen = await page.screenshot(screenshotOptions);

await context.close();

return new Response(Buffer.from(screen), {
headers: {
"Content-Type": `image/${body.format}`,
Expand All @@ -91,5 +92,7 @@ export async function handleScreenshotsRequest(
status: 400,
headers: { "Content-Type": "application/json" },
});
} finally {
await context?.close();
}
}
22 changes: 13 additions & 9 deletions src/routes/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@ import { generateTestHTML } from "../utils/test-page.js";

export async function handleTestRequest(_req: Request): Promise<Response> {
const context = await browser.newContext(defaultContext);
const page = await context.newPage();
try {
const page = await context.newPage();

await page.goto("about:blank");
await page.goto("about:blank");

const html = await page.evaluate(() => {
return new Date().toISOString();
});
const html = await page.evaluate(() => {
return new Date().toISOString();
});

await context.close();
await context.close();

return new Response(generateTestHTML(html), {
headers: { "Content-Type": "text/html" },
});
return new Response(generateTestHTML(html), {
headers: { "Content-Type": "text/html" },
});
} finally {
await context.close();
}
}
Loading