Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
fvsch committed Jan 24, 2025
1 parent fc4bfbc commit e9bea89
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 44 deletions.
64 changes: 49 additions & 15 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Buffer } from 'node:buffer';
import { createReadStream } from 'node:fs';
import { open, stat, type FileHandle } from 'node:fs/promises';
import { basename, dirname } from 'node:path';
import { createGzip, gzipSync } from 'node:zlib';

import { MAX_COMPRESS_SIZE, SUPPORTED_METHODS } from './constants.ts';
Expand All @@ -17,7 +18,7 @@ import type {
RuntimeOptions,
TrailingSlash,
} from './types.d.ts';
import { getLocalPath, headerCase, isSubpath, PathMatcher, trimSlash } from './utils.ts';
import { fwdSlash, getLocalPath, headerCase, isSubpath, PathMatcher, trimSlash } from './utils.ts';

interface Config {
req: Request;
Expand All @@ -38,12 +39,12 @@ export class RequestHandler {
#res: Config['res'];
#resolver: Config['resolver'];
#options: Config['options'];
#file: FSLocation | null = null;

timing: ResMetaData['timing'] = { start: Date.now() };
url?: URL;
localUrl?: URL;
error?: Error | string;
file: FSLocation | null = null;

_canRedirect = true;
_canStream = true;
Expand All @@ -69,6 +70,10 @@ export class RequestHandler {
});
}

get file(): FSLocation | null {
return this.#file?.target ?? this.#file;
}

