Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: rename dirFile option to index #53

Merged
merged 1 commit into from
Dec 11, 2024
Merged
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
34 changes: 17 additions & 17 deletions doc/options.md
Original file line number Diff line number Diff line change
@@ -5,12 +5,12 @@ servitsy supports the following command-line options:
- [`host`](#host): specify a custom host
- [`port`](#port): specify a custom port or range of ports
- [`cors`](#cors): send CORS HTTP headers in responses
- [`dirFile`](#dirfile): directory index file(s)
- [`dirList`](#dirlist): allow or disallow directory listings
- [`exclude`](#exclude): deny file access by pattern
- [`ext`](#ext): extensions used to resolve URLs
- [`gzip`](#gzip): enable or disable gzip compression
- [`header`](#header): add custom HTTP header(s) to responses
- [`index`](#index): directory index file(s)

> [!NOTE]
> Examples on this page use the `servitsy` command. If you haven't installed servitsy globally, you can use `npx servitsy` instead.
@@ -69,24 +69,9 @@ servitsy --cors
servitsy --no-cors
```

## `dirFile`

File names to look up when a request matches a directory. Defaults to `index.html`.

```sh
# Default value
servitsy --dirfile 'index.html'

# Custom values
servitsy --dirfile 'index.html,index.htm' --dirfile 'page.html,page.htm'

# Disable defaults
servitsy --no-dirfile # or --dirfile=''
```

## `dirList`

Enables or disables listing directory contents, when a request matches a directory and no `dirFile` is found in that directory. Enabled by default.
Enables or disables listing directory contents, when a request matches a directory and no `index` file is found in that directory. Enabled by default.

```sh
# Serve directory listings (same as default)
@@ -175,3 +160,18 @@ servitsy --header '*.rst {"content-type": "text/x-rst"}'
```

See the [`exclude` option](#exclude) for more information about file matching patterns.

## `index`

File names to look up when a request matches a directory. Defaults to `index.html`.

```sh
# Default value
servitsy --index 'index.html'

# Custom values
servitsy --index 'index.html,index.htm' --index 'page.html,page.htm'

# Disable defaults
servitsy --no-index # or --index=''
```
6 changes: 3 additions & 3 deletions src/args.ts
Original file line number Diff line number Diff line change
@@ -20,8 +20,8 @@ const PARSE_ARGS_OPTIONS: ParseArgsConfig['options'] = {
'no-gzip': { type: 'boolean' },
ext: { type: 'string', multiple: true },
'no-ext': { type: 'boolean' },
dirfile: { type: 'string', multiple: true },
'no-dirfile': { type: 'boolean' },
index: { type: 'string', multiple: true },
'no-index': { type: 'boolean' },
dirlist: { type: 'boolean' },
'no-dirlist': { type: 'boolean' },
exclude: { type: 'string', multiple: true },
@@ -129,7 +129,7 @@ export class CLIArgs {
host: this.str('host'),
cors: this.bool('cors'),
gzip: this.bool('gzip'),
dirFile: this.splitList('dirfile'),
index: this.splitList('index'),
dirList: this.bool('dirlist'),
exclude: this.splitList('exclude'),
};
6 changes: 3 additions & 3 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ export const DEFAULT_OPTIONS: Omit<RuntimeOptions, 'root'> = {
cors: false,
headers: [],
dirList: true,
dirFile: ['index.html'],
index: ['index.html'],
ext: ['.html'],
exclude: ['.*', '!.well-known'],
};
@@ -33,13 +33,13 @@ export const CLI_OPTIONS: Record<string, string> = {
'-h, --host': `Specify custom host\n(default: undefined)`,
'-p, --port': `Specify custom port(s)\n(default: '${PORTS_CONFIG.initial}+')`,
'--cors': `Send CORS HTTP headers`,
'--dirfile': `Directory index file(s)\n(default: '${DEFAULT_OPTIONS.dirFile}')`,
'--exclude': `Deny file access by pattern\n(default: '${DEFAULT_OPTIONS.exclude.join(', ')}')`,
'--ext': `Extension(s) used to resolve URLs\n(default: '${DEFAULT_OPTIONS.ext}')`,
'--header': `Add custom HTTP header(s) to responses`,
'--no-dirfile': `Do not serve directory index files`,
'--index': `Directory index file(s)\n(default: '${DEFAULT_OPTIONS.index}')`,
'--no-dirlist': `Do not serve directory listings`,
'--no-exclude': `Disable default file access patterns`,
'--no-ext': `Disable default file extensions`,
'--no-gzip': `Disable gzip compression of text responses`,
'--no-index': `Do not serve directory index files`,
};
10 changes: 5 additions & 5 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ export function serverOptions(
host: validator.host(options.host),
cors: validator.cors(options.cors),
headers: validator.headers(options.headers),
dirFile: validator.dirFile(options.dirFile),
index: validator.index(options.index),
dirList: validator.dirList(options.dirList),
ext: validator.ext(options.ext),
exclude: validator.exclude(options.exclude),
@@ -84,10 +84,6 @@ export class OptionsValidator {
return this.#bool(input, 'invalid cors value');
}

dirFile(input?: string[]): string[] | undefined {
return this.#arr(input, 'invalid dirFile value', isValidPattern);
}

dirList(input?: boolean): boolean | undefined {
return this.#bool(input, 'invalid dirList value');
}
@@ -112,6 +108,10 @@ export class OptionsValidator {
return this.#str(input, 'invalid host value', isValidHost);
}

index(input?: string[]): string[] | undefined {
return this.#arr(input, 'invalid index value', isValidPattern);
}

ports(input?: number[]): number[] | undefined {
if (typeof input === 'undefined') return;
if (!Array.isArray(input)) {
10 changes: 5 additions & 5 deletions src/resolver.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { getLocalPath, isSubpath, PathMatcher, trimSlash } from './utils.ts';
export class FileResolver {
#root: string;
#ext: string[] = [];
#dirFile: string[] = [];
#index: string[] = [];
#dirList = false;
#excludeMatcher: PathMatcher;

@@ -22,8 +22,8 @@ export class FileResolver {
if (Array.isArray(options.ext)) {
this.#ext = options.ext;
}
if (Array.isArray(options.dirFile)) {
this.#dirFile = options.dirFile;
if (Array.isArray(options.index)) {
this.#index = options.index;
}
if (typeof options.dirList === 'boolean') {
this.#dirList = options.dirList;
@@ -110,8 +110,8 @@ export class FileResolver {
const kind = await getKind(filePath);

// Try alternates
if (kind === 'dir' && this.#dirFile.length) {
const paths = this.#dirFile.map((name) => join(filePath, name));
if (kind === 'dir' && this.#index.length) {
const paths = this.#index.map((name) => join(filePath, name));
const match = await this.#locateAltFiles(paths);
if (match) return match;
} else if (kind === null && this.#ext.length) {
4 changes: 2 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ export interface ResMetaData {
export interface ServerOptions {
root: string;
ext?: string[];
dirFile?: string[];
index?: string[];
dirList?: boolean;
exclude?: string[];
host?: string;
@@ -40,7 +40,7 @@ export interface ServerOptions {
export interface RuntimeOptions {
root: string;
ext: string[];
dirFile: string[];
index: string[];
dirList: boolean;
exclude: string[];
host: string | undefined;
50 changes: 25 additions & 25 deletions test/args.test.ts
Original file line number Diff line number Diff line change
@@ -81,8 +81,8 @@ suite('CLIArgs', () => {
--gzip
--ext .html,.htm
--ext md,mdown
--dirfile index.html
--dirfile index.htmlx
--index index.html
--index index.htmlx
--dirlist
--exclude .*,*config
--exclude *rc
@@ -96,7 +96,7 @@ suite('CLIArgs', () => {
expect(args.get('cors')).toBe(true);
expect(args.get('gzip')).toBe(true);
expect(args.get('ext')).toEqual(['.html,.htm', 'md,mdown']);
expect(args.get('dirfile')).toEqual(['index.html', 'index.htmlx']);
expect(args.get('index')).toEqual(['index.html', 'index.htmlx']);
expect(args.get('dirlist')).toBe(true);
expect(args.get('exclude')).toEqual(['.*,*config', '*rc']);
});
@@ -133,15 +133,15 @@ suite('CLIArgs', () => {
const args = new CLIArgs(arr`
--port 123456789
--host localhost --host localesthost
--dirfile index.html
--index index.html
--ext html
--random value1
`);
// specified and configured as string
expect(args.str('port')).toBe('123456789');
expect(args.str('host')).toBe('localesthost');
// configured as multiple strings
expect(args.str('dirfile')).toBe(undefined);
expect(args.str('index')).toBe(undefined);
expect(args.str('ext')).toBe(undefined);
// not configured, defaults to boolean
expect(args.str('random')).toBe(undefined);
@@ -161,14 +161,14 @@ suite('CLIArgs', () => {

test('list accessor only returns arrays', () => {
const args = new CLIArgs(arr`
--dirfile 404.html
--index 404.html
--ext red --ext green --ext blue
--header h1 --header h2
--headers h3 --headers h4
--host host1 --host host2
--random value1 --random value2
`);
expect(args.list('dirfile')).toEqual(['404.html']);
expect(args.list('index')).toEqual(['404.html']);
expect(args.list('ext')).toEqual(['red', 'green', 'blue']);
expect(args.list('header')).toEqual(['h1', 'h2']);
expect(args.list('headers')).toEqual(['h3', 'h4']);
@@ -178,8 +178,8 @@ suite('CLIArgs', () => {

test('list accessor returns empty array for --no- prefix', () => {
const args = new CLIArgs(arr`
--dirfile 404.html
--no-dirfile
--index 404.html
--no-index
--no-exclude
--exclude !*.md --exclude .DS_Store
--ext .html --ext .htm
@@ -189,7 +189,7 @@ suite('CLIArgs', () => {
--header h1 --header h2
--headers h3 --headers h4
`);
expect(args.list('dirfile')).toEqual([]);
expect(args.list('index')).toEqual([]);
expect(args.list('exclude')).toEqual([]);
expect(args.list('ext')).toEqual([]);
expect(args.list('header')).toEqual([]);
@@ -198,12 +198,12 @@ suite('CLIArgs', () => {

test('splitList accessor parses comma-separated lists', () => {
const args = new CLIArgs(arr`
--dirfile index.html,index.htmlx
--dirfile page.html,page.htmlx
--index index.html,index.htmlx
--index page.html,page.htmlx
--ext .html
--ext a,b,,,,c,,d
`);
expect(args.splitList('dirfile')).toEqual([
expect(args.splitList('index')).toEqual([
'index.html',
'index.htmlx',
'page.html',
@@ -213,9 +213,9 @@ suite('CLIArgs', () => {
});

test('splitList returns empty array for empty string and --no- variant', () => {
expect(new CLIArgs([]).splitList('dirfile')).toBe(undefined);
expect(new CLIArgs(['--dirfile', '']).splitList('dirfile')).toEqual([]);
expect(new CLIArgs(['--no-dirfile']).splitList('dirfile')).toEqual([]);
expect(new CLIArgs([]).splitList('index')).toBe(undefined);
expect(new CLIArgs(['--index', '']).splitList('index')).toEqual([]);
expect(new CLIArgs(['--no-index']).splitList('index')).toEqual([]);
expect(new CLIArgs([]).splitList('ext')).toBe(undefined);
expect(new CLIArgs(['--ext', '']).splitList('ext')).toEqual([]);
expect(new CLIArgs(['--no-ext']).splitList('ext')).toEqual([]);
@@ -250,16 +250,16 @@ suite('CLIArgs.options', () => {
expect(error.list).toEqual([]);
});

test('parses --dirfile as a string list', () => {
test('parses --index as a string list', () => {
const error = errorList();
const single = new CLIArgs(arr`--dirfile index.html`);
expect(single.options(error)).toEqual({ dirFile: ['index.html'] });
const single = new CLIArgs(arr`--index index.html`);
expect(single.options(error)).toEqual({ index: ['index.html'] });
const multiple = new CLIArgs(arr`
--dirfile index.html,index.htm
--dirfile page.html,page.htm
--index index.html,index.htm
--index page.html,page.htm
`);
expect(multiple.options(error)).toEqual({
dirFile: ['index.html', 'index.htm', 'page.html', 'page.htm'],
index: ['index.html', 'index.htm', 'page.html', 'page.htm'],
});
expect(error.list).toEqual([]);
});
@@ -292,9 +292,9 @@ suite('CLIArgs.options', () => {
const args = new CLIArgs(arr`
--no-exclude
--no-ext --ext html
--dirfile index.html --no-dirfile
--index index.html --no-index
`);
expect(args.options(error)).toEqual({ ext: [], dirFile: [], exclude: [] });
expect(args.options(error)).toEqual({ ext: [], index: [], exclude: [] });
expect(error.list).toEqual([]);
});

@@ -365,7 +365,7 @@ suite('CLIArgs.unknown', () => {
--cors --no-cors
--gzip --no-gzip
--ext --no-ext
--dirfile --no-dirfile
--index --no-index
--dirlist --no-dirlist
--exclude --no-exclude
`);
16 changes: 8 additions & 8 deletions test/options.test.ts
Original file line number Diff line number Diff line change
@@ -223,9 +223,9 @@ suite('OptionsValidator', () => {
valid(val.gzip, true);
valid(val.gzip, false);

valid(val.dirFile, undefined);
valid(val.dirFile, []);
valid(val.dirFile, ['a b c', 'Indéx.html']);
valid(val.index, undefined);
valid(val.index, []);
valid(val.index, ['a b c', 'Indéx.html']);

valid(val.exclude, undefined);
valid(val.exclude, []);
@@ -267,8 +267,8 @@ suite('OptionsValidator', () => {
const val = new OptionsValidator(throwError);

expect(() => val.cors(null as any)).toThrow(`invalid cors value: null`);
expect(() => val.dirFile({ hello: 'world' } as any)).toThrow(
`invalid dirFile value: {"hello":"world"}`,
expect(() => val.index({ hello: 'world' } as any)).toThrow(
`invalid index value: {"hello":"world"}`,
);
expect(() => val.dirList('yes' as any)).toThrow(`invalid dirList value: 'yes'`);
expect(() => val.exclude(new Set(['index']) as any)).toThrow(`invalid exclude pattern: {}`);
@@ -282,7 +282,7 @@ suite('OptionsValidator', () => {
test('sends errors for invalid inputs', () => {
const val = new OptionsValidator(throwError);

expect(() => val.dirFile(['./index.html'])).toThrow(`invalid dirFile value: './index.html'`);
expect(() => val.index(['./index.html'])).toThrow(`invalid index value: './index.html'`);
expect(() => val.exclude([null] as any)).toThrow(`invalid exclude pattern: null`);
expect(() => val.exclude(['.*', 'a:b:c'])).toThrow(`invalid exclude pattern: 'a:b:c'`);
expect(() => val.ext(['.html', 'htm'])).toThrow(`invalid ext value: 'htm'`);
@@ -325,7 +325,7 @@ suite('serverOptions', () => {
const testOptions2: ServerOptions = {
root: cwd(),
ext: ['.htm', '.TXT'],
dirFile: ['page.md', 'Index Page.html'],
index: ['page.md', 'Index Page.html'],
exclude: ['.htaccess', '*.*.*', '_*'],
headers: [{ include: ['*.md', '*.html'], headers: { dnt: 1 } }],
host: '192.168.1.199',
@@ -343,7 +343,7 @@ suite('serverOptions', () => {
const inputs = {
root: 'this/path/doesnt/exist',
cors: null,
dirFile: [undefined, 'invalid/value', 'C:\\Temp'],
index: [undefined, 'invalid/value', 'C:\\Temp'],
dirList: {},
exclude: [{}, 'section/*.json', 'a:b:c:d', 'no\\pe'],
ext: ['html', 'txt', './index.html', '..'],
Loading
Loading