Skip to content

refactor: add shared TypeScript types, ambient declarations, and typed test helper - #610

Open
bkendall wants to merge 13 commits into
mainfrom
prioritize_js_to_ts
Open

bkendall wants to merge 13 commits into
mainfrom
prioritize_js_to_ts

Conversation

@bkendall

@bkendall bkendall commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Prepares the codebase for incremental .js -> .ts migration (Phase 0):

  • DefinitelyTyped @types/* packages: Adds @types/compression, @types/concat-stream, @types/destroy, @types/is-url, @types/morgan, @types/on-finished, @types/on-headers, and @types/std-mocks to devDependencies (and updates package-lock.json).
  • Ambient declarations for remaining untyped packages: Adds src/modules.d.ts covering the 3 packages without @types/* definitions (basic-auth-connect, router, update-notifier-cjs).
  • Shared configuration & request/response types: Extends Configuration, Rewrite, Redirect, and Header (with Matcher requiring at least one of source, glob, or regex) in src/config.ts; exports SuperstaticRequest and SuperstaticResponse from src/options.ts; updates src/middleware/env.ts to use them, default spec to {}, and return early after serving /__/env.json or /__/env.js (noted in changelog.txt).
  • Typed middleware test helper: Converts test/helpers.js to test/helpers.ts with typed decorator and SuperstaticMiddlewareHandler exports compatible with both ESM .spec.ts and CommonJS .spec.js suites.

Verification

  • tsc --noEmit: 0 errors
  • tsc --build tsconfig.publish.json: 0 errors
  • npm install --package-lock-only --ignore-scripts: lockfile up to date
  • eslint: 0 errors (0 warnings on all added/modified TypeScript files)
  • mocha "test/unit/**" "test/integration/**": 234 passing, 15 pending

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors configuration types, exports SuperstaticRequest and SuperstaticResponse interfaces, adds TypeScript declaration files for external modules, and converts the test decorator helper to TypeScript. The review feedback suggests several key improvements: returning early in the env middleware to prevent calling next() after a response is handled, using union types to ensure at least one matcher is defined in Rewrite/Redirect/Header interfaces, making the env property optional in the middleware spec, and passing spec directly in the test decorator to preserve default parameters.

Comment thread src/middleware/env.ts Resolved
Comment thread src/config.ts Resolved
Comment thread src/middleware/env.ts Resolved
Comment thread test/helpers.ts Resolved
…fig matchers

- Return early after serving /__/env.json or /__/env.js without calling next()
- Add breaking change note to changelog.txt
- Require at least one matcher (source, glob, or regex) on Rewrite, Redirect, and Header
- Default env middleware spec to {} with optional env property

@joehan joehan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #610 (refactor: add shared TypeScript types, ambient declarations, and typed test helper)

Strengths & LGTM Aspects

  • Strong foundation for the JS to TS migration across superstatic.
  • Solid type modeling in src/config.ts for cleanUrls, trailingSlash, headers, redirects, and rewrites.
  • Typed helper decorator in test/helpers.ts makes test cases much easier to follow and typesafe.

Key Findings to Address

  1. src/middleware/env.ts module export mismatch: export function env mixed with module.exports = env causes export declare function env to be emitted in .d.ts while exports is overwritten at runtime, crashing TypeScript callers with TypeError.
  2. Packaging / declaration issue: Moving @types/connect to devDependencies causes emitted .d.ts files that reference connect to fail compilation in downstream projects that do not depend on @types/connect (or degrade superstatic() to any under skipLibCheck).
  3. Rewrite type rejects custom rewriters: Firebase Hosting supports function and run rewrites (used in firebase-tools), but Rewrite currently only models destination. Also destination being optional allows targetless rewrites that crash at runtime.
  4. MiddlewareOptions.env: string bug: Passing a string filename compiles cleanly, but env.ts does not load disk files and instead spreads string characters into object properties on /__/env.json.
  5. Ambient basic-auth-connect declaration: pass?: string is marked optional, but the library throws synchronously if password is not a string.

Comment thread src/middleware/env.ts Outdated
* @returns middleware.
*/
export function env(spec: { env: Record<string, string> }) {
export function env(spec: { env?: Record<string, string> } = {}) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [P0 Runtime Crash] Mixed ES export and module.exports breaks TS callers

Line 36 declares export function env(...) while line 63 has module.exports = env;.
When compiling with tsconfig.publish.json, TypeScript emits export declare function env(...) in lib/middleware/env.d.ts (ignoring module.exports), but in the emitted CJS lib/middleware/env.js line 63 overwrites module.exports = env. At runtime, exports.env is undefined.

TypeScript consumers using import { env } from "superstatic/lib/middleware/env" compile with 0 errors and crash at runtime:

TypeError: (0 , env_1.env) is not a function

Consumers using import env from ... fail tsc with TS2349: Type 'typeof import(".../env")' has no call signatures.

Also note line 45 accesses req.superstatic.env without optional chaining (req.superstatic?.env), which will throw TypeError if mounted standalone.

Suggested fix:

function env(spec?: MiddlewareOptions | Record<string, string>): connect.NextHandleFunction {
  // ...
  const config = req.superstatic?.env;
  // ...
}

export = env;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: replaced mixed export with export = env; + export namespace env { export interface EnvOptions ... } so both CommonJS require() and TS import callers work, guarded req.superstatic?.env for standalone mounting, widened handler parameters to connect.IncomingMessage & { superstatic?: Configuration } and ServerResponse & { superstatic?: Responder }, and added standalone test coverage.

Comment thread package.json Outdated
"@types/chai-as-promised": "^8.0.2",
"@types/compression": "^1.8.1",
"@types/concat-stream": "^2.0.3",
"@types/connect": "^3.4.35",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [P1 Packaging / Declaration Defect] @types/connect in devDependencies breaks .d.ts consumers

Emitted declaration files (lib/options.d.ts, lib/server.d.ts, and lib/superstatic.d.ts) import types from "connect". Since connect ships no bundled types, downstream consumers installing superstatic:

  1. Under skipLibCheck: false (or strict monorepo builds): fail tsc with TS2307: Cannot find module 'connect' or its corresponding type declarations.
  2. Under skipLibCheck: true (e.g. firebase-tools, which does not list @types/connect in its dependencies): unresolved connect imports silently degrade superstatic() and server() return types to any.

Because connect types leak across superstatic's public declaration boundary, @types/connect should be listed in "dependencies", or src/options.ts can declare structural handler types ((req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: moved @types/connect from devDependencies to dependencies (and updated package-lock.json) so emitted .d.ts declarations referencing connect resolve cleanly for downstream TypeScript consumers.

Comment thread src/config.ts Outdated

export type Rewrite = Matcher & {
destination?: string;
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [P1 Type Modeling] Rewrite rejects function/run rewrites and allows targetless rewrites

export type Rewrite = Matcher & { destination?: string }; causes two issues:

  1. Valid Firebase Hosting rewrites used by firebase-tools (e.g. { source: "/api/**", function: "api" } or { source: "/run/**", run: { serviceId: "svc" } }) fail compilation with TS2353: Object literal may only specify known properties.
  2. Because destination is optional, a rewrite with { source: "/foo" } compiles cleanly, but crashes at runtime with HTTP 500 (Error: Unable to find a matching rewriter).

Suggested fix:

export type RewriteTarget =
  | { destination: string }
  | { function: string | { functionId: string; region?: string; pinTag?: boolean } }
  | { run: { serviceId: string; region?: string; pinTag?: boolean } }
  | { dynamicLinks: boolean }
  | ({ destination?: string } & Record<string, unknown>);

export type Rewrite = Matcher & RewriteTarget;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: added the RewriteTarget union supporting destination, function, run, dynamicLinks, and custom rewriter objects, and defined export type Rewrite = Matcher & RewriteTarget;.

Comment thread src/options.ts
import { Configuration } from "./config";
import Responder from "./responder";

export interface MiddlewareOptions {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [P1 Type / Runtime Mismatch] MiddlewareOptions.env: string spreads string characters on /__/env.json

env?: string | Record<string, string>; allows passing a file path string. But neither superstatic() nor env.ts loads files from disk when spec.env is a string (the CLI in src/cli/index.ts parses .env.json itself).
If a consumer passes superstatic({ env: ".env.json", config: {} }), env.ts executes Object.assign({}, req.superstatic.env, spec.env) and serves {"0":".","1":"e","2":"n","3":"v",...} at /__/env.json.

env should be typed Record<string, string> (or a loader function).

Also, a few other runtime options are omitted from MiddlewareOptions:

  • provider?: Provider (checked in src/superstatic.js)
  • allowedHeaders?: string[] (checked in headers.ts)
  • compression?: boolean | connect.HandleFunction (checked as function in src/superstatic.js)
  • before?: { [key: string]: connect.HandleFunction | connect.HandleFunction[] } (activator supports arrays)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: removed string from MiddlewareOptions.env (now Record<string, string>), added missing runtime options (provider, allowedHeaders, compression, before, and array support on after), and exported shared provider/responder directive types from src/options.ts so downstream modules can share a single canonical definition.

Comment thread src/modules.d.ts Outdated

function basicAuth(
user: string,
pass?: string,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P2 Ambient Declaration] basic-auth-connect requires pass: string

In basic-auth-connect, line 51 explicitly throws new Error("password argument required") if typeof password !== "string".
Typing pass?: string as optional hides runtime server crashes when colon-less credentials are provided to basic auth.

Suggested change
pass?: string,
(user: string, pass: string): connect.NextHandleFunction;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: changed pass from optional to required pass: string in the basic-auth-connect declaration to match its runtime validation.

Comment thread test/helpers.ts Outdated
): void {
const superstaticReq = req as SuperstaticRequest;
superstaticReq.superstatic = config ?? {};
middleware((spec ?? {}) as Spec)(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [P2 Test Helper] Missing return drops promises from async middleware

middleware((spec ?? {}) as Spec)(req, res, ...) drops the return value. For async middlewares (like files or redirects), any returned Promise is discarded. Adding return preserves returned promises in tests.

Also, spec ?? {} masks testing default parameters when spec is omitted, and as Spec bypasses type checking for required properties.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: added return before middleware(...)(...) (with handler return type : unknown) so promises from async middlewares are preserved in tests. Kept spec ?? {} in helpers.decorator for now because unconverted JS middleware factories (headers, protect, missing, not-found) still access spec.<prop> without default parameters when tests omit spec; we can drop spec ?? {} once those middlewares migrate.

});

it("does not call next after serving env json", (done) => {
app.use(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: Assert rendered body for /__/env.js

The new test checks status code and Content-Type, but does not assert that the response body contains window.__env = .... Adding a body assertion ensures the template payload is formatted as expected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0c71e06: added a rendered body assertion verifying the interpolated templates/env.js.template output on /__/env.js.

bkendall and others added 10 commits November 4, 2026 13:26
* refactor(providers): convert memory provider to TypeScript

* fix(providers): preserve falsy store lookup behavior in memory provider

* fix(providers): address PR review feedback on memory provider
…cript (#613)

* refactor(middleware): convert not-found middleware and tests to TypeScript

* fix(middleware): guard against null spec with optional chaining

* fix(middleware): fallback on empty errorPage and export NotFoundOptions
* refactor(utils): convert patterns to TypeScript

Converts src/utils/patterns.js to TypeScript with typed pattern matching utilities and adds unit tests in test/unit/utils/patterns.spec.ts.

* fix(utils): address PR review feedback on patterns typing and tests
* refactor(middleware): convert files middleware to TypeScript

Converts src/middleware/files.js to TypeScript (src/middleware/files.ts) and updates test/unit/middleware/files.spec.ts to rely on typed module exports.

- Types request/response/next handling with SuperstaticRequest, SuperstaticResponse, Configuration, and provider result types.
- Preserves compatibility with ES default imports and CommonJS require("./files") in src/middleware/index.js.

* fix(middleware): guard missing handleError in files error handler

* fix(middleware): address PR review feedback on files middleware
…es to TypeScript (#615)

* refactor(utils): convert patterns to TypeScript

Converts src/utils/patterns.js to TypeScript with typed pattern matching utilities and adds unit tests in test/unit/utils/patterns.spec.ts.

* fix(utils): address PR review feedback on patterns typing and tests

* refactor(middleware): convert protect, headers, and missing middlewares to TypeScript

Converts src/middleware/protect.js, src/middleware/headers.js, src/middleware/missing.js, and their unit specs to TypeScript (src/middleware/protect.ts, src/middleware/headers.ts, src/middleware/missing.ts, test/unit/middleware/headers.spec.ts, and test/unit/middleware/missing.spec.ts).

- Reuses Configuration, Header, Matcher, SuperstaticRequest, and SuperstaticResponse types and patterns.configMatcher from src/utils/patterns.ts.
- Defaults spec = {} across protect, headers, and missing factories and preserves CommonJS middleware exports for unconverted JS callers.

* refactor(middleware): address review feedback on headers middleware

- Guard headerToSet?.key before calling toLowerCase() when filtering with allowedHeaders
- Simplify normalizedConfigHeaders object construction with conditional source spread
- Replace deprecated url.parse with WHATWG URL in extractPathname with double-slash and invalid-URL handling

* refactor(middleware): address PR review feedback on protect, headers, and missing

- Parse only absolute URIs via URL() in headers extractPathname so backslash paths and decoded non-ASCII error paths normalize consistently via slashPath
- Normalize entry.glob alongside entry.source in normalizedConfigHeaders
- Fall back to req.superstatic.protect when spec.protect is empty string and split credentials on the first colon only, returning 401 when missing a colon
- Type middleware handlers with connect.IncomingMessage & { superstatic?: Configuration } and ServerResponse & { superstatic?: Responder } across headers, missing, and protect
- Add unit tests covering backslash security headers, non-ASCII paths, unslashed globs, Connect direct mounting, and protect middleware edge cases
* refactor(responder): convert Responder to TypeScript

Convert src/responder.js to TypeScript (src/responder.ts), update test/unit/responder.spec.ts to use typed imports and expand coverage, and add void on the floating promise call in src/middleware/rewrites.ts.

* fix(responder): address PR review feedback on error, stream, and date handling

Check headersSent in handleError before setting status or ending response, attach an error listener before piping provider streams in handleFileStream, normalize modified timestamps with explicit Number.isNaN checks in isNotModified, and add corresponding unit tests.

* fix(responder): address PR review feedback on types, headers, and directives

Clear stale entity/framing headers and set Content-Length on 500 error responses, remove public unknown overloads while preserving runtime directive validation, allow empty string data and null status, export Responder namespace types backed by src/options.ts, propagate async middleware errors and next(err), guard invalid modified dates, and expand unit tests.
…cript (#616)

* refactor(utils): convert patterns to TypeScript

Converts src/utils/patterns.js to TypeScript with typed pattern matching utilities and adds unit tests in test/unit/utils/patterns.spec.ts.

* fix(utils): address PR review feedback on patterns typing and tests

* refactor(middleware): convert redirects middleware and tests to TypeScript

Converts src/middleware/redirects.js and test/unit/middleware/redirects.spec.js to TypeScript (src/middleware/redirects.ts and test/unit/middleware/redirects.spec.ts).

- Reuses Redirect and Configuration types from src/config.ts and patterns.configMatcher / patterns.createRaw from src/utils/patterns.ts.

- Replaces internal Redirect constructor function with a typed class/interface while preserving exact path-to-regexp capture, query string preservation, and CommonJS factory export compatibility.

* fix(middleware): address PR review feedback on redirects and activator
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants