Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export function helpPage() {
const options = optionsOrder.map((key) => CLI_OPTIONS[key]);

const section = (heading: string = '', lines: string[] = []) => {
const result = [];
const result: string[] = [];
if (heading.length) result.push(indent + color.style(heading, 'bold'));
if (lines.length) result.push(lines.map((l) => indent.repeat(2) + l).join('\n'));
return result.join('\n\n');
Expand Down
16 changes: 16 additions & 0 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export class RequestHandler {
const body = dirListPage({
root: this.#options.root,
ext: this.#options.ext,
urlMount: urlMountPath(this.#req),
urlPath: this.urlPath ?? '',
filePath,
items,
Expand Down Expand Up @@ -393,3 +394,18 @@ function parseHeaderNames(input: string = ''): string[] {
.map((h) => h.trim())
.filter(isHeader);
}

export function urlMountPath({
baseUrl,
originalUrl,
url,
}: Pick<Request, 'baseUrl' | 'originalUrl' | 'url'>): string | undefined {
const trim = (p = '') => (p.length > 1 ? trimSlash(p, { end: true }) : p);
if (typeof baseUrl === 'string') {
return trim(baseUrl);
} else if (typeof url === 'string' && typeof originalUrl === 'string') {
if (url === '' || url === '/') return trim(originalUrl);
const lastIndex = originalUrl.lastIndexOf(url);
if (lastIndex > 0) return trim(originalUrl.slice(0, lastIndex));
}
}
39 changes: 39 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { RequestHandler } from './handler.ts';
import { serverOptions } from './options.ts';
import { FileResolver } from './resolver.ts';
import type { Request, Response, ServerOptions } from './types.d.ts';
import { errorList } from './utils.ts';

export function middleware(options: Partial<ServerOptions>) {
if (!options || typeof options !== 'object') {
options = {};
}

const onError = errorList();
const fullOptions = serverOptions(
{ root: typeof options.root === 'string' ? options.root : '', ...options },
onError,
);
if (onError.list.length) {
throw new OptionsError(onError.list);
}

const resolver = new FileResolver(fullOptions);

return async function servitsyHandler(req: Request, res: Response, next: (value: any) => void) {
const handler = new RequestHandler({ req, res, resolver, options: fullOptions });
await handler.process();
if (res.statusCode !== 200 && !res.headersSent && typeof next === 'function') {
next(handler.data());
}
};
}

class OptionsError extends Error {
list: string[];
constructor(list: string[]) {
const message = 'Invalid option(s):\n' + list.map((msg) => ` ${msg}`).join('\n');
super(message);
this.list = [...list];
}
}
15 changes: 12 additions & 3 deletions src/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,26 @@ export function errorPage(data: { status: number; url: string; urlPath: string |

export function dirListPage(data: {
root: string;
urlMount?: string;
urlPath: string;
filePath: string;
items: FSLocation[];
ext: string[];
}) {
const { root, urlPath, filePath, items, ext } = data;
const { root, urlMount, urlPath, filePath, items, ext } = data;

const rootName = basename(root);
const trimmedUrl = trimSlash(urlPath);
const trimmedUrl = [urlMount, urlPath]
.map((s) => s && trimSlash(s))
.filter(Boolean)
.join('/');
const baseUrl = trimmedUrl ? `/${trimmedUrl}/` : '/';

const displayPath = decodeURIPathSegments(trimmedUrl ? `${rootName}/${trimmedUrl}` : rootName);
let displayPath = decodeURIPathSegments(trimmedUrl ? `${rootName}/${trimmedUrl}` : rootName);
if (urlMount && urlMount.length > 1) {
displayPath = decodeURIPathSegments(trimmedUrl);
}

const parentPath = dirname(filePath);
const showParent = trimmedUrl !== '';

Expand Down
10 changes: 8 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export type Request = import('node:http').IncomingMessage & { originalUrl?: string };
export type Response = import('node:http').ServerResponse<Request>;
import type { IncomingMessage, ServerResponse } from 'node:http';

export type Request = IncomingMessage & {
baseUrl?: string;
originalUrl?: string;
};

export type Response = ServerResponse<Request>;

export type FSKind = 'dir' | 'file' | 'link' | null;

Expand Down
26 changes: 25 additions & 1 deletion test/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { IncomingMessage, ServerResponse } from 'node:http';
import { Duplex } from 'node:stream';
import { afterAll, expect, suite, test } from 'vitest';

import { extractUrlPath, fileHeaders, isValidUrlPath, RequestHandler } from '../src/handler.ts';
import {
extractUrlPath,
fileHeaders,
isValidUrlPath,
RequestHandler,
urlMountPath,
} from '../src/handler.ts';
import { FileResolver } from '../src/resolver.ts';
import type { HttpHeaderRule, ServerOptions } from '../src/types.d.ts';
import { fsFixture, getBlankOptions, getDefaultOptions, platformSlash } from './shared.ts';
Expand Down Expand Up @@ -418,3 +424,21 @@ suite('RequestHandler', async () => {
});
});
});

suite('urlMountPath', () => {
test('is undefined when baseUrl and originalUrl are undefined', () => {
expect(urlMountPath({})).toBe(undefined);
expect(urlMountPath({ url: '/hello/world' })).toBe(undefined);
});

test('returns the baseUrl when present (express 4+)', () => {
expect(urlMountPath({ baseUrl: '/' })).toBe('/');
expect(urlMountPath({ baseUrl: '/hello' })).toBe('/hello');
expect(urlMountPath({ baseUrl: '/hello/' })).toBe('/hello');
});

test('infers the baseUrl from url and originalUrl (express 3)', () => {
expect(urlMountPath({ originalUrl: '/', url: '/' })).toBe('/');
expect(urlMountPath({ originalUrl: '/static/index.html', url: '/index.html' })).toBe('/static');
});
});