get headers() {
return this.#res.getHeaders();
}
Expand Down Expand Up @@ -124,13 +129,13 @@ export class RequestHandler {

// search for files
const result = await this.#resolver.find(decodeURIComponent(searchPath));
this.file = result.file?.target ?? result.file;
this.#file = result.file;
this.status = result.status;

// redirect multiple slashes, missing/extra trailing slashes
if (this._canRedirect) {
const location = redirectSlash(this.url, {
kind: this.file?.kind,
file: this.#file,
slash: this.#options.trailingSlash,
});
if (location != null) {
Expand Down Expand Up @@ -429,23 +434,52 @@ function pickHeader(headers: Request['headers'], name: string): string[] {

export function redirectSlash(
url: URL | null,
{ kind = null, slash }: { kind?: FSKind; slash?: TrailingSlash },
{ file, slash }: { file: FSLocation | null; slash: TrailingSlash },
): string | undefined {
if (!url || url.pathname.length < 2) return;
let path = url.pathname.replace(/\/{2,}/g, '/');

const trailing = path.endsWith('/');
const aye = slash === 'always' || (slash === 'auto' && kind === 'dir');
const nay = slash === 'never' || (slash === 'auto' && kind === 'file');
if (!url || url.pathname.length < 2 || !file) return;
const { kind, filePath } = file;

let urlPath = url.pathname.replace(/\/{2,}/g, '/');
const trailing = urlPath.endsWith('/');

let aye = slash === 'always';
let nay = slash === 'never';

if (slash === 'auto' && file) {
if (file.kind === 'dir') {
aye = true;
} else if (file.kind === 'file') {
const fileName = basename(file.filePath);
const parentName = basename(dirname(file.filePath));
if (urlPath.endsWith(`/${fileName}`)) {
nay = true;
} else if (urlPath.endsWith(`/${parentName}`) || urlPath.endsWith(`/${parentName}/`)) {
aye = true;
}
if (urlPath.startsWith('/TEST/')) {
console.log({
file,
urlPath,
fileName,
parentName,
nay,
aye,
endsWithFileName: urlPath.endsWith(`/${fileName}`),
endsWithParentName:
urlPath.endsWith(`/${parentName}`) || urlPath.endsWith(`/${parentName}/`),
});
}
}
}

if (aye && !trailing) {
path += '/';
urlPath += '/';
} else if (nay && trailing) {
path = path.replace(/\/+$/, '') || '/';
urlPath = urlPath.replace(/\/$/, '') || '/';
}

if (path !== url.pathname) {
return `${path || '/'}${url.search}${url.hash}`;
if (urlPath !== url.pathname) {
return `${urlPath}${url.search}${url.hash}`;
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ export type FSKind = 'dir' | 'file' | 'link' | null;
export interface FSLocation {
filePath: string;
kind: FSKind;
target?: { filePath: string; kind: FSKind };
target?: {
filePath: string;
kind: FSKind;
};
}

export interface HttpHeaderRule {
Expand Down
54 changes: 26 additions & 28 deletions test/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { afterAll, expect, suite, test } from 'vitest';

import { fileHeaders, isValidUrlPath, redirectSlash, RequestHandler } from '../src/handler.ts';
import { FileResolver } from '../src/resolver.ts';
import type { HttpHeaderRule, RuntimeOptions, TrailingSlash } from '../src/types.d.ts';
import { fsFixture, getBlankOptions, getDefaultOptions, platformSlash } from './shared.ts';
import type { FSLocation, HttpHeaderRule, RuntimeOptions, TrailingSlash } from '../src/types.d.ts';
import { fsFixture, getBlankOptions, getDefaultOptions, loc, platformSlash } from './shared.ts';

type ResponseHeaders = Record<string, undefined | number | string | string[]>;

Expand Down Expand Up @@ -136,24 +136,22 @@ suite('isValidUrlPath', () => {
});

suite('redirectSlash', () => {
const { dir, file } = loc;
const url = (path: string) => {
const base = 'http://localhost/';
return new URL(path.startsWith('//') ? base + path.slice(1) : path, base);
};

const getRs = (slash: TrailingSlash) => {
const rs = (path: string) => redirectSlash(url(path), { kind: null, slash });
rs.dir = (path: string) => redirectSlash(url(path), { kind: 'dir', slash });
rs.file = (path: string) => redirectSlash(url(path), { kind: 'file', slash });
return rs;
return (urlPath: string, file?: FSLocation) => redirectSlash(url(urlPath), { file: file ?? null, slash });
};

test('keeps empty path or single slash', () => {
const rs = getRs('auto');
expect(rs.dir('')).toBeUndefined();
expect(rs.dir('/')).toBeUndefined();
expect(rs.file('')).toBeUndefined();
expect(rs.file('/')).toBeUndefined();
expect(rs('', dir(''))).toBeUndefined();
expect(rs('/', dir(''))).toBeUndefined();
expect(rs('', file('index.html'))).toBeUndefined();
expect(rs('/', file('index.html'))).toBeUndefined();
});

test('redirects duplicate slashes', () => {
Expand All @@ -170,29 +168,29 @@ suite('redirectSlash', () => {
const rs = getRs('ignore');
for (const path of ['/', '/notrail', '/trailing/']) {
expect(rs(path)).toBeUndefined();
expect(rs.file(path)).toBeUndefined();
expect(rs.dir(path)).toBeUndefined();
expect(rs(path, file(path))).toBeUndefined();
expect(rs(path, file(path))).toBeUndefined();
}
});

test('slash=always adds trailing slash', () => {
const rs = getRs('always');
expect(rs('/notrail')).toBe('/notrail/');

Check failure on line 178 in test/handler.test.ts

View workflow job for this annotation

GitHub Actions / Run tests on Node 18

test/handler.test.ts > redirectSlash > slash=always adds trailing slash

AssertionError: expected undefined to be '/notrail/' // Object.is equality - Expected: "/notrail/" + Received: undefined ❯ test/handler.test.ts:178:26

Check failure on line 178 in test/handler.test.ts

View workflow job for this annotation

GitHub Actions / Run tests on Node 22

test/handler.test.ts > redirectSlash > slash=always adds trailing slash

AssertionError: expected undefined to be '/notrail/' // Object.is equality - Expected: "/notrail/" + Received: undefined ❯ test/handler.test.ts:178:26
expect(rs('/trailing/')).toBe(undefined);
expect(rs.file('/notrail')).toBe('/notrail/');
expect(rs.file('/trailing/')).toBe(undefined);
expect(rs.dir('/notrail')).toBe('/notrail/');
expect(rs.dir('/trailing/')).toBe(undefined);
expect(rs('/notrail', file('notrail'))).toBe('/notrail/');
expect(rs('/trailing/', file('trailing'))).toBe(undefined);
expect(rs('/notrail', dir('notrail'))).toBe('/notrail/');
expect(rs('/trailing/', dir('trailing'))).toBe(undefined);
});

test('slash=never removes trailing slash', () => {
const rs = getRs('never');
expect(rs('/notrail')).toBe(undefined);
expect(rs('/trailing/')).toBe('/trailing');

Check failure on line 189 in test/handler.test.ts

View workflow job for this annotation

GitHub Actions / Run tests on Node 18

test/handler.test.ts > redirectSlash > slash=never removes trailing slash

AssertionError: expected undefined to be '/trailing' // Object.is equality - Expected: "/trailing" + Received: undefined ❯ test/handler.test.ts:189:28

Check failure on line 189 in test/handler.test.ts

View workflow job for this annotation

GitHub Actions / Run tests on Node 22

test/handler.test.ts > redirectSlash > slash=never removes trailing slash

AssertionError: expected undefined to be '/trailing' // Object.is equality - Expected: "/trailing" + Received: undefined ❯ test/handler.test.ts:189:28
expect(rs.file('/notrail')).toBe(undefined);
expect(rs.file('/trailing/')).toBe('/trailing');
expect(rs.dir('/notrail')).toBe(undefined);
expect(rs.dir('/trailing/')).toBe('/trailing');
expect(rs('/notrail', file('notrail'))).toBe(undefined);
expect(rs('/trailing/', file('trailing'))).toBe('/trailing');
expect(rs('/notrail', dir('notrail'))).toBe(undefined);
expect(rs('/trailing/', dir('trailing'))).toBe('/trailing');
});

test('slash=auto keeps trailing slash when no file is found', () => {
Expand All @@ -204,18 +202,18 @@ suite('redirectSlash', () => {

test('slash=auto redirects files with trailing slash', () => {
const rs = getRs('auto');
expect(rs.file('/notrail')).toBe(undefined);
expect(rs.file('/trailing/')).toBe('/trailing');
expect(rs.file('/section/notrail.html')).toBe(undefined);
expect(rs.file('/section/trailing.html/')).toBe('/section/trailing.html');
expect(rs('/notrail', file('notrail.html'))).toBe(undefined);
expect(rs('/TEST/trailing/', file('trailing.html'))).toBe('/TEST/trailing');

Check failure on line 206 in test/handler.test.ts

View workflow job for this annotation

GitHub Actions / Run tests on Node 18

test/handler.test.ts > redirectSlash > slash=auto redirects files with trailing slash

AssertionError: expected undefined to be '/TEST/trailing' // Object.is equality - Expected: "/TEST/trailing" + Received: undefined ❯ test/handler.test.ts:206:56

Check failure on line 206 in test/handler.test.ts

View workflow job for this annotation

GitHub Actions / Run tests on Node 22

test/handler.test.ts > redirectSlash > slash=auto redirects files with trailing slash

AssertionError: expected undefined to be '/TEST/trailing' // Object.is equality - Expected: "/TEST/trailing" + Received: undefined ❯ test/handler.test.ts:206:56
expect(rs('/section/notrail.html', file('notrail.html'))).toBe(undefined);
expect(rs('/section/trailing.html/', file('trailing.html'))).toBe('/section/trailing.html');
});

test('slash=auto redirects dirs without trailing slash', () => {
const rs = getRs('auto');
expect(rs.dir('/notrail')).toBe('/notrail/');
expect(rs.dir('/trailing/')).toBe(undefined);
expect(rs.dir('/.test/notrail')).toBe('/.test/notrail/');
expect(rs.dir('/.test/trailing/')).toBe(undefined);
expect(rs('/notrail', dir('notrail'))).toBe('/notrail/');
expect(rs('/trailing/', dir('trailing'))).toBe(undefined);
expect(rs('/.test/notrail', dir('.test/notrail'))).toBe('/.test/notrail/');
expect(rs('/.test/trailing/', dir('.test/trailing'))).toBe(undefined);
});
});

Expand Down
Loading

0 comments on commit e9bea89

Please sign in to comment.