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
5 changes: 5 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"extends": ["./node_modules/@ffflorian/oxlint-config/index.json"],
"ignorePatterns": ["**/dist/**", "**/node_modules/**", "**/.yarn/**"]
}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ yarn dist # clean + build + write commit hash file
yarn start # run app from source with tsx
yarn start:dev # start + NODE_DEBUG
yarn start:prod # node dist/src/index.js
yarn lint # oxlint + eslint
yarn lint # oxlint
yarn test # run Vitest test suite
yarn fix # auto-fix lint issues + prettier
```
Expand Down
4 changes: 0 additions & 4 deletions eslint.config.ts

This file was deleted.

6 changes: 2 additions & 4 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ pre-commit:
prettier:
glob: '*.{js,ts,css,csv,md,json,yml,yaml}'
exclude:
- CLAUDE.md
- 'CLAUDE.md'
- '.claude'
run: npx --no -- prettier --write {staged_files} && git add {staged_files}
oxlint:
glob: '*.{js,ts}'
run: npx --no -- oxlint --ignore-path .gitignore --fix {staged_files} && git add {staged_files}
eslint:
glob: '*.{js,ts}'
run: npx --no -- eslint --fix {staged_files} && git add {staged_files}
15 changes: 3 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,20 @@
},
"description": "Find the source of an npm package in an instant.",
"devDependencies": {
"@eslint/js": "10.0.1",
"@ffflorian/eslint-config": "1.1.8",
"@ffflorian/oxlint-config": "1.0.5",
"@ffflorian/prettier-config": "1.0.2",
"@types/express": "5.0.6",
"@types/node": "~26",
"@types/validate-npm-package-name": "4.0.2",
"@vitest/coverage-v8": "5.0.0",
"eslint": "10.10.0",
"eslint-import-resolver-typescript": "4.4.5",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-oxlint": "1.82.0",
"eslint-plugin-perfectionist": "5.11.0",
"globals": "17.12.0",
"jiti": "2.7.0",
"lefthook": "2.1.12",
"oxlint": "1.82.0",
"prettier": "3.9.6",
"tsx": "4.23.13",
"type-fest": "5.9.0",
"typescript": "6.0.3",
"typescript-eslint": "8.70.0",
"vite": "8.3.0",
"vitest": "5.0.0"
},
Expand Down Expand Up @@ -65,12 +58,10 @@
"dist": "yarn clean && yarn build",
"fix": "yarn fix:ts && yarn fix:other",
"fix:other": "yarn prettier --write",
"fix:ts": "yarn lint:ts:oxlint --fix && yarn lint:ts:eslint --fix",
"fix:ts": "yarn lint:ts --fix",
"lint": "yarn lint:other && yarn lint:ts",
"lint:other": "yarn prettier --list-different",
"lint:ts": "yarn lint:ts:oxlint && yarn lint:ts:eslint",
"lint:ts:eslint": "eslint .",
"lint:ts:oxlint": "oxlint --ignore-path .gitignore .",
"lint:ts": "oxlint --ignore-path .gitignore .",
"prettier": "prettier \"*.{json,md,yml}\"",
"start:prod": "node dist/index.js",
"start": "tsx src/index.ts",
Expand Down
4 changes: 2 additions & 2 deletions src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function cleanupExpiredRateLimitEntries(now: number): void {
}

function createRateLimitMiddleware(config: ServerConfig) {
const windowMs = config.RATE_LIMIT_WINDOW_SECONDS * 1_000;
const windowMs = config.RATE_LIMIT_WINDOW_SECONDS * rateLimitCleanupThreshold;

return (request: Request, response: Response, next: NextFunction) => {
const now = Date.now();
Expand All @@ -87,7 +87,7 @@ function createRateLimitMiddleware(config: ServerConfig) {
}

if (current.count >= config.RATE_LIMIT_MAX_REQUESTS) {
const retryAfter = Math.ceil((current.resetAt - now) / 1_000);
const retryAfter = Math.ceil((current.resetAt - now) / rateLimitCleanupThreshold);
response.setHeader('Retry-After', String(retryAfter));
response.status(HTTP_STATUS.TOO_MANY_REQUESTS).json({
code: HTTP_STATUS.TOO_MANY_REQUESTS,
Expand Down
6 changes: 4 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface ServerConfig {
VERSION: string;
}

const ONE_MINUTE_IN_SECONDS = 60;

const config: ServerConfig = {
CACHE_DURATION_SECONDS: 300, // 5 minutes
COMMIT: process.env.COMMIT || 'unknown',
Expand All @@ -22,8 +24,8 @@ const config: ServerConfig = {
DIST_DIR: '.',
ENVIRONMENT: process.env.ENVIRONMENT || 'prod',
PORT_HTTP: Number(process.env.PORT || defaultPort),
RATE_LIMIT_MAX_REQUESTS: Number(process.env.RATE_LIMIT_MAX_REQUESTS || 120),
RATE_LIMIT_WINDOW_SECONDS: Number(process.env.RATE_LIMIT_WINDOW_SECONDS || 60),
RATE_LIMIT_MAX_REQUESTS: Number(process.env.RATE_LIMIT_MAX_REQUESTS || ONE_MINUTE_IN_SECONDS * 2),
RATE_LIMIT_WINDOW_SECONDS: Number(process.env.RATE_LIMIT_WINDOW_SECONDS || ONE_MINUTE_IN_SECONDS),
VERSION: process.env.VERSION || 'unknown',
};

Expand Down
53 changes: 27 additions & 26 deletions test/Server.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {ServerConfig} from '../src/config.js';
import {ParseStatus} from '../src/RepositoryParser.js';
import * as repositoryParser from '../src/RepositoryParser.js';
import {createApp} from '../src/Server.js';
import {createApp, HTTP_STATUS} from '../src/Server.js';

const defaultConfig: ServerConfig = {
CACHE_DURATION_SECONDS: 300,
COMMIT: '',
COMPRESS_LEVEL: 6,
COMPRESS_MIN_SIZE: 500,
DEVELOPMENT: true,
Expand Down Expand Up @@ -55,40 +56,40 @@ describe('server routes', () => {
it('serves health endpoint', async () => {
const response = await fetch(`${baseUrl}/_health`);

expect(response.status).toBe(200);
expect(response.status).toBe(HTTP_STATUS.OK);
});

it('serves info endpoint', async () => {
const response = await fetch(`${baseUrl}/_info`);
const body = (await response.json()) as {code: number; commit: string; version?: string};

expect(response.status).toBe(200);
expect(body.code).toBe(200);
expect(response.status).toBe(HTTP_STATUS.OK);
expect(body.code).toBe(HTTP_STATUS.OK);
expect(body.commit).toBeDefined();
});

it('redirects main route to repository', async () => {
const response = await fetch(`${baseUrl}/`, {redirect: 'manual'});

expect(response.status).toBe(302);
expect(response.status).toBe(HTTP_STATUS.MOVED_TEMPORARILY);
expect(response.headers.get('location')).toBe('https://github.com/ffflorian/pkgsource');
});

it('returns raw main route payload', async () => {
const response = await fetch(`${baseUrl}/?raw=true`);
const body = (await response.json()) as {code: number; url: string};

expect(response.status).toBe(200);
expect(response.status).toBe(HTTP_STATUS.OK);
expect(body).toEqual({
code: 200,
code: HTTP_STATUS.OK,
url: 'https://github.com/ffflorian/pkgsource',
});
});

it('supports unpkg mode on main route', async () => {
const response = await fetch(`${baseUrl}/?unpkg=true`, {redirect: 'manual'});

expect(response.status).toBe(302);
expect(response.status).toBe(HTTP_STATUS.MOVED_TEMPORARILY);
expect(response.headers.get('location')).toBe('https://unpkg.com/browse/pkgsource@latest/');
});

Expand All @@ -97,16 +98,16 @@ describe('server routes', () => {
const favicon = await fetch(`${baseUrl}/favicon.ico`);

expect(await robots.text()).toBe('User-agent: *\nDisallow: /');
expect(favicon.status).toBe(404);
expect(favicon.status).toBe(HTTP_STATUS.NOT_FOUND);
});

it('returns json not found via global exception filter', async () => {
const response = await fetch(`${baseUrl}/not-a-scope/unknown-package`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(404);
expect(response.status).toBe(HTTP_STATUS.NOT_FOUND);
expect(body).toEqual({
code: 404,
code: HTTP_STATUS.NOT_FOUND,
message: 'Not found',
});
});
Expand All @@ -115,14 +116,14 @@ describe('server routes', () => {
const response = await fetch(`${baseUrl}/!invalid`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(422);
expect(response.status).toBe(HTTP_STATUS.UNPROCESSABLE_ENTITY);
expect(body.message).toBe('Invalid package name');
});

it('supports unpkg mode for package endpoints', async () => {
const response = await fetch(`${baseUrl}/lodash@4.17.21?unpkg=true`, {redirect: 'manual'});

expect(response.status).toBe(302);
expect(response.status).toBe(HTTP_STATUS.MOVED_TEMPORARILY);
expect(response.headers.get('location')).toBe('https://unpkg.com/browse/lodash@4.17.21/');
});

Expand All @@ -135,34 +136,34 @@ describe('server routes', () => {
const response = await fetch(`${baseUrl}/lodash?raw=true`);
const body = (await response.json()) as {code: number; url: string};

expect(response.status).toBe(200);
expect(response.status).toBe(HTTP_STATUS.OK);
expect(body).toEqual({
code: 200,
code: HTTP_STATUS.OK,
url: 'https://github.com/lodash/lodash',
});
});

it('maps parser not found statuses to 404', async () => {
it('maps parser not found statuses to HTTP_STATUS.NOT_FOUND', async () => {
vi.spyOn(repositoryParser, 'getPackageUrl').mockResolvedValueOnce({
status: ParseStatus.NO_URL_FOUND,
});

const response = await fetch(`${baseUrl}/left-pad`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(404);
expect(response.status).toBe(HTTP_STATUS.NOT_FOUND);
expect(body.message).toContain('No source URL found');
});

it('maps parser package not found status to 404', async () => {
it('maps parser package not found status to HTTP_STATUS.NOT_FOUND', async () => {
vi.spyOn(repositoryParser, 'getPackageUrl').mockResolvedValueOnce({
status: ParseStatus.PACKAGE_NOT_FOUND,
});

const response = await fetch(`${baseUrl}/definitely-missing-package`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(404);
expect(response.status).toBe(HTTP_STATUS.NOT_FOUND);
expect(body.message).toBe('Package not found');
});

Expand All @@ -174,7 +175,7 @@ describe('server routes', () => {
const response = await fetch(`${baseUrl}/lodash@0.0.0-does-not-exist`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(404);
expect(response.status).toBe(HTTP_STATUS.NOT_FOUND);
expect(body.message).toBe('Version not found');
});

Expand All @@ -186,7 +187,7 @@ describe('server routes', () => {
const response = await fetch(`${baseUrl}/problematic-package`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(500);
expect(response.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR);
expect(body.message).toBe('Internal server error');
});

Expand All @@ -199,7 +200,7 @@ describe('server routes', () => {
const response = await fetch(`${baseUrl}/%40scope/pkg@1.2.3?raw=true`);
const body = (await response.json()) as {code: number; url: string};

expect(response.status).toBe(200);
expect(response.status).toBe(HTTP_STATUS.OK);
expect(body.url).toBe('https://github.com/example/pkg');
expect(getPackageUrlSpy).toHaveBeenCalledWith('@scope/pkg', '1.2.3');
});
Expand All @@ -208,9 +209,9 @@ describe('server routes', () => {
const response = await fetch(`${baseUrl}/scope/pkg`);
const body = (await response.json()) as {code: number; message: string};

expect(response.status).toBe(404);
expect(response.status).toBe(HTTP_STATUS.NOT_FOUND);
expect(body).toEqual({
code: 404,
code: HTTP_STATUS.NOT_FOUND,
message: 'Not found',
});
});
Expand All @@ -229,8 +230,8 @@ describe('rate limiting', () => {

await started.app.close();

expect([200, 429]).toContain(firstResponse.status);
expect(secondResponse.status).toBe(429);
expect([HTTP_STATUS.OK, HTTP_STATUS.TOO_MANY_REQUESTS]).toContain(firstResponse.status);
expect(secondResponse.status).toBe(HTTP_STATUS.TOO_MANY_REQUESTS);
expect(body).toEqual({
code: 429,
message: 'Too many requests',
Expand Down
Loading