diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e0b2e7..964ffa3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,15 +16,15 @@ jobs: - &checkout uses: actions/checkout@v6 - &setup-pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: 10 + version: 11 - &setup-node uses: actions/setup-node@v6 with: - cache: 'pnpm' + cache: "pnpm" node-version: ${{ matrix.node }} - - run: pnpm install --frozen-lockfile + - run: pnpm ci - run: pnpm run typecheck test: runs-on: ubuntu-latest @@ -36,5 +36,17 @@ jobs: - *checkout - *setup-pnpm - *setup-node - - run: pnpm install --frozen-lockfile + - run: pnpm ci - run: pnpm test + lint: + runs-on: ubuntu-latest + strategy: + matrix: + node: [24] + name: Lint (node${{ matrix.node }}) + steps: + - *checkout + - *setup-pnpm + - *setup-node + - run: pnpm ci + - run: pnpm run lint diff --git a/.oxfmtrc.json b/.oxfmtrc.json index c65b048..51f70ab 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,10 +1,10 @@ { - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "endOfLine": "lf", - "printWidth": 100, - "singleQuote": true, - "useTabs": true, - "sortImports": true, - "sortPackageJson": false, - "ignorePatterns": [] + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "endOfLine": "lf", + "printWidth": 100, + "singleQuote": true, + "useTabs": true, + "sortImports": true, + "sortPackageJson": false, + "ignorePatterns": [] } diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..f14d6ed --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,25 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["import", "node", "promise"], + "categories": { + "correctness": "error", + "suspicious": "warn", + "restriction": "warn" + }, + "rules": { + "eslint/class-methods-use-this": "off", + "eslint/no-param-reassign": "off", + "eslint/no-undefined": "off", + "eslint/no-use-before-define": "off", + "import/no-relative-parent-imports": "off", + "typescript/consistent-type-imports": "error" + }, + "overrides": [ + { + "files": ["./scripts/**"], + "rules": { + "eslint/no-console": "off" + } + } + ] +} diff --git a/package.json b/package.json index 57ed212..c224cc5 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "prepack": "npm run build && npm test", "build": "node scripts/prebuild.js && tsc -p tsconfig.json --listEmittedFiles && oxfmt --write lib", "format": "oxfmt --write '**/*.{css,js,ts}' '**/*config*.json'", + "lint": "oxlint", "test": "vitest --run test/*.test.ts", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/tsconfig.json --noEmit" }, @@ -40,6 +41,7 @@ "fs-fixture": "^2.12.0", "linkedom": "^0.18.12", "oxfmt": "^0.47.0", + "oxlint": "^1.62.0", "typescript": "~6.0.3", "vitest": "^4.1.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c0b439..1c3efe7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: oxfmt: specifier: ^0.47.0 version: 0.47.0 + oxlint: + specifier: ^1.62.0 + version: 1.62.0 typescript: specifier: ~6.0.3 version: 6.0.3 @@ -172,6 +175,128 @@ packages: cpu: [x64] os: [win32] + '@oxlint/binding-android-arm-eabi@1.62.0': + resolution: {integrity: sha512-pKsthNECyvJh8lPTICz6VcwVy2jOqdhhsp1rlxCkhgZR47aKvXPmaRWQDv+zlXpRae4qm1MaaTnutkaOk5aofg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.62.0': + resolution: {integrity: sha512-b1AUNViByvgmR2xJDubvLIr+dSuu3uraG7bsAoKo+xrpspPvu6RIn6Fhr2JUhobfep3jwUTy18Huco6GkwdvGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.62.0': + resolution: {integrity: sha512-iG+Tvf70UJ6otfwFYIHk36Sjq9cpPP5YLxkoggANNRtzgi3Tj3g8q6Ybqi6AtkU3+yg9QwF7bDCkCS6bbL4PCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.62.0': + resolution: {integrity: sha512-oOWI6YPPr5AJUx+yIDlxmuUbQjS5gZX3OH3QisawYvsZgLiQVvZtR0rPBcJTxLWqt2ClrWg0DlSrlUiG5SQNHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.62.0': + resolution: {integrity: sha512-dLP33T7VLCmLVv4cvjkVX+rmkcwNk2UfxmsZPNur/7BQHoQR60zJ7XLiRvNUawlzn0u8ngCa3itjEG73MAMa/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.62.0': + resolution: {integrity: sha512-fl//LWNks6qo9chNY60UDYyIwtp7a5cEx4Y/rHPjaarhuwqx6jtbzEpD5V5AqmdL4a6Y5D8zeXg5HF2Cr0QmSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.62.0': + resolution: {integrity: sha512-i5vkAuxvueTODV3J2dL61/TXewDHhMFKvtD156cIsk7GsdfiAu7zW7kY0NJXhKeFHeiMZIh7eFNjkPYH6J47HQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.62.0': + resolution: {integrity: sha512-QwN19LLuIGuOjEflSeJkZmOTfBdBMlTmW8xbMf8TZhjd//cxVNYQPq75q7oKZBJc6hRx3gY7sX0Egc8cEIFZYg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.62.0': + resolution: {integrity: sha512-8eCy3FCDuWUM5hWujAv6heMvfZPbcCOU3SdQUAkixZLu5bSzOkNfirJiLGoQFO943xceOKkiQRMQNzH++jM3WA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.62.0': + resolution: {integrity: sha512-NjQ7K7tpTPDe9J+yq8p/s/J0E7lRCkK2uDBDqvT4XIT6f4Z0tlnr59OBg/WcrmVHER1AbrcfyxhGTXgcG8ytWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.62.0': + resolution: {integrity: sha512-oKZed9gmSwze29dEt3/Wnsv6l/Ygw/FUst+8Kfpv2SGeS/glEoTGZAMQw37SVyzFV76UTHJN2snGgxK2t2+8ow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.62.0': + resolution: {integrity: sha512-gBjBxQ+9lGpAYq+ELqw0w8QXsBnkZclFc7GRX2r0LnEVn3ZTEqeIKpKcGjucmp76Q53bvJD0i4qBWBhcfhSfGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.62.0': + resolution: {integrity: sha512-Ew2Kxs9EQ9/mbAIJ2hvocMC0wsOu6YKzStI2eFBDt+Td5O8seVC/oxgRIHqCcl5sf5ratA1nozQBAuv7tphkHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.62.0': + resolution: {integrity: sha512-5z25jcAA0gfKyVwz71A0VXgaPlocPoTAxhlv/hgoK6tlCrfoNuw7haWbDHvGMfjXhdic4EqVXGRv5XsTqFnbRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.62.0': + resolution: {integrity: sha512-IWpHmMB6ZDllPvqWDkG6AmXrN7JF5e/c4g/0PuURsmlK+vHoYZPB70rr4u1bn3I4LsKCSpqqfveyx6UCOC8wdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.62.0': + resolution: {integrity: sha512-fjlSxxrD5pA594vkyikCS9MnPRjQawW6/BLgyTYkO+73wwPlYjkcZ7LSd974l0Q2zkHQmu4DPvJFLYA7o8xrxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.62.0': + resolution: {integrity: sha512-EiFXr8loNS0Ul3Gu80+9nr1T8jRmnKocqmHHg16tj5ZqTgUXyb97l2rrspVHdDluyFn9JfR4PoJFdNzw4paHww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.62.0': + resolution: {integrity: sha512-IgOFvL73li1bFgab+hThXYA0N2Xms2kV2MvZN95cebV+fmrZ9AVui1JSxfeeqRLo3CpPxKZlzhyq4G0cnaAvIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.62.0': + resolution: {integrity: sha512-6hMpyDWQ2zGA1OXFKBrdYMUveUCO8UJhkO6JdwZPd78xIdHZNhjx+pib+4fC2Cljuhjyl0QwA2F3df/bs4Bp6A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/binding-android-arm64@1.0.0-rc.17': resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -502,6 +627,16 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + oxlint@1.62.0: + resolution: {integrity: sha512-1uFkg6HakjsGIpW9wNdeW4/2LOHW9MEkoWjZUTUfQtIHyLIZPYt00w3Sg+H3lH+206FgBPHBbW5dVE5l2ExECQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.18.0' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -742,6 +877,63 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.47.0': optional: true + '@oxlint/binding-android-arm-eabi@1.62.0': + optional: true + + '@oxlint/binding-android-arm64@1.62.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.62.0': + optional: true + + '@oxlint/binding-darwin-x64@1.62.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.62.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.62.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.62.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.62.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.62.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.62.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.62.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.62.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.62.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.62.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.62.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.62.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.62.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.62.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.62.0': + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true @@ -1017,6 +1209,28 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.47.0 '@oxfmt/binding-win32-x64-msvc': 0.47.0 + oxlint@1.62.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.62.0 + '@oxlint/binding-android-arm64': 1.62.0 + '@oxlint/binding-darwin-arm64': 1.62.0 + '@oxlint/binding-darwin-x64': 1.62.0 + '@oxlint/binding-freebsd-x64': 1.62.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.62.0 + '@oxlint/binding-linux-arm-musleabihf': 1.62.0 + '@oxlint/binding-linux-arm64-gnu': 1.62.0 + '@oxlint/binding-linux-arm64-musl': 1.62.0 + '@oxlint/binding-linux-ppc64-gnu': 1.62.0 + '@oxlint/binding-linux-riscv64-gnu': 1.62.0 + '@oxlint/binding-linux-riscv64-musl': 1.62.0 + '@oxlint/binding-linux-s390x-gnu': 1.62.0 + '@oxlint/binding-linux-x64-gnu': 1.62.0 + '@oxlint/binding-linux-x64-musl': 1.62.0 + '@oxlint/binding-openharmony-arm64': 1.62.0 + '@oxlint/binding-win32-arm64-msvc': 1.62.0 + '@oxlint/binding-win32-ia32-msvc': 1.62.0 + '@oxlint/binding-win32-x64-msvc': 1.62.0 + pathe@2.0.3: {} picocolors@1.1.1: {} diff --git a/src/args.ts b/src/args.ts index e18905c..b484f71 100644 --- a/src/args.ts +++ b/src/args.ts @@ -136,9 +136,9 @@ export class CLIArgs { // args that require extra parsing const port = this.str('port') ?? this.str('ports'); - if (port != null) { + if (typeof port === 'string') { const value = parsePort(port); - if (value != null) options.ports = value; + if (Array.isArray(value)) options.ports = value; else invalid('--port', port); } @@ -150,13 +150,13 @@ export class CLIArgs { if (!rule) invalid('--header', value); return rule; }) - .filter((rule) => rule != null); + .filter((rule) => rule !== undefined); if (headers.length) { options.headers = headers; } const ext = this.splitList('ext'); - if (ext != null) { + if (Array.isArray(ext)) { options.ext = ext.map((item) => normalizeExt(item)); } @@ -165,7 +165,11 @@ export class CLIArgs { } // remove undefined values - return Object.fromEntries(Object.entries(options).filter((entry) => entry[1] != null)); + return Object.fromEntries( + Object.entries(options).filter(([_key, val]) => { + return val !== undefined && val !== null; + }), + ); } unknown(): string[] { @@ -210,7 +214,7 @@ export function parseHeaders(input: string): HttpHeaderRule | undefined { const json = input.slice(jsonStart); try { const obj = JSON.parse(json); - if (obj != null && typeof obj === 'object') { + if (obj !== null && typeof obj === 'object') { const entries = Object.entries(obj) .map(([key, val]) => [ typeof key === 'string' ? key.trim() : '', @@ -221,7 +225,9 @@ export function parseHeaders(input: string): HttpHeaderRule | undefined { return makeHeadersRule(include, entries); } } - } catch {} + } catch { + // oxlint-disable no-empty + } } // parse header:value syntax diff --git a/src/content-type.ts b/src/content-type.ts index 40fe2de..ab899a8 100644 --- a/src/content-type.ts +++ b/src/content-type.ts @@ -250,7 +250,7 @@ export function isBinHeader(bytes: Uint8Array): boolean { return false; } - for (let i = 0; i < limit; i++) { + for (let i = 0; i < limit; i += 1) { if (isBinDataByte(bytes[i])) { return true; } diff --git a/src/fs-utils.ts b/src/fs-utils.ts index dc3fee5..bf38601 100644 --- a/src/fs-utils.ts +++ b/src/fs-utils.ts @@ -12,6 +12,7 @@ export async function checkDirAccess( const stats = await stat(dirPath); if (stats.isDirectory()) { // needs r-x permissions to access contents of the directory + // oxlint-disable no-bitwise await access(dirPath, constants.R_OK | constants.X_OK); return true; } else { @@ -62,18 +63,15 @@ export async function getRealpath(filePath: string): Promise { } } -export async function isReadable(filePath: string, kind?: FSKind): Promise { - if (kind === undefined) { - kind = await getKind(filePath); - } - if (kind === 'dir' || kind === 'file' || kind === 'link') { - const mode = kind === 'dir' ? constants.R_OK | constants.X_OK : constants.R_OK; - return access(filePath, mode).then( - () => true, - () => false, - ); - } - return false; +export async function isReadable(filePath: string, asKind?: FSKind): Promise { + const kind = asKind === undefined ? await getKind(filePath) : asKind; + if (typeof kind !== 'string') return false; + + const mode = kind === 'dir' ? constants.R_OK | constants.X_OK : constants.R_OK; + return access(filePath, mode).then( + () => true, + () => false, + ); } function statsKind(stats: { diff --git a/src/handler.ts b/src/handler.ts index 7ab3440..b75d0e8 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,5 +1,5 @@ import { Buffer } from 'node:buffer'; -import { createReadStream } from 'node:fs'; +import { createReadStream, type ReadStream } from 'node:fs'; import { open, stat, type FileHandle } from 'node:fs/promises'; import { createGzip, gzipSync } from 'node:zlib'; @@ -25,7 +25,7 @@ interface Config { } interface Payload { - body?: string | Buffer | import('node:fs').ReadStream; + body?: string | Buffer | ReadStream; contentType?: string; isText?: boolean; statSize?: number; @@ -94,7 +94,7 @@ export class RequestHandler { return this.#send(); } - if (this.urlPath == null) { + if (typeof this.urlPath !== 'string') { this.status = 400; return this.#sendErrorPage(); } @@ -155,7 +155,7 @@ export class RequestHandler { this.status = 204; } // read file as stream - else if (this.method !== 'HEAD' && !this.#options._noStream) { + else if (this.method !== 'HEAD' && this.#options.disableStreaming !== true) { data.body = createReadStream(filePath, { autoClose: true, start: 0 }); } @@ -237,7 +237,7 @@ export class RequestHandler { this.#header('content-length', String(statSize)); } - if (isHead || body == null) { + if (isHead || !body) { this.#res.end(); return; } @@ -289,7 +289,7 @@ export class RequestHandler { } const localPath = getLocalPath(this.#options.root, filePath); - if (localPath != null && headerRules.length) { + if (typeof localPath === 'string' && headerRules.length) { const blockList = ['content-encoding', 'content-length']; for (const { name, value } of fileHeaders(localPath, headerRules, blockList)) { this.#header(name, value, false); @@ -333,9 +333,9 @@ function canCompress({ isText?: boolean; statSize?: number; }): boolean { - accept = Array.isArray(accept) ? accept.join(',') : accept; + const values = Array.isArray(accept) ? accept.join(',') : accept; if (isText && statSize <= MAX_COMPRESS_SIZE && accept) { - return accept + return values .toLowerCase() .split(',') .some((value) => value.split(';')[0].trim() === 'gzip'); diff --git a/src/logger.ts b/src/logger.ts index a8d8c97..f4255e0 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -123,7 +123,7 @@ export function requestLogLine({ const duration = start && close ? Math.ceil(close - start) : undefined; let displayPath = _(urlPath ?? url, 'cyan'); - if (isSuccess && urlPath != null && localPath != null) { + if (isSuccess && urlPath !== null && localPath !== null) { const parts = pathSuffix(urlPath, localPath); if (parts) displayPath = _(parts[0], 'cyan') + brackets(parts[1], 'dim,gray,dim'); } diff --git a/src/options.ts b/src/options.ts index 2e538d1..48b31b2 100644 --- a/src/options.ts +++ b/src/options.ts @@ -32,7 +32,7 @@ export function serverOptions( (final as Record)[key] = value; } } - if (final.host == null && getRuntime() === 'webcontainer') { + if (typeof final.host !== 'string' && getRuntime() === 'webcontainer') { final.host = 'localhost'; } @@ -148,22 +148,21 @@ export function isValidHeader(name: string): boolean { return typeof name === 'string' && /^[a-z\d-_]+$/i.test(name); } -/** @type {(value: any) => value is HttpHeaderRule} */ export function isValidHeaderRule(value: unknown): value is HttpHeaderRule { if (!value || typeof value !== 'object') return false; const { include, headers } = value as any; if (typeof include !== 'undefined' && !isStringArray(include)) { return false; } - if (headers == null || typeof headers !== 'object') { + if (headers === null || typeof headers !== 'object') { return false; } const entries = Object.entries(headers); return ( entries.length > 0 && - entries.every(([key, value]) => { + entries.every(([key, val]) => { if (!isValidHeader(key)) return false; - return typeof value === 'string' || typeof value === 'boolean' || Number.isFinite(value); + return typeof val === 'string' || typeof val === 'boolean' || Number.isFinite(val); }) ); } diff --git a/src/pages.ts b/src/pages.ts index 486424a..6e2a668 100644 --- a/src/pages.ts +++ b/src/pages.ts @@ -177,5 +177,6 @@ function html(input: string) { } function nl2sp(input: string) { + /* oxlint-disable no-control-regex */ return input.replace(/[\u{000A}-\u{000D}\u{2028}]/gu, ' '); } diff --git a/src/resolver.ts b/src/resolver.ts index 39f078b..3b51388 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -36,20 +36,24 @@ export class FileResolver { allowedPath(filePath: string): boolean { const localPath = getLocalPath(this.#root, filePath); - if (localPath == null) return false; - return this.#excludeMatcher.test(localPath) === false; + return typeof localPath === 'string' && this.#excludeMatcher.test(localPath) === false; } async find(localPath: string): Promise<{ status: number; file: FSLocation | null }> { + let file: FSLocation | null = null; const targetPath = this.resolvePath(localPath); - let file: FSLocation | null = targetPath != null ? await this.locateFile(targetPath) : null; + if (typeof targetPath === 'string') { + file = await this.locateFile(targetPath); + } // Resolve symlink if (file?.kind === 'link') { const realPath = await getRealpath(file.filePath); - const real = realPath != null ? await this.locateFile(realPath) : null; - if (real?.kind === 'file' || real?.kind === 'dir') { - file = real; + if (typeof realPath === 'string') { + const real = await this.locateFile(realPath); + if (real !== null && (real.kind === 'file' || real.kind === 'dir')) { + file = real; + } } } @@ -67,7 +71,7 @@ export class FileResolver { if (!this.#list) return []; const items: FSLocation[] = (await getIndex(dirPath)).filter( - (item) => item.kind != null && this.allowedPath(item.filePath), + (item) => typeof item.kind === 'string' && this.allowedPath(item.filePath), ); items.sort((a, b) => a.filePath.localeCompare(b.filePath)); @@ -77,7 +81,7 @@ export class FileResolver { // resolve symlinks if (item.kind === 'link') { const filePath = await getRealpath(item.filePath); - if (filePath != null && this.withinRoot(filePath)) { + if (typeof filePath === 'string' && this.withinRoot(filePath)) { const kind = await getKind(filePath); item.target = { filePath, kind }; } diff --git a/src/types.d.ts b/src/types.d.ts index ac9192e..c434f19 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,5 +1,7 @@ -export type Request = import('node:http').IncomingMessage & { originalUrl?: string }; -export type Response = import('node:http').ServerResponse; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +export type Request = IncomingMessage & { originalUrl?: string }; +export type Response = ServerResponse; export type FSKind = 'dir' | 'file' | 'link' | null; @@ -48,5 +50,5 @@ export interface RuntimeOptions { headers: HttpHeaderRule[]; cors: boolean; gzip: boolean; - _noStream?: boolean; + disableStreaming?: boolean; } diff --git a/src/utils.ts b/src/utils.ts index 1124fb1..6b6af52 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -15,7 +15,7 @@ export class PathMatcher { const isNegative = input.startsWith('!'); const trimmedInput = input.slice(isNegative ? 1 : 0); const pattern = trimmedInput.length > 0 ? this.#parse(trimmedInput) : null; - if (pattern != null) { + if (pattern !== null) { (isNegative ? this.#negative : this.#positive).push(pattern); } } @@ -52,7 +52,7 @@ export class PathMatcher { return pattern === value; } else if (pattern.test(value)) { const matches = value.match(pattern); - return matches != null && matches[0] === value; + return matches !== null && matches[0] === value; } return false; } @@ -75,7 +75,6 @@ export class PathMatcher { } export function clamp(value: number, min: number, max: number): number { - if (typeof value !== 'number') value = min; return Math.min(max, Math.max(min, value)); } @@ -175,12 +174,14 @@ export function trimSlash( input: string = '', config: { start?: boolean; end?: boolean } = { start: true, end: true }, ) { - if (config.start === true) input = input.replace(/^[/\\]/, ''); - if (config.end === true) input = input.replace(/[/\\]$/, ''); - return input; + let out = input; + if (config.start === true) out = out.replace(/^[/\\]/, ''); + if (config.end === true) out = out.replace(/[/\\]$/, ''); + return out; } export function withResolvers() { + // oxlint-disable no-empty-function const noop = () => {}; let resolve: (value: T | PromiseLike) => void = noop; let reject: (reason?: any) => void = noop; diff --git a/test/handler.test.ts b/test/handler.test.ts index 5b4c15b..eba16f4 100644 --- a/test/handler.test.ts +++ b/test/handler.test.ts @@ -179,7 +179,6 @@ suite('RequestHandler', async () => { }); const blankOptions = getBlankOptions(path()); const defaultOptions = getDefaultOptions(path()); - const request = handlerContext(defaultOptions); afterAll(() => fixture.rm()); @@ -194,6 +193,7 @@ suite('RequestHandler', async () => { for (const method of ['PUT', 'DELETE']) { test(`${method} method is unsupported`, async () => { + const request = handlerContext(defaultOptions); const handler = request(method, '/README.md'); expect(handler.method).toBe(method); expect(handler.status).toBe(200); @@ -209,6 +209,7 @@ suite('RequestHandler', async () => { } test('GET resolves a request with an index file', async () => { + const request = handlerContext(defaultOptions); const handler = request('GET', '/'); await handler.process(); @@ -241,6 +242,8 @@ suite('RequestHandler', async () => { }); test('GET returns a 404 for an unknown path', async () => { + const request = handlerContext(defaultOptions); + const control = request('GET', '/index.html'); await control.process(); expect(control.status).toBe(200); @@ -254,6 +257,8 @@ suite('RequestHandler', async () => { }); test('GET finds .html files without extension', async () => { + const request = handlerContext(defaultOptions); + const page1 = request('GET', '/section/page'); await page1.process(); expect(page1.status).toBe(200); @@ -266,6 +271,8 @@ suite('RequestHandler', async () => { }); test('GET shows correct content-type', async () => { + const request = handlerContext(defaultOptions); + const checkType = async (url = '', contentType = '') => { const handler = request('GET', url); await handler.process(); @@ -283,6 +290,8 @@ suite('RequestHandler', async () => { }); test('POST is handled as GET', async () => { + const request = handlerContext(defaultOptions); + const cases = [ { url: '/', localPath: 'index.html', status: 200 }, { url: '/manifest.json', localPath: 'manifest.json', status: 200 }, @@ -309,8 +318,10 @@ suite('RequestHandler', async () => { }); test('HEAD with a 200 response', async () => { + const request = handlerContext(defaultOptions); const handler = request('HEAD', '/'); await handler.process(); + expect(handler.method).toBe('HEAD'); expect(handler.status).toBe(200); expect(handler.localPath).toBe('index.html'); @@ -319,8 +330,10 @@ suite('RequestHandler', async () => { }); test('HEAD with a 404 response', async () => { + const request = handlerContext(defaultOptions); const handler = request('HEAD', '/doesnt/exist'); await handler.process(); + expect(handler.method).toBe('HEAD'); expect(handler.status).toBe(404); expect(handler.file).toBe(null); @@ -329,8 +342,10 @@ suite('RequestHandler', async () => { }); test('OPTIONS *', async () => { + const request = handlerContext(defaultOptions); const handler = request('OPTIONS', '*'); await handler.process(); + expect(handler.method).toBe('OPTIONS'); expect(handler.status).toBe(204); checkHeaders(handler.headers, { @@ -340,8 +355,10 @@ suite('RequestHandler', async () => { }); test('OPTIONS for existing file', async () => { + const request = handlerContext(defaultOptions); const handler = request('OPTIONS', '/section/page'); await handler.process(); + expect(handler.method).toBe('OPTIONS'); expect(handler.status).toBe(204); checkHeaders(handler.headers, { @@ -351,8 +368,10 @@ suite('RequestHandler', async () => { }); test('OPTIONS for missing file', async () => { + const request = handlerContext(defaultOptions); const handler = request('OPTIONS', '/doesnt/exist'); await handler.process(); + expect(handler.status).toBe(404); checkHeaders(handler.headers, { allow: allowMethods, diff --git a/test/resolver.test.ts b/test/resolver.test.ts index 5fcd6f4..01a939a 100644 --- a/test/resolver.test.ts +++ b/test/resolver.test.ts @@ -26,6 +26,7 @@ suite('FileResolver.#root', () => { test('throws when root is not defined', () => { expect(() => { // @ts-expect-error + // oxlint-disable no-new new FileResolver({}); }).toThrow(/Missing root directory/); }); @@ -190,8 +191,8 @@ suite('FileResolver.find', async () => { test('default options block dotfiles', async () => { const resolver = new TestFileResolver(defaultOptions); const check = async (url: string, expected: string) => { - const { status, file } = await resolver.find(url); - const result = `${status} ${file ? getLocalPath(defaultOptions.root, file.filePath) : null}`; + const { status, file: f } = await resolver.find(url); + const result = `${status} ${f ? getLocalPath(defaultOptions.root, f.filePath) : null}`; expect(result).toBe(platformSlash(expected)); }; diff --git a/test/shared.ts b/test/shared.ts index b1d9b9c..15cc420 100644 --- a/test/shared.ts +++ b/test/shared.ts @@ -1,7 +1,7 @@ import { join, resolve, sep as dirSep } from 'node:path'; import { cwd } from 'node:process'; -import { createFixture } from 'fs-fixture'; +import { createFixture, type FileTree } from 'fs-fixture'; import { DEFAULT_OPTIONS } from '../src/constants.ts'; import type { FSLocation, RuntimeOptions } from '../src/types.d.ts'; @@ -9,7 +9,7 @@ import { trimSlash } from '../src/utils.ts'; export const loc = testPathUtils(join(cwd(), '_servitsy_test_')); -export async function fsFixture(fileTree: import('fs-fixture').FileTree) { +export async function fsFixture(fileTree: FileTree) { const fixture = await createFixture(fileTree); return { fileTree, fixture, ...testPathUtils(fixture.path) }; }