Full Code of divshot/superstatic for AI

master d2154f1f7014 cached
71 files
191.7 KB
48.0k tokens
49 symbols
1 requests
Download .txt
Showing preview only (209K chars total). Download the full file or copy to clipboard to get everything.
Repository: divshot/superstatic
Branch: master
Commit: d2154f1f7014
Files: 71
Total size: 191.7 KB

Directory structure:
gitextract_0ny9bt5u/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── node-test.yml
├── .gitignore
├── .mocharc.yaml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── changelog.txt
├── eslint.config.mjs
├── examples/
│   ├── middleware/
│   │   ├── app/
│   │   │   └── index.html
│   │   └── index.js
│   └── server/
│       ├── app/
│       │   └── index.html
│       ├── error.html
│       └── index.js
├── package.json
├── src/
│   ├── activator.js
│   ├── bin/
│   │   └── server.ts
│   ├── cli/
│   │   └── index.ts
│   ├── config.ts
│   ├── index.ts
│   ├── loaders/
│   │   └── config-file.js
│   ├── middleware/
│   │   ├── env.ts
│   │   ├── files.js
│   │   ├── headers.js
│   │   ├── index.js
│   │   ├── missing.js
│   │   ├── not-found.js
│   │   ├── protect.js
│   │   ├── redirects.js
│   │   └── rewrites.ts
│   ├── options.ts
│   ├── providers/
│   │   ├── fs.ts
│   │   └── memory.js
│   ├── responder.js
│   ├── server.js
│   ├── superstatic.js
│   └── utils/
│       ├── i18n.ts
│       ├── objectutils.ts
│       ├── pathutils.ts
│       ├── patterns.js
│       └── promiseback.js
├── templates/
│   ├── env.js.template
│   └── not_found.html
├── test/
│   ├── fixtures/
│   │   ├── a/
│   │   │   └── index.html
│   │   └── b/
│   │       ├── b.html
│   │       └── index.html
│   ├── helpers.js
│   ├── integration/
│   │   ├── clean-urls.spec.ts
│   │   ├── error-pages.spec.ts
│   │   ├── i18n.spec.ts
│   │   └── serving-files.spec.ts
│   └── unit/
│       ├── loaders/
│       │   └── config-file.spec.js
│       ├── middleware/
│       │   ├── env.spec.js
│       │   ├── files.spec.ts
│       │   ├── headers.spec.js
│       │   ├── missing.spec.ts
│       │   ├── not-found.spec.js
│       │   ├── redirects.spec.js
│       │   └── rewrites.spec.ts
│       ├── providers/
│       │   ├── fs.spec.ts
│       │   └── memory.spec.js
│       ├── responder.spec.js
│       ├── server.spec.js
│       ├── superstatic.spec.js
│       └── utils/
│           ├── i18n.spec.ts
│           ├── pathutils.spec.ts
│           └── promiseback.spec.js
├── tsconfig.base.json
├── tsconfig.json
└── tsconfig.publish.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/node-test.yml
================================================
name: CI Tests

on:
  - pull_request
  - push

jobs:
  check-license:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-go@v6
        with:
          go-version: "^1.18.0"
      - run: go install github.com/google/addlicense@latest
      - run: env bash -c 'addlicense --check -c "Google LLC" -l MIT src/**/* bin/* examples/**/*'

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version:
          - "20"
          - "22"
          - "24"
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
          cache-dependency-path: package-lock.json

      - run: npm i --global npm@^11.7.0
      - run: npm ci
      - run: npm test
      - run: npm outdated
        continue-on-error: true

  check-package-lock:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: "24"
          cache: npm
          cache-dependency-path: package-lock.json

      - run: npm i --global npm@^11.7.0
      - run: npm install --package-lock-only --ignore-scripts
      - run: "git diff --exit-code -- package-lock.json || (echo 'Error: package-lock.json is changed during npm install! Please make sure to use npm >= 6.9.0 and commit package-lock.json.' && false)"


================================================
FILE: .gitignore
================================================
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz

pids
logs
results

coverage
.nyc_output
npm-debug.log
node_modules
.tmp
.tmp_root

spikes

test/__testing

*.0
asdf.txt
todo.txt

# Errors from testing
firebase.json
superstatic.json

lib


================================================
FILE: .mocharc.yaml
================================================
require:
  - ts-node/register
  - source-map-support/register


================================================
FILE: CONTRIBUTING.md
================================================
Want to contribute? Great! First, read this page (including the small print at the end).

### Before you contribute

Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.

### Code reviews

All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.

### The small print

Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).


================================================
FILE: Dockerfile
================================================
FROM node:0.10
ADD ./package.json /superstatic/package.json
WORKDIR /superstatic
RUN npm install
ADD . /superstatic

VOLUME /data
WORKDIR /data

EXPOSE 80
ENTRYPOINT ["/superstatic/bin/server","-p","80","-o","0.0.0.0"]

================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2022 Google LLC

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# Superstatic   [![NPM Module](http://img.shields.io/npm/v/superstatic.svg?style=flat-square)](https://npmjs.org/package/superstatic) [![NPM download count](https://img.shields.io/npm/dm/superstatic.svg?style=flat-square)](https://npmjs.org/package/superstatic) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/firebase/superstatic/node-test.yml?style=flat-square)

Superstatic is an enhanced static web server that was built to power.
It has fantastic support for HTML5 pushState applications, clean URLs,
caching, and many other goodies.

## Documentation

* [Installation](#installation)
* [Usage](#usage)
* [Configuration](#configuration)
* [API](#api)
  * [Middleware](#middleware)
  * [Server](#server)
* [Providers](#providers)
  * [Authoring Providers](#authoring-providers)
* [Run Tests](#run-tests)
* [Changelog](https://github.com/firebase/superstatic/blob/master/CHANGELOG.md)
* [Contributing](#contributing)

## Installation

Superstatic should be installed globally using npm:

For use via CLI

```
$ npm install -g superstatic
```

For use via API

```
npm install superstatic --save
```

## Usage

By default, Superstatic will simply serve the current directory on port
`3474`. This works just like any other static server:

```
$ superstatic
```

You can optionally specify the directory, port and hostname of the server:

```
$ superstatic public --port 8080 --host 127.0.0.1
```

## Configuration

Superstatic reads special configuration from a JSON file (either `superstatic.json`
or `firebase.json` by default, configurable with `-c`). This JSON file enables
enhanced static server functionality beyond simply serving files.

**public:** by default, Superstatic will serve the current working directory (or the
ancestor of the current directory that contains the configuration json being used).
This configuration key specifies a directory *relative to the configuration file* that
should be served. For example, if serving a Jekyll app, this might be set to `"_site"`.
A directory passed as an argument into the command line app supercedes this configuration
directive.

**cleanUrls:** if `true`, all `.html` files will automatically have their extensions
dropped. If `.html` is used at the end of a filename, it will perform a 301 redirect
to the same path with `.html` dropped.

All paths have clean urls

```json
{
  "cleanUrls": true
}
```

**rewrites:** you can specify custom route recognition for your application by supplying
an object to the routes key. Use a single star `*` to replace one URL segment or a
double star to replace an arbitrary piece of URLs. This works great for single page
apps. An example:

```json
{
  "rewrites": [
    {"source":"app/**","destination":"/application.html"},
    {"source":"projects/*/edit","destination":"/projects.html"}
  ]
}
```

**redirects:** you can specify certain url paths to be redirected to another url by supplying configuration to the `redirects` key. Path matching is similar to using custom routes. `redirects` use the `301` HTTP status code by default, but this can be overridden by configuration.

```json
{
  "redirects": [
    {"source":"/some/old/path", "destination":"/some/new/path"},
    {"source":"/firebase/*", "destination":"https://www.firebase.com", "type": 302}
  ]
}
```

Route segments are also supported in the `redirects` configuration. Segmented `redirects` also support custom status codes (see above):

```json
{
  "redirects": [
    {"source":"/old/:segment/path", "destination":"/new/path/:segment"}
  ]
}
```

In this example, `/old/custom-segment/path` redirects to `/new/path/custom-segment`

**headers:** Superstatic allows you to set the response headers for certain paths as well:

```json
{
  "headers": [
    {
      "source" : "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
      "headers" : [{
        "key" : "Access-Control-Allow-Origin",
        "value" : "*"
      }]
    }, {
      "source" : "**/*.@(jpg|jpeg|gif|png)",
      "headers" : [{
        "key" : "Cache-Control",
        "value" : "max-age=7200"
      }]
    }, {
      "source" : "404.html",
      "headers" : [{
        "key" : "Cache-Control",
        "value" : "max-age=300"
      }]
    }]
  }
}
```

**trailingSlash:** Have full control over whether or not your app has or doesn't have trailing slashes. By default, Superstatic will make assumptions for on the best times to add or remove the trailing slash. Other options include `true`, which always adds a trailing slash, and `false`, which always removes the trailing slash.

```json
{
  "trailingSlash": true
}
```

**i18n:** Internationalized content can be served based on `accept-language` or `x-country-code` headers.

Imagine a setup with the following files:

```
- public/
  - index.html
  - i18n/
    - fr_ca/
      - index.html
    - ALL_ca/
      - index.html
    - fr/
      - index.html
```

With `i18n` enabled, when a request is received for `/index.html` with the `accept-language` header set to `fr` (and no `x-country-code`), the content at `public/i18n/fr/index.html` will be returned as a response.

If `accept-language: fr` and `x-country-code: ca` are passed, the content at `public/i18n/fr_ca/index.html` will be returned for `/index.html`.

For more information about how content is resolved when using `i18n`, see [the Firebase Hosting documentation of the feature](https://firebase.google.com/docs/hosting/i18n-rewrites).

```json
{
  "i18n": {
    "root": "/intl"
  }
}
```

## API

Superstatic is available as a middleware and a standalone [Connect](http://www.npmjs.org/package/connect) server. This means you can plug this into your current server or run your own static server using Superstatic's server.


### Middleware

```js
var superstatic = require('superstatic')
var connect = require('connect');

var app = connect()
	.use(superstatic(/* options */));

app.listen(3000, function() {

});

```

### `superstatic([options])`

Instantiates middleware. See an [example](https://github.com/firebase/superstatic/tree/master/examples) for detail on real world use.

* `options` - Optional configuration:
  * `fallthrough` - When `false`, render a 404 page from within Superstatic rather than calling through to the next middleware. Defaults to `true`.
  * `config` - A file path to your application's configuration file (see [Configuration](#configuration)) or an object containing your application's configuration. If an object is provided, it will be merged into existing config in a `superstatic.json`.
  * `protect` - Adds HTTP basic auth. Example:  `username:password`
  * `env`- A file path your application's environment variables file or an object containing values that are made available at the urls `/__/env.json` and `/__/env.js`. See the documentation detail on [environment variables](http://docs.firebase.com/guides/environment-variables).
  * `cwd` - The current working directory to set as the root. Your application's `public` configuration option will be used relative to this.
  * `compression` - An option which controls superstatic's response compression. Pass in a standard `compression(req, res, next)` Express middleware function to override the default compression behavior (for example, require [shrink-ray](https://www.npmjs.com/package/shrink-ray) to enable advanced compression schemes such as brotli, or require node.js' stock [compression](https://www.npmjs.com/package/compression) middleware yourself to change the compression quality and caching behavior). Any other truthy value will default to the stock node.js middleware.

### Server

```js
var superstatic = require('superstatic').server;

var app = superstatic(/* options */);

var server = app.listen(function() {

});
```

Since Superstatic's server is a barebones Connect server using the Superstatic middleware, see the [Connect documentation](https://github.com/senchalabs/connect) on how to correctly instantiate, start, and stop the server.

### `superstatic([options])`

Instantiates a Connect server, setting up Superstatic middleware, port, host, debugging, compression, etc.

* `options` - Optional configuration. Uses the same options as the middleware, plus a few more options:
  * `port` - The port of the server. Defaults to `3474`.
  * `host` or `hostname` - The hostname of the server. Defaults to `localhost`.
  * `errorPage` - A file path to a custom error page. Defaults to [Superstatic's error page](https://github.com/firebase/superstatic/blob/master/templates/not_found.html).
  * `debug` - A boolean value that tells Superstatic to show or hide network logging in the console. Defaults to `false`.
  * `compression` - A boolean value that tells Superstatic to serve gzip/deflate compressed responses based on the request Accept-Encoding header and the response Content-Type header. Defaults to `false`.
  * `gzip` **[DEPRECATED]** - A boolean value which is now equivalent in behavior to `compression`. Defaults to `false`.

## Providers

Superstatic reads content from **providers**. The default provider for Superstatic
reads from the local filesystem. Other providers can be substituted when initializing
Superstatic:

```js
superstatic({
  provider: require('superstatic-someprovider')({
    provider: 'options'
  })
});
```

### Authoring Providers

Implementing a new provider is quite simple. You simply need to create a function
that takes a request and pathname and returns a Promise. The Promise should:

1. Resolve `null` when content isn't found (i.e. a 404 response).
2. Resolve with a metadata object as described below when content is found.
3. Reject when an error occurs in the content-fetching process.

The metadata object returned by a provider needs the following properties:

* **stream:** A readable stream for the content.
* **size:** The length of the content.
* **etag:** (optional) a content-unique string such as an MD5 hash computed from the content
* **modified:** (optional) a Date object for when the content was last modified

A simple in-memory store provider can be found at `lib/providers/memory.js` in
this repo as a simple reference example of a provider.

**Note:** The pathname will be URL-encoded. You should make sure your provider
properly handles files with non-standard characters (spaces, unicode, etc).

## Run Tests

In superstatic module directory:

```
npm install
npm test
```

## Contributing

We LOVE open source and open source contributors. If you would like to contribute to Superstatic, please review our [contributing guidelines](https://github.com/firebase/superstatic/blob/master/CONTRIBUTING.md) before you jump in and get your hands dirty.


================================================
FILE: changelog.txt
================================================



================================================
FILE: eslint.config.mjs
================================================
import eslint from "@eslint/js";
import eslintConfigGoogle from "eslint-config-google";
import eslintConfigPrettier from "eslint-config-prettier";
import eslintConfigJSDoc from "eslint-plugin-jsdoc";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import tseslint from "typescript-eslint";
import globals from "globals";

export default [
  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,
  eslintConfigJSDoc.configs["flat/recommended"],
  eslintConfigGoogle,
  eslintConfigPrettier,
  eslintPluginPrettierRecommended,
  {
    ignores: ["eslint.config.mjs", "lib/**/*", "coverage/**/*"],
  },
  {
    rules: {
      "jsdoc/newline-after-description": "off",
      "jsdoc/require-jsdoc": ["warn", { publicOnly: true }],
      "jsdoc/require-param-type": "off",
      "jsdoc/require-returns-type": "off",

      "@typescript-eslint/no-require-imports": "warn", // TODO(bkendall): remove allow to error.

      "no-unused-vars": "off", // Turned off in favor of @typescript-eslint/no-unused-vars.
      "require-atomic-updates": "off", // This rule is so noisy and isn't useful: https://github.com/eslint/eslint/issues/11899
      "require-jsdoc": "off", // This rule is deprecated and superseded by jsdoc/require-jsdoc.
      "valid-jsdoc": "off", // This is deprecated but included in recommended configs.
    },
    languageOptions: {
      ecmaVersion: 2020,
      globals: {
        ...globals.node,
        ...globals.mocha,
      },
      parserOptions: {
        ecmaVersion: "es2020",
        projectService: {
          project: "./tsconfig.json",
          allowDefaultProject: ["eslint.config.js"],
        },
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    files: ["**/*.ts"],
    rules: {
      "@typescript-eslint/no-explicit-any": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-argument": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-assignment": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-call": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-member-access": "warn", // TODO(bkendall): remove allow to error.
    },
  },
  {
    files: ["**/*.js"],
    rules: {
      "@typescript-eslint/no-this-alias": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-argument": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-assignment": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-call": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-member-access": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-unsafe-return": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/no-var-requires": "warn", // TODO(bkendall): remove allow to error.
      "@typescript-eslint/unbound-method": "warn", // TODO(bkendall): remove allow to error.
    },
  },
];


================================================
FILE: examples/middleware/app/index.html
================================================
<!--
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Superstatic Middleware</title>

</head>
<body>

  Hello using Superstatic middleware.

</body>
</html>


================================================
FILE: examples/middleware/index.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const { default: superstatic } = require("../../");
const connect = require("connect");

const spec = {
  config: {
    public: "./app",
    rewrites: [
      {
        source: "**",
        destination: "/index.html",
      },
    ],
  },
  cwd: process.cwd(),
  compression: true,
};

const app = connect().use(superstatic(spec));

app.listen(3474, (err) => {
  if (err) {
    console.log(err);
  }
  console.log("Superstatic now serving on port 3474 ...");
});


================================================
FILE: examples/server/app/index.html
================================================
<!--
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Superstatic Server</title>

</head>
<body>

  Hello using Superstatic server.

</body>
</html>


================================================
FILE: examples/server/error.html
================================================
<!--
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>404 Not Found</title>

</head>
<body>
  Nothing here. Move on.
</body>
</html>


================================================
FILE: examples/server/index.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const { server: superstaticServer } = require("../../");

const spec = {
  port: 3474,
  config: {
    public: "./app",
  },
  cwd: __dirname,
  errorPage: __dirname + "/error.html",
  compression: true,
  debug: true,
};

const app = superstaticServer(spec);
app.listen((err) => {
  if (err) {
    console.log(err);
  }
  console.log("Superstatic now serving on port 3474 ...");
});


================================================
FILE: package.json
================================================
{
  "name": "superstatic",
  "version": "10.0.0",
  "description": "A static file server for fancy apps",
  "main": "./lib/index.js",
  "types": "./lib/index.d.ts",
  "scripts": {
    "build": "tsc",
    "build:watch": "tsc --watch",
    "clean": "rimraf lib",
    "prepare": "npm run clean && npm run build -- --build tsconfig.publish.json",
    "format": "npm run lint -- --fix --quiet",
    "test": "npm run lint && npm run coverage",
    "test-unit": "mocha \"test/unit/**\"",
    "test-integration": "mocha \"test/integration/**\"",
    "lint": "eslint",
    "coverage": "nyc mocha \"test/unit/**\" \"test/integration/**\"",
    "watch": "mocha -w \"test/unit/**\" \"test/integration/**\""
  },
  "author": "Firebase (https://www.firebase.com/)",
  "license": "MIT",
  "bin": {
    "superstatic": "./lib/bin/server.js"
  },
  "files": [
    "bin",
    "lib",
    "templates"
  ],
  "engines": {
    "node": "20 || 22 || 24"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/firebase/superstatic.git"
  },
  "bugs": {
    "url": "https://github.com/firebase/superstatic/issues"
  },
  "keywords": [
    "static",
    "server",
    "firebase",
    "hosting",
    "pushstate",
    "html5",
    "router",
    "file",
    "directory",
    "hash",
    "hashbang"
  ],
  "dependencies": {
    "basic-auth-connect": "^1.1.0",
    "commander": "^10.0.0",
    "compression": "^1.7.0",
    "connect": "^3.7.0",
    "destroy": "^1.0.4",
    "glob-slasher": "^1.0.1",
    "is-url": "^1.2.2",
    "join-path": "^1.1.1",
    "mime-types": "^2.1.35",
    "minimatch": "^6.1.6",
    "morgan": "^1.8.2",
    "on-finished": "^2.2.0",
    "on-headers": "^1.0.0",
    "path-to-regexp": "^1.9.0",
    "router": "^2.0.0",
    "update-notifier-cjs": "^5.1.6"
  },
  "optionalDependencies": {
    "re2": "^1.17.7"
  },
  "devDependencies": {
    "@eslint/js": "^9.35.0",
    "@types/chai": "^4.3.3",
    "@types/chai-as-promised": "^7.1.5",
    "@types/connect": "^3.4.35",
    "@types/mime-types": "^2.1.1",
    "@types/mocha": "^10.0.0",
    "@types/node": "^24.3.1",
    "@types/supertest": "^6.0.2",
    "chai": "^4.4.1",
    "chai-as-promised": "^7.1.2",
    "concat-stream": "^2.0.0",
    "eslint": "^9.35.0",
    "eslint-config-google": "^0.14.0",
    "eslint-config-prettier": "^10.1.8",
    "eslint-plugin-jsdoc": "^60.6.0",
    "eslint-plugin-prettier": "^5.5.4",
    "globals": "^16.3.0",
    "mocha": "^11.7.2",
    "nyc": "^17.0.0",
    "prettier": "^3.1.1",
    "rimraf": "^6.0.1",
    "sinon": "^17.0.1",
    "sinon-chai": "^3.7.0",
    "source-map-support": "^0.5.21",
    "std-mocks": "^2.0.0",
    "supertest": "^7.0.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.9.2",
    "typescript-eslint": "^8.43.0"
  }
}


================================================
FILE: src/activator.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const middleware = require("./middleware");
const promiseback = require("./utils/promiseback");

const Activator = function (spec, provider) {
  this.spec = spec;
  this.provider = provider;
  this.stack = this.buildStack();

  if (typeof spec.config === "function") {
    this.awaitConfig = spec.config;
  } else {
    this.awaitConfig = function () {
      return Promise.resolve(spec.config);
    };
  }
};

Activator.prototype.buildStack = function () {
  const self = this;

  const stack = this.spec.stack.slice(0);
  Object.entries(this.spec.before ?? {}).forEach(([name, wares]) => {
    stack.splice(...[stack.indexOf(name), 0].concat(wares));
  });

  Object.entries(this.spec.after ?? {}).forEach(([name, wares]) => {
    stack.splice(...[stack.indexOf(name) + 1, 0].concat(wares));
  });

  return stack.map((ware) => {
    return typeof ware === "function" ? ware : middleware[ware](self.spec);
  });
};

Activator.prototype.build = function () {
  const self = this;

  return function (req, res, next) {
    promiseback(self.awaitConfig, 2)(req, res).then((config) => {
      req.superstatic = config ?? {};

      const stack = self.stack.slice(0).reverse();
      const _run = function () {
        if (!stack.length) {
          return next();
        }
        const fn = stack.pop();
        return fn(req, res, _run);
      };

      _run();
    }, next);
  };
};

module.exports = function (spec, provider) {
  return new Activator(spec, provider).build();
};


================================================
FILE: src/bin/server.ts
================================================
#!/usr/bin/env node

/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const updateNotifier = require("update-notifier-cjs");

import cli from "../cli";
const pkg = require("../../package.json");

const updateCheckInterval = 1000 * 60 * 60 * 24 * 7; // 1 week

const notifier = updateNotifier({
  pkg,
  updateCheckInterval: updateCheckInterval,
  shouldNotifyInNpmScript: true,
});

notifier.notify();

cli.parse();


================================================
FILE: src/cli/index.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import { Command } from "commander";
import * as fs from "node:fs";
import * as path from "node:path";

const pkg = require("../../package.json");
import server = require("../server");

const PORT = "3474";
const HOSTNAME = "localhost";
const CONFIG_FILENAME = ["superstatic.json", "firebase.json"];
const ENV_FILENAME = ".env.json";

let env: Record<string, string> | undefined = undefined;
try {
  env = JSON.parse(fs.readFileSync(path.resolve(ENV_FILENAME), "utf8"));
} catch {
  // do nothing
}

const cli = new Command();

cli.name("superstatic").version(pkg.version, "-v, --version");

cli
  .command("serve", { isDefault: true })
  .argument("[folder]")
  .option("-p, --port <port>", "Port", PORT)
  .option("--host, --hostname <hostname>", "Hostname", HOSTNAME)
  .option("-c, --config <config>", "Filename of config", CONFIG_FILENAME)
  .option("--debug")
  .option("--gzip")
  .option("--compression")
  .description("start server")
  .action((folder, options) => {
    return new Promise((resolve) => {
      const app = server({
        cwd: path.join(process.cwd(), folder),
        config: options.config,
        port: options.port,
        hostname: options.hostname,
        compression: options.compression,
        debug: options.debug,
        env: env,
      });
      app.listen(() => resolve());
      console.log(
        `Superstatic started.\nVisit http://${options.hostname}:${options.port} to view your app.`,
      );
    });
  });

export default cli;


================================================
FILE: src/config.ts
================================================
export interface Configuration {
  // Defaults to the current working directory.
  public?: string;
  cleanUrls?: boolean | string[];
  rewrites?: Rewrite[];
  redirects?: Redirect[];
  headers?: Header[];
  trailingSlash?: boolean;
  i18n?: { root: string };
  errorPage?: string;
}

export interface Rewrite {
  source: string;
  destination: string;
}

export interface Redirect {
  source: string;
  destination: string;
  type?: number;
}

export interface Header {
  source: string;
  headers: {
    key: string;
    value: string;
  }[];
}


================================================
FILE: src/index.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import superstatic = require("./superstatic");
import server = require("./server");
import patterns = require("./utils/patterns");
const RE2mode = patterns.re2Available;

export default superstatic;
export { server, RE2mode };


================================================
FILE: src/loaders/config-file.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const fs = require("fs");

const join = require("join-path");
const path = require("path");
const { isPlainObject } = require("../utils/objectutils");

const CONFIG_FILE = ["superstatic.json", "firebase.json"];

module.exports = function (filename) {
  if (typeof filename === "function") {
    return filename;
  }

  filename = filename ?? CONFIG_FILE;

  let configObject = {};
  let config = {};

  // From custom config data passed in
  try {
    configObject = JSON.parse(filename);
  } catch {
    if (isPlainObject(filename)) {
      configObject = filename;
      filename = CONFIG_FILE;
    }
  }

  if (Array.isArray(filename)) {
    filename = filename.find((name) => {
      return fs.existsSync(join(process.cwd(), name));
    });
  }

  // Set back to default config file if stringified object is
  // given as config. With this, we override values in the config file
  if (isPlainObject(filename)) {
    filename = CONFIG_FILE;
  }

  // A file name or array of file names
  if (typeof filename === "string" && filename.endsWith("json")) {
    try {
      config = JSON.parse(fs.readFileSync(path.resolve(filename)));
      config = config.hosting ?? config;
    } catch {
      // do nothing
    }
  }

  // Passing an object as the config value merges
  // the config data
  return { ...config, ...configObject };
};


================================================
FILE: src/middleware/env.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "fs";
import * as mime from "mime-types";

const template = fs
  .readFileSync(`${__dirname}/../../templates/env.js.template`)
  .toString();

interface SuperstaticRequest {
  superstatic: {
    env: Record<string, string>;
  };
}

interface SuperstaticResponse {
  superstatic: {
    handleData: (d: Record<string, string>) => void;
  };
}

/**
 * Returns middleware for `/__/env.json|js`.
 * @param spec superstatic options.
 * @param spec.env environment variables.
 * @returns middleware.
 */
export function env(spec: { env: Record<string, string> }) {
  return (
    req: Request & SuperstaticRequest,
    res: Response & SuperstaticResponse,
    next: () => void,
  ): void => {
    // const config = req.superstatic.env;
    let env = undefined;
    if (spec.env || req.superstatic.env) {
      env = Object.assign({}, req.superstatic.env, spec.env);
    } else {
      return next();
    }

    if (req.url === "/__/env.json") {
      res.superstatic.handleData({
        data: JSON.stringify(env, null, 2),
        contentType: mime.contentType("json") || "",
      });
    } else if (req.url === "/__/env.js") {
      const payload = template.replace("{{ENV}}", JSON.stringify(env));
      res.superstatic.handleData({
        data: payload,
        contentType: mime.contentType("js") || "",
      });
    }

    return next();
  };
}

module.exports = env;


================================================
FILE: src/middleware/files.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const { i18nContentOptions } = require("../utils/i18n");
const pathutils = require("../utils/pathutils");
const url = require("url");

/**
 * We cannot redirect to "", redirect to "/" instead.
 * @param {string} path path
 * @returns {string} noramlized path
 */
function normalizeRedirectPath(path) {
  return path || "/";
}

module.exports = function () {
  return function (req, res, next) {
    const config = req.superstatic;
    const trailingSlashBehavior = config.trailingSlash;

    const parsedUrl = url.parse(req.url);
    const pathname = pathutils.normalizeMultiSlashes(parsedUrl.pathname);
    const search = parsedUrl.search ?? "";

    const cleanUrlRules = !!req?.superstatic?.cleanUrls;

    // Exact file always wins.
    return providerResult(req, res, pathname)
      .then((result) => {
        if (result) {
          // If we are using cleanURLs, we'll trim off any `.html` (or `/index.html`), if it exists.
          if (cleanUrlRules) {
            if (pathname.endsWith(".html")) {
              let redirPath = pathutils.removeTrailingString(pathname, ".html");
              if (redirPath.endsWith("/index")) {
                redirPath = pathutils.removeTrailingString(redirPath, "/index");
              }
              if (trailingSlashBehavior === true) {
                redirPath = pathutils.addTrailingSlash(redirPath);
              }
              return res.superstatic.handle({
                redirect: normalizeRedirectPath(redirPath + search),
              });
            }
          }
          return res.superstatic.handleFileStream({ file: pathname }, result);
        }

        // Now, let's consider the trailing slash.
        const hasTrailingSlash = pathutils.hasTrailingSlash(pathname);

        // We want to check for some other files, namely an `index.html` if this were a directory.
        const pathAsDirectoryWithIndex = pathutils.asDirectoryIndex(
          pathutils.addTrailingSlash(pathname),
        );
        return providerResult(req, res, pathAsDirectoryWithIndex).then(
          (pathAsDirectoryWithIndexResult) => {
            // If an exact file wins now, we know that this path leads us to a directory.
            if (pathAsDirectoryWithIndexResult) {
              if (
                trailingSlashBehavior === undefined &&
                !hasTrailingSlash &&
                !cleanUrlRules
              ) {
                return res.superstatic.handle({
                  redirect: pathutils.addTrailingSlash(pathname) + search,
                });
              }
              if (
                trailingSlashBehavior === false &&
                hasTrailingSlash &&
                pathname !== "/"
              ) {
                // No infinite redirects
                return res.superstatic.handle({
                  redirect: normalizeRedirectPath(
                    pathutils.removeTrailingSlash(pathname) + search,
                  ),
                });
              }
              if (trailingSlashBehavior === true && !hasTrailingSlash) {
                return res.superstatic.handle({
                  redirect: pathutils.addTrailingSlash(pathname) + search,
                });
              }
              // If we haven't returned yet, our path is "correct" and we should be serving a file, not redirecting.
              return res.superstatic.handleFileStream(
                { file: pathAsDirectoryWithIndex },
                pathAsDirectoryWithIndexResult,
              );
            }

            // Let's check on the clean URLs property.
            // We want to know if a specific mutation of the path exists.
            if (cleanUrlRules) {
              let appendedPath = pathname;
              if (hasTrailingSlash) {
                if (trailingSlashBehavior !== undefined) {
                  // We want to remove the trailing slash and see if a file exists with an .html attached.
                  appendedPath =
                    pathutils.removeTrailingString(pathname, "/") + ".html";
                }
              } else {
                // Let's see if our path is a simple clean URL missing a .HTML5
                appendedPath += ".html";
              }

              return providerResult(req, res, appendedPath).then(
                (appendedPathResult) => {
                  if (appendedPathResult) {
                    // Okay, back to trailing slash behavior
                    if (trailingSlashBehavior === false && hasTrailingSlash) {
                      // If we had a slash to begin with, and we could be serving a file without it, we'll remove the slash.
                      // (This works because we are in the cleanURL block.)
                      return res.superstatic.handle({
                        redirect: normalizeRedirectPath(
                          pathutils.removeTrailingSlash(pathname) + search,
                        ),
                      });
                    }
                    if (trailingSlashBehavior === true && !hasTrailingSlash) {
                      // If we are missing a slash and need to add it, we want to make sure our appended path is cleaned up.
                      appendedPath = pathutils.removeTrailingString(
                        appendedPath,
                        ".html",
                      );
                      appendedPath = pathutils.removeTrailingString(
                        appendedPath,
                        "/index",
                      );
                      return res.superstatic.handle({
                        redirect:
                          pathutils.addTrailingSlash(appendedPath) + search,
                      });
                    }
                    // If we've gotten this far and still have `/index.html` on the end, we want to remove it from the URL.
                    if (appendedPath.endsWith("/index.html")) {
                      return res.superstatic.handle({
                        redirect: normalizeRedirectPath(
                          pathutils.removeTrailingString(
                            appendedPath,
                            "/index.html",
                          ) + search,
                        ),
                      });
                    }
                    // And if we should be serving a file and we're at the right path, we'll serve the file.
                    return res.superstatic.handleFileStream(
                      { file: appendedPath },
                      appendedPathResult,
                    );
                  }

                  return next();
                },
              );
            }

            return next();
          },
        );
      })
      .catch((err) => {
        res.superstatic.handleError(err);
      });
  };
};

/**
 * Uses the provider to look for a file given a path.
 * This also takes into account i18n settings.
 * @param {*} req the Request.
 * @param {*} res the Response.
 * @param {string} p the path to search for.
 * @returns {Promise<*>} a non-null value if a file is found.
 */
function providerResult(req, res, p) {
  const promises = [];

  const i18n = req.superstatic.i18n;
  if (i18n?.root) {
    const paths = i18nContentOptions(p, req);
    for (const pth of paths) {
      promises.push(res.superstatic.provider(req, pth));
    }
  }
  promises.push(res.superstatic.provider(req, p));

  return Promise.all(promises).then((results) => {
    for (const r of results) {
      if (r) {
        return r;
      }
    }
  });
}


================================================
FILE: src/middleware/headers.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const slasher = require("glob-slasher");
const urlParser = require("url");
const onHeaders = require("on-headers");
const patterns = require("../utils/patterns");

const normalizedConfigHeaders = function (spec, config) {
  const out = config ?? [];
  if (Array.isArray(config)) {
    const _isAllowed = function (headerToSet) {
      return spec.allowedHeaders.includes(headerToSet.key.toLowerCase());
    };

    for (const c of config) {
      c.source = slasher(c.source);
      c.headers = c.headers ?? [];
      if (spec.allowedHeaders) {
        c.headers = c.headers.filter(_isAllowed);
      }
    }
  }

  return out;
};

const matcher = function (configHeaders) {
  return function (url) {
    return configHeaders
      .filter((configHeader) => {
        return patterns.configMatcher(url, configHeader);
      })
      .reduce((out, val) => {
        val.headers.forEach((headerToSet) => {
          out.push(headerToSet);
        });
        return out;
      }, []);
  };
};

module.exports = function (spec) {
  return function (req, res, next) {
    const config = req?.superstatic?.headers;
    if (!config) {
      return next();
    }

    const headers = matcher(normalizedConfigHeaders(spec, config));
    const pathname = urlParser.parse(req.url).pathname;
    const matches = headers(slasher(pathname));

    onHeaders(res, () => {
      matches.forEach((header) => {
        res.setHeader(header.key, header.value);
      });
    });

    return next();
  };
};


================================================
FILE: src/middleware/index.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

[
  "protect",
  "redirects",
  "headers",
  "env",
  "files",
  "rewrites",
  "missing",
].forEach((name) => {
  exports[name] = function (spec, config) {
    const mware = require("./" + name)(spec, config);
    mware._name = name;
    return mware;
  };
});


================================================
FILE: src/middleware/missing.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const fs = require("fs");

const setHeaders = require("./headers");
const { i18nContentOptions } = require("../utils/i18n");

module.exports = function (spec) {
  let defaultErrorContent = undefined;
  if (spec.errorPage) {
    defaultErrorContent = fs.readFileSync(spec.errorPage, "utf8");
  }

  return function (req, res, next) {
    const config = req.superstatic;
    const errorPage = config.errorPage ?? "/404.html";

    setHeaders(spec)(
      {
        superstatic: config,
        url: errorPage,
      },
      res,
      () => {
        const handles = [];
        const i18n = req.superstatic.i18n;
        // To handle i18n, we will try to resolve i18n paths first.
        if (i18n?.root) {
          const paths = i18nContentOptions(errorPage, req);
          for (const pth of paths) {
            handles.push({ file: pth, status: 404 });
          }
        }
        handles.push({ file: errorPage, status: 404 });
        if (defaultErrorContent) {
          handles.push({ data: defaultErrorContent, status: 404 });
        }
        res.superstatic.handle(handles, next);
      },
    );
  };
};


================================================
FILE: src/middleware/not-found.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const fs = require("fs");

const DEFAULT_ERROR_PAGE = __dirname + "/../../templates/not_found.html";

module.exports = function (spec) {
  const content = fs.readFileSync(spec.errorPage ?? DEFAULT_ERROR_PAGE);

  return function (req, res) {
    // NOTE: provider isn't used to serve the pages
    // because this middleware should only serve local
    // static pages around the same directory as the
    // superstatic middleware

    res.statusCode = 404;
    res.setHeader("Content-Type", "text/html; charset=utf-8");
    res.setHeader("Cache-Control", "public, max-age=3600");
    res.end(content);
  };
};


================================================
FILE: src/middleware/protect.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const basicAuth = require("basic-auth-connect");

module.exports = function (spec) {
  return function (req, res, next) {
    const config = req.superstatic;

    if (spec.protect || config.protect) {
      return basicAuth.apply(
        basicAuth,
        (spec.protect ?? config.protect).split(":"),
      )(req, res, next);
    }

    return next();
  };
};


================================================
FILE: src/middleware/redirects.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const isUrl = require("is-url");

const patterns = require("../utils/patterns");
const pathToRegexp = require("path-to-regexp");
const slasher = require("glob-slasher");

function formatExternalUrl(u) {
  const cleaned = u
    .replace("/http:/", "http://")
    .replace("/https:/", "https://");

  return isUrl(cleaned) ? cleaned : u;
}

function addQuery(url, qs) {
  if (url.indexOf("?") >= 0) {
    return url + "&" + qs;
  } else if (qs?.length) {
    return url + "?" + qs;
  }
  return url;
}

const Redirect = function (glob, regex, destination, type) {
  this.type = type ?? 301;
  this.glob = slasher(glob);
  this.regex = regex;
  this.destination = destination;

  if (this.destination.match(/(?:^|\/):/)) {
    this.captureKeys = [];
    if (this.glob) {
      this.engine = "glob";
      this.capture = pathToRegexp(this.glob, this.captureKeys);
    }
    if (this.regex) {
      this.engine = "pattern";
      this.capture = patterns.createRaw(this.regex);
    }
    this.compileDestination = pathToRegexp.compile(this.destination);
  }
};

Redirect.prototype.test = function (url) {
  let qs = "";
  if (url.indexOf("?") >= 0) {
    const parts = url.split("?");
    url = parts[0];
    qs = parts[1];
  }

  let match = undefined;
  if (this.capture) {
    match = this.capture.exec(url);
  }
  if (match) {
    let params = {};
    if (this.engine === "glob") {
      for (let i = 0; i < this.captureKeys.length; i++) {
        let m = match[i + 1];
        if (m?.includes("/")) {
          m = m.split("/");
        }

        params[this.captureKeys[i].name] = m;
      }
    } else {
      for (let j = 0; j < match.length; j++) {
        params[j.toString()] = match[j];
      }
      if (match.groups) {
        params = Object.assign(params, match.groups);
      }
    }

    try {
      const dest = decodeURIComponent(this.compileDestination(params));
      return {
        type: this.type,
        destination: encodeURI(addQuery(dest, qs)),
      };
    } catch {
      return undefined;
    }
  } else if (
    patterns.configMatcher(url, { glob: this.glob, regex: this.regex })
  ) {
    return {
      type: this.type,
      destination: encodeURI(addQuery(this.destination, qs)),
    };
  }
  return undefined;
};

module.exports = function () {
  return function (req, res, next) {
    const config = req?.superstatic?.redirects;
    if (!config) {
      return next();
    }

    const redirects = [];
    if (Array.isArray(config)) {
      config.forEach((redir) => {
        const glob = redir.glob ?? redir.source;
        redirects.push(
          new Redirect(glob, redir.regex, redir.destination, redir.type),
        );
      });
    } else {
      throw new Error("redirects provided in an unrecognized format");
    }

    const matcher = function (url) {
      for (const redirect of redirects) {
        const result = redirect.test(url);
        if (result) {
          return result;
        }
      }
      return undefined;
    };

    const match = matcher(decodeURI(req.url));

    if (!match) {
      return next();
    }

    // Remove leading slash of a url
    const redirectUrl = formatExternalUrl(match.destination);

    return res.superstatic.handle({
      redirect: redirectUrl,
      status: match.type,
    });
  };
};


================================================
FILE: src/middleware/rewrites.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const slasher = require("glob-slasher");
import * as urlParser from "url";
import { NextFunction } from "connect";
import { IncomingMessage, ServerResponse } from "http";

import { Configuration, Rewrite } from "../config";
import Responder = require("../responder");
import * as patterns from "../utils/patterns";

function matcher(rewrites: Rewrite[]) {
  return function (url: string) {
    for (const rw of rewrites) {
      if (patterns.configMatcher(url, rw)) {
        return rw;
      }
    }
    return;
  };
}

/**
 * Looks for possible rewrites for the given req.url.
 * @returns middleware for handling rewrites.
 */
module.exports = function () {
  return function (
    req: IncomingMessage & { superstatic: Configuration },
    res: ServerResponse & { superstatic: Responder },
    next: NextFunction,
  ) {
    const rewrites = matcher(req.superstatic.rewrites ?? []);
    if (!req.url) {
      return next();
    }
    const pathname = urlParser.parse(req.url).pathname;
    const match = rewrites(slasher(pathname));

    if (!match) {
      return next();
    }

    res.statusCode = 200;
    res.superstatic.handle({ rewrite: match }, next);
  };
};


================================================
FILE: src/options.ts
================================================
import { HandleFunction } from "connect";
import { Configuration } from "./config";

export interface MiddlewareOptions {
  fallthrough?: boolean;
  config?: string | Configuration;
  protect?: string;
  env?: string | Record<string, string>;
  cwd?: string;
  compression?: boolean;
  stack?: string | string[];
  after?: Record<string, HandleFunction>;
  before?: Record<string, HandleFunction>;
  rewriters?: Record<string, unknown>;
  errorPage?: string;
}

export interface ServerOptions extends MiddlewareOptions {
  port?: number;
  hostname?: string;
  errorPage?: string;
  debug?: boolean;
  compression?: boolean;
}


================================================
FILE: src/providers/fs.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as crypto from "node:crypto";
import { stat as fsStat } from "node:fs/promises";
import * as fs from "node:fs";
const pathjoin = require("join-path");

async function multiStat(
  paths: string[],
): Promise<fs.Stats & { pathname: string }> {
  // const pathname = paths.shift();
  let err: any;
  for (const pathname of paths) {
    try {
      const stat = await fsStat(pathname);
      return { pathname, ...stat };
    } catch (e: unknown) {
      err = e;
    }
  }
  throw err;
}

module.exports = function provider(options: any) {
  const etagCache: Record<string, { timestamp: Date; value: string }> = {};
  const cwd = options.cwd ?? process.cwd();
  let publicPaths: string[] = options.public ?? ["."];
  if (!Array.isArray(publicPaths)) {
    publicPaths = [publicPaths];
  }

  async function fetchEtag(pathname: string, stat: fs.Stats): Promise<string> {
    return new Promise((resolve, reject) => {
      const cached = etagCache[pathname];
      // Chaining doesn't work. If `stat.mtime` is undefined, the chained
      // version is true and breaks the logic.
      // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
      if (cached && cached.timestamp === stat.mtime) {
        return resolve(cached.value);
      }

      // the file you want to get the hash
      const fd = fs.createReadStream(pathname);
      const hash = crypto.createHash("md5");
      hash.setEncoding("hex");

      fd.on("error", reject);

      fd.on("end", () => {
        hash.end();
        const etag = hash.read();
        etagCache[pathname] = {
          timestamp: stat.mtime,
          value: etag,
        };
        resolve(etag);
      });

      // read all file and pipe it (write it) to the hash object
      return fd.pipe(hash);
    });
  }

  return async function (
    req: unknown,
    pathname: string,
  ): Promise<{
    modified: number;
    size: number;
    etag: string;
    stream: fs.ReadStream;
  } | null> {
    pathname = decodeURI(pathname);
    // jumping to parent directories is not allowed
    if (
      pathname.includes("../") ||
      pathname.includes("..\\") ||
      pathname.toLowerCase().includes("..%5c") ||
      pathname.match(/\0/g) !== null ||
      // A path that didn't start with a slash is not valid.
      !pathname.startsWith("/")
    ) {
      return Promise.resolve(null);
    }

    const fullPathnames: string[] = publicPaths.map((p) =>
      // eslint-disable-next-line @typescript-eslint/no-unsafe-return
      pathjoin(cwd, p, pathname),
    );

    try {
      const stat = await multiStat(fullPathnames);
      return {
        // stat.mtime is gone in node.js v22, so transition over to mtimeMs.
        modified: stat.mtime?.getTime() ?? stat.mtimeMs,
        size: stat.size,
        etag: await fetchEtag(stat.pathname, stat),
        stream: fs.createReadStream(stat.pathname),
      };
    } catch (err: any) {
      if (["ENOENT", "ENOTDIR", "EISDIR", "EINVAL"].includes(err.code)) {
        return null;
      }
      if (err instanceof Error) {
        return Promise.reject(err);
      }
      return Promise.reject(new Error(`Unknown error: ${err}`));
    }
  };
};


================================================
FILE: src/providers/memory.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const crypto = require("crypto");
const Readable = require("stream").Readable;

module.exports = function (options) {
  const fn = function (req, pathname) {
    pathname = decodeURI(pathname);

    if (!options.store[pathname]) {
      return Promise.resolve(null);
    }

    const content = options.store[pathname];

    const stream = new Readable();
    stream.push(content);
    stream.push(null);

    const hash = crypto.createHash("md5");
    hash.update(content);

    return Promise.resolve({
      modified: options.modified ?? null,
      stream: stream,
      size: content.length,
      etag: hash.digest("hex"),
    });
  };
  fn.store = options.store ?? {};
  return fn;
};


================================================
FILE: src/responder.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const mime = require("mime-types");
const { isPlainObject } = require("./utils/objectutils");
const path = require("path");
const onFinished = require("on-finished");
const destroy = require("destroy");

const awaitFinished = (res) => {
  return new Promise((resolve) => {
    onFinished(res, resolve);
  });
};

/** @type Class */
const Responder = function (req, res, options) {
  this.req = req;
  this.res = res;
  this.provider = options.provider;
  this.config = options.config ?? {};
  this.rewriters = options.rewriters ?? {};
  this.compressor = options.compressor;
};

Responder.prototype.isNotModified = function (stats) {
  if (stats.etag && stats.etag === this.req.headers["if-none-match"]) {
    return true;
  }

  let reqModified = this.req.headers["if-modified-since"];
  if (reqModified) {
    reqModified = new Date(reqModified).getTime();
  }
  if (stats.modified && reqModified && stats.modified < reqModified) {
    return true;
  }

  return false;
};

Responder.prototype.handle = function (item, next) {
  const self = this;
  return this._handle(item)
    .then((responded) => {
      if (!responded && next) {
        next();
      }
      return responded;
    })
    .catch((err) => {
      return self.handleError(err);
    });
};

Responder.prototype._handle = function (item) {
  if (Array.isArray(item)) {
    return this.handleStack(item);
  } else if (typeof item === "string") {
    return this.handleFile({ file: item });
  } else if (isPlainObject(item)) {
    if (item.file) {
      return this.handleFile(item);
    } else if (item.redirect) {
      return this.handleRedirect(item);
    } else if (item.rewrite) {
      return this.handleRewrite(item);
    } else if (item.data) {
      return this.handleData(item);
    }
  } else if (typeof item === "function") {
    return this.handleMiddleware(item);
  }

  return Promise.reject(
    new Error(
      JSON.stringify(item) + " is not a recognized responder directive",
    ),
  );
};

Responder.prototype.handleError = function (err) {
  this.res.statusCode = 500;
  console.log(err.stack);
  this.res.end("Unexpected error occurred.");
};

Responder.prototype.handleStack = function (stack) {
  const self = this;
  if (stack.length) {
    return this._handle(stack.shift()).then((responded) => {
      return responded ? true : self.handleStack(stack);
    });
  }

  return Promise.resolve(false);
};

Responder.prototype.handleFile = function (file) {
  const self = this;
  return this.provider(this.req, file.file).then((result) => {
    if (!result) {
      return false;
    }

    if (self.isNotModified(result)) {
      return self.handleNotModified(result);
    }

    return self.handleFileStream(file, result);
  });
};

Responder.prototype.handleFileStream = function (file, result) {
  const self = this;

  this.streamedFile = file;
  this.res.statusCode = file.status ?? 200;
  if (this.res.statusCode === 200 && file.file === this.config.errorPage) {
    this.res.statusCode = 404;
  }
  this.res.setHeader(
    "Content-Type",
    result.contentType ?? mime.contentType(path.extname(file.file)),
  );
  if (result.size) {
    this.res.setHeader("Content-Length", result.size);
  }
  if (result.etag) {
    this.res.setHeader("ETag", result.etag);
  }
  if (result.modified) {
    this.res.setHeader(
      "Last-Modified",
      new Date(result.modified).toUTCString(),
    );
  }

  if (this.compressor) {
    this.compressor(this.req, this.res, () => {
      result.stream.pipe(self.res);
    });
  } else {
    result.stream.pipe(self.res);
  }

  return awaitFinished(this.res).then(() => {
    destroy(result.stream);
    return true;
  });
};

Responder.prototype.handleNotModified = function () {
  this.res.statusCode = 304;
  this.res.removeHeader("Content-Type");
  this.res.removeHeader("Content-Length");
  this.res.removeHeader("Transfer-Encoding");
  this.res.end();
  return true;
};

Responder.prototype.handleRedirect = function (redirect) {
  this.res.statusCode = redirect.status ?? 301;
  this.res.setHeader("Location", redirect.redirect);
  this.res.setHeader("Content-Type", "text/html; charset=utf-8");
  this.res.end("Redirecting to " + redirect.redirect);
  return Promise.resolve(true);
};

Responder.prototype.handleMiddleware = function (middleware) {
  const self = this;
  return new Promise((resolve) => {
    middleware(self.req, self.res, () => {
      resolve(false);
    });
  });
};

Responder.prototype.handleRewrite = function (item) {
  const self = this;
  if (item.rewrite.destination) {
    return self.handleFile({ file: item.rewrite.destination });
  }

  for (const key in this.rewriters) {
    if (item.rewrite[key]) {
      return this.rewriters[key](item.rewrite, this).then((result) => {
        return self._handle(result);
      });
    }
  }
  return Promise.reject(
    new Error(
      "Unable to find a matching rewriter for " + JSON.stringify(item.rewrite),
    ),
  );
};

Responder.prototype.handleData = function (data) {
  this.res.statusCode = data.status ?? 200;
  this.res.setHeader(
    "Content-Type",
    data.contentType ?? "text/html; charset=utf-8",
  );
  this.res.end(data.data);
  return Promise.resolve(true);
};

module.exports = Responder;


================================================
FILE: src/server.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const connect = require("connect");
const networkLogger = require("morgan");

const superstatic = require("./superstatic");

/**
 * @param {ServerOptions} spec superstatic options.
 * @returns unknown
 */
module.exports = function (spec) {
  spec.fallthrough ??= false;

  const app = connect();
  const listen = app.listen.bind(app);

  // Override method because port and host are given
  // in the spec object
  app.listen = function (done) {
    let server = {};

    app.use(superstatic(spec));

    // Start server
    server = listen(spec.port, spec.hostname ?? spec.host, done);

    return server;
  };

  // Console output for network requests
  if (spec.debug) {
    app.use(networkLogger("combined"));
  }

  return app;
};


================================================
FILE: src/superstatic.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const makerouter = require("router");

const fsProvider = require("./providers/fs");
const Responder = require("./responder");
const activator = require("./activator");
const notFound = require("./middleware/missing");

const promiseback = require("./utils/promiseback");
const loadConfigFile = require("./loaders/config-file");
const defaultCompressor = require("compression")();

const CWD = process.cwd();

/**
 * Superstatic returns a router that can be used in a server.
 * @param {MiddlewareOptions} spec superstatic options.
 * @returns {HandleFunction} router handler.
 */
const superstatic = function (spec = {}) {
  spec.stack ??= "default";

  spec.fallthrough ??= true;

  if (typeof spec.stack === "string" && superstatic.stacks[spec.stack]) {
    spec.stack = superstatic.stacks[spec.stack];
  }

  const router = makerouter();
  const cwd = spec.cwd ?? CWD;

  // Load data
  /** @type {Configuration} */
  const config = (spec.config = loadConfigFile(spec.config));
  config.errorPage = config.errorPage ?? "/404.html";

  // Set up provider
  const provider = spec.provider
    ? promiseback(spec.provider, 2)
    : fsProvider({ cwd, ...config });

  // Select compression middleware
  let compressor;
  if (typeof spec.compression === "function") {
    compressor = spec.compression;
  } else if (spec.compression ?? spec.gzip) {
    compressor = defaultCompressor;
  } else {
    compressor = null;
  }

  // Setup helpers
  router.use((req, res, next) => {
    res.superstatic = new Responder(req, res, {
      provider: provider,
      config: config,
      compressor: compressor,
      rewriters: spec.rewriters,
    });

    next();
  });

  router.use(activator(spec, provider));

  // Handle not found pages
  if (!spec.fallthrough) {
    router.use(notFound(spec));
  }

  return router;
};

superstatic.stacks = {
  default: [
    "protect",
    "redirects",
    "headers",
    "env",
    "files",
    "rewrites",
    "missing",
  ],
  strict: ["redirects", "headers", "files", "rewrites", "missing"],
};

module.exports = superstatic;


================================================
FILE: src/utils/i18n.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import { normalizeMultiSlashes } from "./pathutils";

/**
 * Returns the list of paths to check for i18n content.
 * The path order is:
 * (1) root/language_country/path (for each language)
 * (2) root/ALL_country/path (if country is set)
 * (3) root/language_ALL/path or root/language/path (for each language)
 *
 * If i18n is *not* configured, an empty list is returned.
 * @param p requested path.
 * @param req the request object containing superstatic and i18n configuration.
 * @returns list of paths to check for i18n content.
 */
export function i18nContentOptions(p: string, req: any): string[] {
  const paths: string[] = [];
  const i18n = req.superstatic.i18n;
  if (!i18n?.root) {
    return paths;
  }
  const country = getCountryCode(req.headers);
  const languages = getI18nLanguages(req.headers);
  // (1)
  if (country) {
    for (const l of languages) {
      paths.push(join(i18n.root, `${l}_${country}`, p));
    }
    // (2)
    paths.push(join(i18n.root, `ALL_${country}`, p));
  }
  // (3)
  for (const l of languages) {
    paths.push(join(i18n.root, `${l}_ALL`, p));
    paths.push(join(i18n.root, `${l}`, p));
  }
  return paths;
}

function join(...arr: string[]): string {
  arr.unshift("/");
  return normalizeMultiSlashes(arr.join("/"));
}

/**
 * Fetches the country code from the headers object.
 * @param headers headers from the request.
 * @returns country code, or an empty string.
 */
function getCountryCode(headers: Record<string, string>): string {
  const overrideValue = cookieValue(
    headers.cookie,
    "firebase-country-override",
  );
  if (overrideValue) {
    return overrideValue;
  }
  return headers["x-country-code"] || "";
}

/**
 * Fetches the languages from the accept-language header.
 * @param headers headers from the request.
 * @returns ordered list of languages from the header.
 */
function getI18nLanguages(headers: Record<string, string>): string[] {
  const overrideValue = cookieValue(
    headers.cookie,
    "firebase-language-override",
  );
  if (overrideValue) {
    return overrideValue.includes(",")
      ? overrideValue.split(",")
      : [overrideValue];
  }

  const acceptLanguage = headers["accept-language"];
  if (!acceptLanguage) {
    return [];
  }

  const languagesSeen = new Set<string>();
  const languagesOrdered = [];
  for (const v of acceptLanguage.split(",")) {
    const l = v.split("-")[0];
    if (!l) {
      continue;
    }
    if (!languagesSeen.has(l)) {
      languagesOrdered.push(l);
    }
    languagesSeen.add(l);
  }
  return languagesOrdered;
}

/**
 * Fetches a value from a cookie string.
 * @param cookieString full cookie string.
 * @param key key to look for.
 * @returns the value, or empty string;
 */
function cookieValue(cookieString: string, key: string): string {
  if (!cookieString) {
    return "";
  }
  const cookies = cookieString.split(";").map((c) => c.trim());
  for (const cookie of cookies) {
    if (cookie.startsWith(key)) {
      const s = cookie.split("=", 2);
      return s.length === 2 ? s[1] : "";
    }
  }
  return "";
}


================================================
FILE: src/utils/objectutils.ts
================================================
/**
 * Copyright (c) 2026 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * Returns true for plain objects (`{}` or `Object.create(null)`).
 * @param val value to check
 * @returns value indicating whether the value is a plain object
 */
export function isPlainObject(val: unknown): val is Record<string, unknown> {
  return (
    val !== null &&
    typeof val === "object" &&
    !Array.isArray(val) &&
    (Object.getPrototypeOf(val) === Object.prototype ||
      Object.getPrototypeOf(val) === null)
  );
}


================================================
FILE: src/utils/pathutils.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const INDEX_FILE = "index.html";

/**
 * @param pathname path to check.
 * @returns the path with "/index.html" appended, if required.
 */
export function asDirectoryIndex(pathname: string): string {
  if (isDirectoryIndex(pathname)) {
    return pathname;
  }
  return normalizeMultiSlashes(`${pathname}/${INDEX_FILE}`);
}

/**
 * @param pathname path to check.
 * @returns true if pathname ends with "/index.html".
 */
export function isDirectoryIndex(pathname: string): boolean {
  return pathname.endsWith(`/${INDEX_FILE}`);
}

/**
 * @param pathname path to check.
 * @returns true if it ends with a slash.
 */
export function hasTrailingSlash(pathname: string): boolean {
  return pathname.endsWith("/");
}

/**
 * @param pathname path to check.
 * @returns pathname with a trailing slash.
 */
export function addTrailingSlash(pathname: string): string {
  return hasTrailingSlash(pathname) ? pathname : pathname + "/";
}

/**
 * @param pathname path to check.
 * @returns pathname without a trailing slash.
 */
export function removeTrailingSlash(pathname: string): string {
  return removeTrailingString(pathname, "/");
}

/**
 * @param pathname path to check.
 * @returns pathname with any "//" replaced with "/".
 */
export function normalizeMultiSlashes(pathname: string): string {
  return pathname.replace(/\/+/g, "/");
}

/**
 * @param string string to check.
 * @param rm string to search for.
 * @returns string with rm removed if it's the end of string. Else, string.
 */
export function removeTrailingString(string: string, rm: string): string {
  if (!string.endsWith(rm)) {
    return string;
  }
  return string.slice(0, string.lastIndexOf(rm));
}


================================================
FILE: src/utils/patterns.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

let RE2;
const minimatch = require("minimatch");
try {
  RE2 = require("re2");
} catch {
  RE2 = null;
}

/**
 * Evaluates whether a configured redirect/rewrite/custom header should
 * be applied to a request against a specific path. All three features
 * are configured with a hash that contains either a Node-like glob path
 * specification as its `source` or `glob` field, or a RE2 regular
 * expression as its `regex` field.
 *
 * Since Javascript lacks a native library for RE2, Superstatic uses the C
 * bindings as an optional dependency, and falls over to PCRE if the import
 * is unavailable. Under most circumstances not involving named capturing
 * groups, the two libraries should have identical behavior.
 *
 * No special consideration is taken if the configuration hash contains both
 * a glob and a regex. normalizeConfig() will error in that case.
 * @param {string} path The URL path from the request.
 * @param {object} config A dictionary from a sanitized JSON configuration.
 * @returns {boolean} Whether the config should be applied to the request.
 */
function configMatcher(path, config) {
  const glob = config.glob ?? config.source;
  const regex = config.regex;
  if (glob) {
    return minimatch(path, glob);
  }
  if (regex) {
    const pattern = RE2 ? new RE2(regex, "u") : new RegExp(regex, "u");
    return path.match(pattern) !== null;
  }
  return false;
}

/**
 * Creates either an RE2 or a Javascript RegExp from a provided string
 * pattern, depending on whether or not the RE2 library is available as an
 * import.
 * @param {string} pattern A regular expression pattern to test against.
 * @returns {RegExp} A regular expression object, created by either base
 *                  RegExp or RE2, which matches the RegExp prototype
 */
function createRaw(pattern) {
  return RE2 ? new RE2(pattern, "u") : new RegExp(pattern, "u");
}

/**
 * Returns true if RE2, which is an optional dependency, has been loaded.
 * @returns {boolean}
 */
function re2Available() {
  return RE2 ? true : false;
}

/**
 * Is truthy if the provided raw string pattern contains a RE2 named capture
 * group opening, ?P<, which is not interpretable when Superstatic is falling
 * back on the base Javascript RegExp implementation.
 * @param {string} pattern
 * @returns {boolean}
 */
function containsRE2Capture(pattern) {
  return pattern?.includes("?P<");
}

/**
 * Is truthy if the provided raw string pattern contains a PCRE named capture
 * group opening, ?<, which is not interpretable when Superstatic has loaded
 * the RE2 bindings.
 * @param {string} pattern
 * @returns {boolean}
 */
function containsPCRECapture(pattern) {
  return pattern?.includes("?<");
}

module.exports = {
  configMatcher: configMatcher,
  createRaw: createRaw,
  re2Available: re2Available,
  containsRE2Capture: containsRE2Capture,
  containsPCRECapture: containsPCRECapture,
};


================================================
FILE: src/utils/promiseback.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

module.exports = function promiseback(userFn, argCount) {
  // if no callback argument is provided, assume the promise form
  if (userFn.length <= argCount) {
    return userFn;
  }
  // otherwise promise-ify the callback
  return (...args) => {
    return new Promise((resolve, reject) => {
      userFn(...args, (err, res) => {
        if (err) {
          if (err instanceof Error) {
            return reject(err);
          }
          return reject(new Error(`Unknown error: ${err}`));
        }
        resolve(res);
      });
    });
  };
};


================================================
FILE: templates/env.js.template
================================================
(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([''], function() {
            return (root.__env = factory());
        });
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like enviroments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals
        root.__env = factory();
    }
}(this, function() {
    return {{ENV}};
}));


================================================
FILE: templates/not_found.html
================================================
<!DOCTYPE html>
<html>
<head>
  <!--
    This is the Superstatic default 404 page.
    If you're seeing it, that means you haven't configured a custom error page.
    See https://github.com/firebase/superstatic for more info.
  -->
  <meta charset=utf-8 />
  <title>Page Not Found</title>
  <style>
    body {
      font-family: Roboto, Arial, sans-serif;
      background: white;
    }

    #message {
      margin: 100px auto;
      text-align: center;
      padding: 20px;
    }

    #message h1 {
      font-size: 60px;
      font-weight: normal;
      letter-spacing: -0.03em;
      margin: 0 0 10px;
    }

    #message p {
      margin: 10px 0 0;
      color: #aaa;
    }

    #footer {
      text-align: center;
      margin-top: 100px;
      font-size: 12px;
      color: #aaa;
    }
    #footer a {
      color: #aaa;
    }
  </style>
</head>
<body>
  <div id="message">
    <h1>Page Not Found</h1>
    <p>Sorry, we were unable to find anything at this location.</p>
  </div>
</body>
</html>


================================================
FILE: test/fixtures/a/index.html
================================================
A


================================================
FILE: test/fixtures/b/b.html
================================================
B


================================================
FILE: test/fixtures/b/index.html
================================================
B


================================================
FILE: test/helpers.js
================================================
module.exports = {
  decorator: function (middleware) {
    return function (config, spec) {
      return function (req, res, next) {
        req.superstatic = config ?? {};
        return middleware(spec ?? {})(req, res, next);
      };
    };
  },
};


================================================
FILE: test/integration/clean-urls.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "node:fs/promises";
import * as connect from "connect";
import * as request from "supertest";

import superstatic from "../../src/";
import { MiddlewareOptions } from "../../src/options";
import { Configuration } from "../../src/config";

// config will always exist, so let's make typing nicer.
function options(): MiddlewareOptions & { config: Configuration } {
  return {
    config: {
      public: ".tmp",
    },
  };
}

describe("clean urls", () => {
  before(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
    await fs.mkdir(".tmp");
    await fs.mkdir(".tmp/dir");
    await fs.writeFile(".tmp/index.html", "index", "utf8");
    await fs.writeFile(".tmp/test.html", "test", "utf8");
    await fs.writeFile(".tmp/app.js", 'console.log("js")', "utf8");
    await fs.writeFile(".tmp/dir/index.html", "dir index", "utf8");
    await fs.writeFile(".tmp/dir/sub.html", "dir sub", "utf8");
  });

  after(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("not configured", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app).get("/test").expect(404);
  });

  it("redirects html file", async () => {
    const opts = options();

    opts.config.cleanUrls = true;

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/test.html")
      .expect(301)
      .expect("Location", "/test");
  });

  it("serves html file", async () => {
    const opts = options();

    opts.config.cleanUrls = true;

    const app = connect().use(superstatic(opts));

    await request(app).get("/test").expect(200).expect("test");
  });

  it("redirects using globs", async () => {
    const opts = options();

    opts.config.cleanUrls = ["/*.html"];

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/test.html")
      .expect(301)
      .expect("Location", "/test");
  });

  it("serves html file using globs", async () => {
    const opts = options();

    opts.config.cleanUrls = ["*.html"];

    const app = connect().use(superstatic(opts));

    await request(app).get("/test").expect(200).expect("test");
  });
});


================================================
FILE: test/integration/error-pages.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "node:fs/promises";
import * as connect from "connect";
import * as request from "supertest";

import superstatic from "../../src/";
import { MiddlewareOptions } from "../../src/options";
import { Configuration } from "../../src/config";

function options(): MiddlewareOptions & { config: Configuration } {
  return {
    fallthrough: false,
    config: {
      public: ".tmp",
    },
  };
}

describe("error page", () => {
  beforeEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/default-error.html", "default error", "utf8");
    await fs.writeFile(".tmp/error.html", "config error", "utf8");
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("from 404.html", async () => {
    await fs.writeFile(".tmp/404.html", "404.html error", "utf8");
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/does-not-exist")
      .expect(404)
      .expect("404.html error")
      .expect("Content-Type", "text/html; charset=utf-8");
  });

  it("from custom error page", async () => {
    const opts = options();
    opts.config.errorPage = "/error.html";

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/does-not-exist")
      .expect(404)
      .expect("config error")
      .expect("Content-Type", "text/html; charset=utf-8");
  });

  it("falls back to default when configured error page does not exist", async () => {
    const opts = options();

    opts.errorPage = ".tmp/default-error.html";

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/does-not-exist")
      .expect(404)
      .expect("default error")
      .expect("Content-Type", "text/html; charset=utf-8");
  });
});


================================================
FILE: test/integration/i18n.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "node:fs/promises";
import * as connect from "connect";
import * as request from "supertest";

import superstatic from "../../src/";
import { MiddlewareOptions } from "../../src/options";
import { Configuration } from "../../src/config";

function options(): MiddlewareOptions & { config: Configuration } {
  return {
    fallthrough: false,
    config: {
      public: ".tmp",
      i18n: { root: "intl" },
    },
  };
}

describe("i18n resolution", () => {
  before(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/index.html", "normal index", "utf8");
    await fs.writeFile(".tmp/404.html", "normal 404", "utf8");
    await fs.mkdir(".tmp/intl");
    await fs.mkdir(".tmp/intl/fr");
    await fs.writeFile(".tmp/intl/fr/index.html", "french index", "utf8");
    await fs.writeFile(".tmp/intl/fr/404.html", "french 404", "utf8");
  });

  after(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("index.html", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app).get("/").expect(200, "normal index");
  });

  it("index.html with language", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/")
      .set("accept-language", "fr")
      .expect(200, "french index");
  });

  it("index.html with unknown language", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/")
      .set("accept-language", "de")
      .expect(200, "normal index");
  });

  it("an unknown file", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app).get("/nope").expect(404, "normal 404");
  });

  it("an unknown file with language", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/nope")
      .set("accept-language", "fr")
      .expect(404, "french 404");
  });
});


================================================
FILE: test/integration/serving-files.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "node:fs/promises";
import { join } from "path";
import * as connect from "connect";
import * as request from "supertest";

import superstatic from "../../src/";
import { MiddlewareOptions } from "../../src/options";
import { Configuration } from "../../src/config";

function options(): MiddlewareOptions & { config: Configuration } {
  return {
    fallthrough: false,
    config: {
      public: ".tmp",
    },
  };
}

describe("serves", () => {
  beforeEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/index.html", "index", "utf-8");
    await fs.writeFile(".tmp/test.html", "test", "utf-8");
    await fs.writeFile(".tmp/app.js", 'console.log("js")', "utf-8");
    await fs.mkdir(".tmp/dir");
    await fs.writeFile(".tmp/dir/index.html", "dir index", "utf-8");
    await fs.writeFile(".tmp/dir/sub.html", "dir sub", "utf-8");
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("static file", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/test.html")
      .expect(200)
      .expect("test")
      .expect("Content-Type", "text/html; charset=utf-8");
  });

  it("directory index file", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/dir/")
      .expect(200)
      .expect("dir index")
      .expect("Content-Type", "text/html; charset=utf-8");
  });

  it("cannot access files above the root", async () => {
    const app = connect().use(superstatic(options()));

    await request(app).get("/../README.md").expect(404);
  });

  it("missing directory index", async () => {
    const opts = options();

    opts.config.public = "./";

    const app = connect().use(superstatic(opts));

    await request(app).get("/").expect(404);
  });

  it("javascript file", async () => {
    const opts = options();

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/app.js")
      .expect(200)
      .expect('console.log("js")')
      .expect("Content-Type", "application/javascript; charset=utf-8");
  });

  it("from custom current working directory", async () => {
    const opts = options();

    opts.cwd = join(process.cwd(), ".tmp");
    opts.config.public = "./dir";

    const app = connect().use(superstatic(opts));

    await request(app)
      .get("/index.html")
      .expect(200)
      .expect("dir index")
      .expect("Content-Type", "text/html; charset=utf-8");
  });

  describe("redirects", () => {
    const opts = options();

    opts.config.redirects = [
      { source: "/from", destination: "/to" },
      { source: "/fromCustom", destination: "/toCustom", type: 302 },
      { source: "/external", destination: "http://redirect.com" },
    ];

    const app = connect().use(superstatic(opts));

    it("301", async () => {
      await request(app).get("/from").expect(301).expect("Location", "/to");
    });

    it("custom", async () => {
      await request(app)
        .get("/fromCustom")
        .expect(302)
        .expect("Location", "/toCustom");
    });

    it("external urls", async () => {
      await request(app)
        .get("/external")
        .expect(301)
        .expect("Location", "http://redirect.com");
    });
  });

  describe("trailing slash", () => {
    xit("removes trailling slash for file", async () => {
      const app = connect().use(superstatic(options()));

      await request(app)
        .get("/test.html/")
        .expect(301)
        .expect("Location", "/test.html");
    });

    it("add trailing slash with a directory index file", async () => {
      const app = connect().use(superstatic(options()));

      await request(app).get("/dir").expect(301).expect("Location", "/dir/");
    });
  });

  describe("basic auth", () => {
    it("protects", async () => {
      const opts = options();

      opts.protect = "username:passwords";

      const app = connect().use(superstatic(opts));

      await request(app)
        .get("/")
        .expect(401)
        .expect("www-authenticate", 'Basic realm="Authorization Required"');
    });
  });

  describe("custom headers", () => {
    it("with globs", async () => {
      const opts = options();

      opts.config.headers = [
        {
          source: "/**/*.html",
          headers: [
            {
              key: "x-custom",
              value: "testing",
            },
          ],
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/dir/sub.html").expect("x-custom", "testing");
    });

    it("exact", async () => {
      const opts = options();

      opts.config.headers = [
        {
          source: "/app.js",
          headers: [
            {
              key: "x-custom",
              value: "testing",
            },
          ],
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/app.js").expect("x-custom", "testing");
    });
  });

  xdescribe("environment variables", () => {
    it("json", async () => {
      const opts = options();

      opts.env = {
        key: "value",
      };

      const app = connect().use(superstatic(opts));

      await request(app)
        .get("/__/env.json")
        .expect({ key: "value" })
        .expect("Content-Type", "application/json; charset=utf-8");
    });

    it("js", async () => {
      const opts = options();

      opts.env = {
        key: "value",
      };

      const app = connect().use(superstatic(opts));

      await request(app)
        .get("/__/env.js")
        .expect(200)
        .expect("Content-Type", "application/javascript; charset=utf-8");
    });

    it("defaults to .env.json", async () => {
      await fs.writeFile(".env.json", '{"key":"value"}', "utf8");

      const app = connect().use(superstatic());

      await request(app).get("/__/env.json").expect({ key: "value" });

      await fs.rm(".env.json");
    });

    it("serves env file, overriding static routing", async () => {
      const opts = options();

      opts.env = {
        key: "value",
      };

      opts.config.rewrites = [
        {
          source: "**",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app)
        .get("/__/env.json")
        .expect({ key: "value" })
        .expect("Content-Type", "application/json; charset=utf-8");
    });
  });

  describe("custom routes", () => {
    it("serves file", async () => {
      const opts = options();

      opts.config.rewrites = [
        {
          source: "/testing",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app)
        .get("/testing")
        .expect(200)
        .expect("index")
        .expect("Content-Type", "text/html; charset=utf-8");
    });

    it("serves file from custom route when clean urls are on and route matches an html as a clean url", async () => {
      const opts = options();

      opts.config.cleanUrls = true;
      opts.config.rewrites = [
        {
          source: "/testing",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app)
        .get("/testing")
        .expect(200)
        .expect("index")
        .expect("Content-Type", "text/html; charset=utf-8");
    });

    it("serves static file when no matching route", async () => {
      const opts = options();

      opts.config.rewrites = [
        {
          source: "/testing",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/test.html").expect(200).expect("test");
    });

    it("serves with negation", async () => {
      const opts = options();

      opts.config.rewrites = [
        {
          source: "!/no",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/no").expect(404);
    });

    it("serves file if url matches exact file path", async () => {
      const opts = options();

      opts.config.rewrites = [
        {
          source: "**",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/test.html").expect(200).expect("test");
    });
  });

  describe("rewrites", () => {
    beforeEach(async () => {
      await fs.writeFile(".tmp/404.html", "404", "utf8");
    });

    it("rewrites unknown files to /index.html", async () => {
      const opts = options();
      opts.config.rewrites = [
        {
          source: "/**",
          destination: "/index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/index.html").expect(200).expect("index");
      await request(app).get("/dir/").expect(200).expect("dir index");
      await request(app).get("/testing/").expect(200).expect("index");
    });

    it("never is able to rewrite to a relative path", async () => {
      const opts = options();
      opts.config.rewrites = [
        {
          source: "/**",
          destination: "index.html",
        },
      ];

      const app = connect().use(superstatic(opts));

      await request(app).get("/index.html").expect(200).expect("index");
      await request(app).get("/foo").expect(404).expect("404");
      await request(app).get("/page2/").expect(404).expect("404");
    });
  });
});


================================================
FILE: test/unit/loaders/config-file.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const fs = require("node:fs/promises");
const expect = require("chai").expect;

const loadConfigFile = require("../../../src/loaders/config-file");

describe("loading config files", () => {
  beforeEach(async () => {
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/file.json", '{"key": "value"}', "utf-8");
    await fs.writeFile(
      ".tmp/package.json",
      JSON.stringify({
        superstatic: {
          key: "value",
        },
      }),
    );
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("filename", (done) => {
    const data = loadConfigFile(".tmp/file.json");

    expect(data).to.eql({
      key: "value",
    });
    done();
  });

  it("loads first existing file in array", (done) => {
    const data = loadConfigFile(["another.json", ".tmp/file.json"]);

    expect(data).to.eql({
      key: "value",
    });
    done();
  });

  it("empty object for when no file", (done) => {
    const data = loadConfigFile(".tmp/nope.json");
    expect(data).to.eql({});
    done();
  });

  it("loads object as config", (done) => {
    const config = loadConfigFile({
      my: "data",
    });

    expect(config).to.eql({
      my: "data",
    });
    done();
  });

  describe("extends the file config with the object passed", () => {
    it("superstatic.json", async () => {
      await fs.writeFile(
        "superstatic.json",
        '{"firebase": "superstatic", "public": "./"}',
        "utf-8",
      );

      const config = loadConfigFile({
        override: "test",
        public: "app",
      });

      expect(config).to.eql({
        firebase: "superstatic",
        override: "test",
        public: "app",
      });

      await fs.rm("superstatic.json");
    });

    it("firebase.json", async () => {
      await fs.writeFile(
        "firebase.json",
        '{"firebase": "example", "public": "./"}',
        "utf-8",
      );

      const config = loadConfigFile({
        override: "test",
        public: "app",
      });

      expect(config).to.eql({
        firebase: "example",
        override: "test",
        public: "app",
      });

      await fs.rm("firebase.json");
    });
  });
});


================================================
FILE: test/unit/middleware/env.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const request = require("supertest");
const connect = require("connect");

const helpers = require("../../helpers");
const env = helpers.decorator(require("../../../src/middleware/env"));
const Responder = require("../../../src/responder");

describe("env", () => {
  let app;

  beforeEach(() => {
    app = connect().use((req, res, next) => {
      res.superstatic = new Responder(req, res, {
        provider: {},
      });
      next();
    });
  });

  it("serves json", (done) => {
    app.use(
      env({
        env: {
          key: "value",
        },
      }),
    );

    void request(app)
      .get("/__/env.json")
      .expect(200)
      .expect({
        key: "value",
      })
      .expect("content-type", "application/json; charset=utf-8")
      .end(done);
  });

  it("serves javascript", (done) => {
    app.use(
      env({
        env: {
          key: "value",
        },
      }),
    );

    void request(app)
      .get("/__/env.js")
      .expect(200)
      .expect("content-type", "application/javascript; charset=utf-8")
      .end(done);
  });
});


================================================
FILE: test/unit/middleware/files.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "node:fs/promises";
import { expect } from "chai";
import * as request from "supertest";
import * as connect from "connect";
import { ServerResponse } from "node:http";

import * as helpers from "../../helpers";
import * as filesPkg from "../../../src/middleware/files";
const fsProvider = require("../../../src/providers/fs");
import * as Responder from "../../../src/responder";

const files = helpers.decorator(filesPkg);

describe("i18n", () => {
  const provider = fsProvider({ public: ".tmp" });
  let app: connect.Server;

  beforeEach(async () => {
    await fs.mkdir(".tmp", { recursive: true });
    await fs.writeFile(".tmp/foo.html", "foo.html content", "utf8");
    await fs.mkdir(".tmp/foo", { recursive: true });
    await fs.writeFile(".tmp/foo/index.html", "foo/index.html content", "utf8");
    await fs.writeFile(".tmp/foo/bar.html", "foo/bar.html content", "utf8");
    await fs.mkdir(".tmp/intl/es", { recursive: true });
    await fs.writeFile(".tmp/intl/es/index.html", "hola", "utf8");
    await fs.mkdir(".tmp/intl/fr", { recursive: true });
    await fs.writeFile(".tmp/intl/fr/index.html", "French Index!", "utf8");
    await fs.mkdir(".tmp/intl/jp_ALL", { recursive: true });
    await fs.writeFile(".tmp/intl/jp_ALL/other.html", "Japanese!", "utf8");
    await fs.mkdir(".tmp/intl/fr_ca", { recursive: true });
    await fs.writeFile(".tmp/intl/fr_ca/index.html", "French CA!", "utf8");
    await fs.mkdir(".tmp/intl/ALL_ca", { recursive: true });
    await fs.writeFile(".tmp/intl/ALL_ca/index.html", "Oh Canada", "utf8");
    await fs.writeFile(".tmp/intl/ALL_ca/hockey.html", "Only Canada", "utf8");

    app = connect().use(
      (req, res: ServerResponse & { superstatic?: Responder }, next) => {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        res.superstatic = new Responder(req, res, { provider });
        next();
      },
    );
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("should resolve i18n content by accept-language", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/")
      .set("accept-language", "es")
      .expect(200, "hola");
  });

  it("should resolve default files if nothing is provided", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/foo.html")
      .set("accept-language", "jp")
      .expect(200, "foo.html content");
  });

  it("should resolve i18n content by x-country-code", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/")
      .set("x-country-code", "ca")
      .expect(200, "Oh Canada");
  });

  it("should not show i18n content for other countries", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/hockey.html")
      .set("x-country-code", "jp")
      .expect(404);
  });

  it("should allow i18n content specific to a country", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/hockey.html")
      .set("x-country-code", "ca")
      .expect(200, "Only Canada");
  });

  it("should resolve i18n content by accept-language and x-country-code", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/")
      .set("accept-language", "fr")
      .set("x-country-code", "ca")
      .expect(200, "French CA!");
  });

  it("should override the content using cookies for location", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/")
      .set("accept-language", "es")
      .set("cookie", "firebase-language-override=fr")
      .expect(200, "French Index!");
  });

  it("should override the content using cookies for location and country", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/")
      .set("accept-language", "en")
      .set(
        "cookie",
        "firebase-language-override=fr; firebase-country-override=ca",
      )
      .expect(200, "French CA!");
  });

  it("should allow i18n resolution by language with _ALL", async () => {
    app.use(files({ i18n: { root: "/intl" } }, { provider }));

    await request(app)
      .get("/other.html")
      .set("accept-language", "jp")
      .expect(200, "Japanese!");
  });
});

describe("static server with trailing slash customization", () => {
  const provider = fsProvider({
    public: ".tmp",
  });
  let app: connect.Server;

  beforeEach(async () => {
    await fs.mkdir(".tmp", { recursive: true });
    await fs.writeFile(".tmp/foo.html", "foo.html content", "utf8");
    await fs.mkdir(".tmp/foo", { recursive: true });
    await fs.writeFile(".tmp/foo/index.html", "foo/index.html content", "utf8");
    await fs.writeFile(".tmp/foo/bar.html", "foo/bar.html content", "utf8");

    app = connect().use(
      (req, res: ServerResponse & { superstatic?: Responder }, next) => {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        res.superstatic = new Responder(req, res, { provider });
        next();
      },
    );
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("serves html file", async () => {
    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/foo.html")
      .expect(200)
      .expect("foo.html content")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves html file with unicode name", async () => {
    await fs.writeFile(".tmp/äää.html", "test", "utf8");

    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/äää.html")
      .expect(200)
      .expect("test")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves css file", async () => {
    await fs.writeFile(".tmp/style.css", "body {}", "utf8");

    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/style.css")
      .expect(200)
      .expect("body {}")
      .expect("content-type", "text/css; charset=utf-8");
  });

  it("serves a directory index file", async () => {
    await fs.writeFile(".tmp/index.html", "test", "utf8");

    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/")
      .expect(200)
      .expect("test")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves a file with query parameters", async () => {
    await fs.writeFile(".tmp/superstatic.html", "test", "utf8");

    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/superstatic.html?key=value")
      .expect(200)
      .expect("test");
  });

  it("does not redirect the root url because of the trailing slash", async () => {
    await fs.writeFile(".tmp/index.html", "an actual index", "utf8");

    app.use(files({}, { provider: provider }));

    await request(app).get("/").expect(200).expect("an actual index");
  });

  it("does not redirect for directory index files", async () => {
    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/foo/")
      .expect(200)
      .expect("foo/index.html content");
  });

  it("function() directory index to have a trailing slash", async () => {
    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/foo")
      .expect((res) => {
        expect(res.headers.location).to.equal("/foo/");
      })
      .expect(301);
  });

  it("preserves query parameters and slash on subdirectory directory index redirect", async () => {
    app.use(files({}, { provider: provider }));

    await request(app)
      .get("/foo?query=params")
      .expect((req) => {
        expect(req.headers.location).to.equal("/foo/?query=params");
      })
      .expect(301);
  });

  describe("force trailing slash", () => {
    it("adds slash to url with no extension", async () => {
      app.use(files({ trailingSlash: true }, { provider: provider }));

      await request(app).get("/foo").expect(301).expect("Location", "/foo/");
    });
  });

  describe("force remove trailing slash", () => {
    it("removes trailing slash on urls with no file extension", async () => {
      app.use(files({ trailingSlash: false }, { provider: provider }));

      await request(app).get("/foo/").expect(301).expect("Location", "/foo");
    });

    it("returns a 404 if a trailing slash was added to a valid path", async () => {
      app.use(files({ trailingSlash: false }, { provider: provider }));

      await request(app).get("/foo.html/").expect(404);
    });

    it("removes trailing slash on directory index urls", async () => {
      app.use(files({ trailingSlash: false }, { provider: provider }));

      await request(app).get("/foo/").expect(301).expect("Location", "/foo");
    });

    it("normalizes multiple leading slashes on a redirect", async () => {
      app.use(files({ trailingSlash: false }, { provider: provider }));

      await request(app).get("/foo////").expect(301).expect("Location", "/foo");
    });
  });

  [
    {
      trailingSlashBehavior: undefined,
      cleanUrls: false,
      tests: [
        { path: "/foo", wantRedirect: "/foo/" },
        { path: "/foo.html", wantContent: "foo.html content" },
        { path: "/foo.html/", wantNotFound: true },
        { path: "/foo/", wantContent: "foo/index.html content" },
        { path: "/foo/bar", wantNotFound: true },
        { path: "/foo/bar.html", wantContent: "foo/bar.html content" },
        { path: "/foo/bar.html/", wantNotFound: true },
        { path: "/foo/bar/", wantNotFound: true },
        { path: "/foo/index", wantNotFound: true },
        { path: "/foo/index.html", wantContent: "foo/index.html content" },
        { path: "/foo/index.html/", wantNotFound: true },
      ],
    },
    {
      trailingSlashBehavior: false,
      cleanUrls: false,
      tests: [
        { path: "/foo", wantContent: "foo/index.html content" },
        { path: "/foo.html", wantContent: "foo.html content" },
        { path: "/foo.html/", wantNotFound: true },
        { path: "/foo/", wantRedirect: "/foo" },
        { path: "/foo/bar", wantNotFound: true },
        { path: "/foo/bar.html", wantContent: "foo/bar.html content" },
        { path: "/foo/bar.html/", wantNotFound: true },
        { path: "/foo/bar/", wantNotFound: true },
        { path: "/foo/index", wantNotFound: true },
        { path: "/foo/index.html", wantContent: "foo/index.html content" },
        { path: "/foo/index.html/", wantNotFound: true },
      ],
    },
    {
      trailingSlashBehavior: true,
      cleanUrls: false,
      tests: [
        { path: "/foo", wantRedirect: "/foo/" },
        { path: "/foo.html", wantContent: "foo.html content" },
        { path: "/foo.html/", wantNotFound: true },
        { path: "/foo/", wantContent: "foo/index.html content" },
        { path: "/foo/bar", wantNotFound: true },
        { path: "/foo/bar.html", wantContent: "foo/bar.html content" },
        { path: "/foo/bar.html/", wantNotFound: true },
        { path: "/foo/bar/", wantNotFound: true },
        { path: "/foo/index", wantNotFound: true },
        { path: "/foo/index.html", wantContent: "foo/index.html content" },
        { path: "/foo/index.html/", wantNotFound: true },
      ],
    },
    {
      trailingSlashBehavior: undefined,
      cleanUrls: true,
      tests: [
        { path: "/foo", wantContent: "foo/index.html content" },
        { path: "/foo.html", wantRedirect: "/foo" },
        { path: "/foo.html/", wantNotFound: true },
        { path: "/foo/", wantContent: "foo/index.html content" },
        { path: "/foo/bar", wantContent: "foo/bar.html content" },
        { path: "/foo/bar.html", wantRedirect: "/foo/bar" },
        { path: "/foo/bar.html/", wantNotFound: true },
        { path: "/foo/bar/", wantNotFound: true },
        { path: "/foo/index", wantRedirect: "/foo" },
        { path: "/foo/index.html", wantRedirect: "/foo" },
        { path: "/foo/index.html/", wantNotFound: true },
      ],
    },
    {
      trailingSlashBehavior: false,
      cleanUrls: true,
      tests: [
        { path: "/foo", wantContent: "foo/index.html content" },
        { path: "/foo.html", wantRedirect: "/foo" },
        { path: "/foo.html/", wantNotFound: true },
        { path: "/foo/", wantRedirect: "/foo" },
        { path: "/foo/bar", wantContent: "foo/bar.html content" },
        { path: "/foo/bar.html", wantRedirect: "/foo/bar" },
        { path: "/foo/bar.html/", wantNotFound: true },
        { path: "/foo/bar/", wantRedirect: "/foo/bar" },
        { path: "/foo/index", wantRedirect: "/foo" },
        { path: "/foo/index.html", wantRedirect: "/foo" },
        { path: "/foo/index.html/", wantNotFound: true },
      ],
    },
    {
      trailingSlashBehavior: true,
      cleanUrls: true,
      tests: [
        { path: "/foo", wantRedirect: "/foo/" },
        { path: "/foo.html", wantRedirect: "/foo/" },
        { path: "/foo.html/", wantNotFound: true },
        { path: "/foo/", wantContent: "foo/index.html content" },
        { path: "/foo/bar", wantRedirect: "/foo/bar/" },
        { path: "/foo/bar.html", wantRedirect: "/foo/bar/" },
        { path: "/foo/bar.html/", wantNotFound: true },
        { path: "/foo/bar/", wantContent: "foo/bar.html content" },
        { path: "/foo/index", wantRedirect: "/foo/" },
        { path: "/foo/index.html", wantRedirect: "/foo/" },
        { path: "/foo/index.html/", wantNotFound: true },
      ],
    },
  ].forEach((t) => {
    const desc = `trailing slash ${t.trailingSlashBehavior} cleanUrls ${t.cleanUrls}`;
    t.tests.forEach((tt) => {
      const ttDesc = `${desc} ${JSON.stringify(tt)}`;
      it("should behave correctly: " + ttDesc, async () => {
        app.use(
          files(
            { trailingSlash: t.trailingSlashBehavior, cleanUrls: t.cleanUrls },
            { provider: provider },
          ),
        );

        const r = request(app).get(tt.path);
        if (tt.wantRedirect) {
          await r.expect(301).expect("Location", tt.wantRedirect);
        } else if (tt.wantNotFound) {
          await r.expect(404);
        } else if (tt.wantContent) {
          await r.expect(200).expect(tt.wantContent);
        } else {
          throw new Error("Test set up incorrectly");
        }
      });
    });
  });
});


================================================
FILE: test/unit/middleware/headers.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const helpers = require("../../helpers");
const headers = helpers.decorator(require("../../../src/middleware/headers"));
const connect = require("connect");
const request = require("supertest");

const defaultHeaders = [
  { source: "/test1", headers: [{ key: "Content-Type", value: "mime/type" }] },
  {
    source: "/test3",
    headers: [
      { key: "Access-Control-Allow-Origin", value: "https://www.example.net" },
    ],
  },
  {
    source: "/api/**",
    headers: [{ key: "Access-Control-Allow-Origin", value: "*" }],
  },
];

function okay(req, res) {
  res.writeHead(200);
  res.end();
}

describe("cors middleware", () => {
  it("serves custom content types", (done) => {
    const app = connect()
      .use(headers({ headers: defaultHeaders }))
      .use(okay);

    void request(app)
      .get("/test1")
      .expect(200)
      .expect("Content-Type", "mime/type")
      .end(done);
  });

  it("serves custom access control headers", (done) => {
    const app = connect()
      .use(headers({ headers: defaultHeaders }))
      .use(okay);

    void request(app)
      .get("/test3")
      .expect(200)
      .expect("Access-Control-Allow-Origin", "https://www.example.net")
      .end(done);
  });

  it("uses routing rules", (done) => {
    const app = connect()
      .use(headers({ headers: defaultHeaders }))
      .use(okay);

    void request(app)
      .get("/api/whatever/you/wish")
      .expect(200)
      .expect("Access-Control-Allow-Origin", "*")
      .end(done);
  });

  it("uses glob negation to set headers", (done) => {
    const app = connect()
      .use(
        headers({
          headers: [
            {
              source: "!/anything/**",
              headers: [{ key: "custom-header", value: "for testing" }],
            },
          ],
        }),
      )
      .use(okay);

    void request(app)
      .get("/something")
      .expect(200)
      .expect("custom-header", "for testing")
      .end(done);
  });

  it("uses regular expressions to set headers", (done) => {
    const app = connect()
      .use(
        headers({
          headers: [
            {
              regex: "/resources/\\d+\\.jpg",
              headers: [{ key: "custom-header", value: "for testing" }],
            },
          ],
        }),
      )
      .use(okay);

    void request(app)
      .get("/resources/281.jpg")
      .expect(200)
      .expect("custom-header", "for testing")
      .end(done);
  });
});


================================================
FILE: test/unit/middleware/missing.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as connect from "connect";
import * as fs from "node:fs/promises";
import * as request from "supertest";

const fsProvider = require("../../../src/providers/fs");
import * as helpers from "../../helpers";
import * as missingModule from "../../../src/middleware/missing";
import * as Responder from "../../../src/responder";

const missing = helpers.decorator(missingModule);

describe("custom not found", () => {
  const provider = fsProvider({
    public: ".tmp",
  });
  let app: connect.Server;

  beforeEach(async () => {
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/not-found.html", "custom not found file");

    app = connect().use(
      (
        req: connect.IncomingMessage,
        res: any, // TODO(bkendall): extend http.ServerResponse.
        next: connect.NextFunction,
      ): void => {
        res.superstatic = new Responder(req, res, { provider: provider });
        next();
      },
    );
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true });
  });

  it("serves the file", async () => {
    app.use(missing({ errorPage: "/not-found.html" }, { provider: provider }));

    await request(app)
      .get("/anything")
      .expect(404)
      .expect("custom not found file");
  });

  it("skips middleware on file serve error", async () => {
    app
      .use(
        missing({ errorPage: "/does-not-exist.html" }, { provider: provider }),
      )
      .use((req, res) => {
        res.end("does not exist");
      });

    await request(app).get("/anything").expect("does not exist");
  });

  describe("with i18n files", () => {
    beforeEach(async () => {
      await fs.mkdir(".tmp/i18n/fr", { recursive: true });
      await fs.writeFile(
        ".tmp/i18n/fr/not-found.html",
        "my custom 404, in French",
      );
    });

    it("should resolve to the normal error page by default", async () => {
      app.use(
        missing(
          { errorPage: "/not-found.html", i18n: { root: "/i18n" } },
          { provider: provider },
        ),
      );

      await request(app).get("/anything").expect(404, "custom not found file");
    });

    it("should resolve the i18n missing page if one was provided and matches", async () => {
      app.use(
        missing(
          { errorPage: "/not-found.html", i18n: { root: "/i18n" } },
          { provider: provider },
        ),
      );

      await request(app)
        .get("/anything")
        .set("accept-language", "fr")
        .expect(404, "my custom 404, in French");
    });
  });
});


================================================
FILE: test/unit/middleware/not-found.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const fs = require("node:fs/promises");
const request = require("supertest");
const connect = require("connect");
const { join } = require("path");
const { expect } = require("chai");

const notFound = require("../../../src/middleware/not-found");
const Responder = require("../../../src/responder");

describe("not found", () => {
  let app;

  beforeEach(async () => {
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/not-found.html", "not found file", "utf8");

    app = connect().use((req, res, next) => {
      res.superstatic = new Responder(req, res, {
        provider: {},
      });
      next();
    });
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("serves the file", (done) => {
    app.use(
      notFound({
        errorPage: ".tmp/not-found.html",
      }),
    );

    void request(app)
      .get("/anything")
      .expect(404)
      .expect("not found file")
      .end(done);
  });

  it("throws on file read error", () => {
    expect(() => {
      notFound({
        errorPage: ".tmp/does-not-exist.html",
      });
    }).to.throw("ENOENT");
  });

  it("caches for one hour", (done) => {
    app.use(
      notFound({
        errorPage: join(process.cwd(), ".tmp/not-found.html"),
      }),
    );

    void request(app)
      .get("/anything")
      .expect(404)
      .expect("Cache-Control", "public, max-age=3600")
      .end(done);
  });
});


================================================
FILE: test/unit/middleware/redirects.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const helpers = require("../../helpers");
const redirect = helpers.decorator(
  require("../../../src/middleware/redirects"),
);
const connect = require("connect");
const request = require("supertest");
const Responder = require("../../../src/responder");
const patterns = require("../../../src/utils/patterns");
const superstaticSetup = function (req, res, next) {
  res.superstatic = new Responder(req, res, { provider: {} });
  next();
};

describe("redirect middleware", () => {
  it("skips the middleware if there are no redirects configured", (done) => {
    const app = connect().use(redirect({ redirects: [] }));

    void request(app).get("/").expect(404).end(done);
  });

  it("skips middleware when there are no matching redirects", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "/redirect",
              type: 301,
            },
          ],
        }),
      );

    void request(app).get("/none").expect(404).end(done);
  });

  it("redirects to a configured path", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "/redirect",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(301)
      .expect("location", "/redirect")
      .end(done);
  });

  it("recognizes glob as synonymous with source", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              glob: "/source",
              destination: "/redirect",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(301)
      .expect("location", "/redirect")
      .end(done);
  });

  it("redirects to a configured regexp path", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              regex: "/source",
              destination: "/redirect",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(301)
      .expect("location", "/redirect")
      .end(done);
  });

  it("redirects to a configured path with a custom status code", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "/redirect",
              type: 302,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(302)
      .expect("location", "/redirect")
      .end(done);
  });

  it("adds leading slash to all redirect paths", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "source",
              destination: "/redirect",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(301)
      .expect("location", "/redirect")
      .end(done);
  });

  it("redirects using glob negation", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "!source",
              destination: "/redirect",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/anthing")
      .expect(301)
      .expect("location", "/redirect")
      .end(done);
  });

  it("redirects using segments in the url path", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/old/:value/path/:loc",
              destination: "/new/:value/path/:loc",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/old/redirect/path/there")
      .expect(301)
      .expect("location", "/new/redirect/path/there")
      .end(done);
  });

  it("uses capturing groups as segments when given a regex", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              regex: "/old/(.+)/group/(.+)",
              destination: "/new/:1/path/:2",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/old/capture/group/there")
      .expect(301)
      .expect("location", "/new/capture/path/there")
      .end(done);
  });

  it("handles Unicode codepoints in regexes", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              regex: "/äöü",
              destination: "/aou",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/äöü")
      .expect(301)
      .expect("location", "/aou")
      .end(done);
  });

  it("percent encodes the redirect location", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              regex: "/aou",
              destination: "/ć",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/aou")
      .expect(301)
      .expect("location", "/%C4%87")
      .end(done);
  });

  it("redirects using regexp captures inside path segments", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              regex: "/foo/(.+)bar/baz",
              destination: "/:1",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/foo/barbar/baz")
      .expect(301)
      .expect("location", "/bar")
      .end(done);
  });

  it("redirects using regexp captures across path segments", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              regex: "/foo/(.+)/bar",
              destination: "/:1",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/foo/1/2/3/4/bar")
      .expect(301)
      .expect("location", "/1/2/3/4")
      .end(done);
  });

  if (patterns.re2Available()) {
    it("redirects using RE2 capturing groups", (done) => {
      const app = connect()
        .use(superstaticSetup)
        .use(
          redirect({
            redirects: [
              {
                regex: "/(?P<asdf>foo)/bar",
                destination: "/:asdf",
                type: 301,
              },
            ],
          }),
        );

      void request(app)
        .get("/foo/bar")
        .expect(301)
        .expect("location", "/foo")
        .end(done);
    });

    it("redirects using both named and unnamed capture groups", (done) => {
      const app = connect()
        .use(superstaticSetup)
        .use(
          redirect({
            redirects: [
              {
                regex: "/(?P<asdf>.+)/(.+)/(?P<jkl>.+)",
                destination: "/:asdf/:2/:jkl",
                type: 301,
              },
            ],
          }),
        );

      void request(app)
        .get("/mixed/capture/types")
        .expect(301)
        .expect("location", "/mixed/capture/types")
        .end(done);
    });
  }

  it("redirects a missing optional segment", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/old/:value?",
              destination: "/new/:value?",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/old/")
      .expect(301)
      .expect("location", "/new")
      .end(done);
  });

  it("redirects a present optional segment", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/old/:value?",
              destination: "/new/:value?",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/old/derp")
      .expect(301)
      .expect("location", "/new/derp")
      .end(done);
  });

  it("redirects a splat segment", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/blog/:post*",
              destination: "/new/:post*",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/blog/this/old/post")
      .expect(301)
      .expect("location", "/new/this/old/post")
      .end(done);
  });

  it("redirects using segments in the url path with a 302", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/old/:value/path/:loc",
              destination: "/new/:value/path/:loc",
              type: 302,
            },
          ],
        }),
      );

    void request(app)
      .get("/old/redirect/path/there")
      .expect(302)
      .expect("location", "/new/redirect/path/there")
      .end(done);
  });

  it("redirects to external http url", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "http://redirectedto.com",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(301)
      .expect("Location", "http://redirectedto.com")
      .end(done);
  });

  it("redirects to external https url", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "https://redirectedto.com",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source")
      .expect(301)
      .expect("Location", "https://redirectedto.com")
      .end(done);
  });

  it("preserves query params when redirecting", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "/destination",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source?foo=bar&baz=qux")
      .expect(301)
      .expect("Location", "/destination?foo=bar&baz=qux")
      .end(done);
  });

  it("appends query params to the destination when redirecting", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "/destination?hello=world",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source?foo=bar&baz=qux")
      .expect(301)
      .expect("Location", "/destination?hello=world&foo=bar&baz=qux")
      .end(done);
  });

  it("preserves query params when redirecting to external urls", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source",
              destination: "http://example.com/destination",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source?foo=bar&baz=qux")
      .expect(301)
      .expect("Location", "http://example.com/destination?foo=bar&baz=qux")
      .end(done);
  });

  it("preserves query params when redirecting with captures", (done) => {
    const app = connect()
      .use(superstaticSetup)
      .use(
        redirect({
          redirects: [
            {
              source: "/source/:foo",
              destination: "/:foo/bar",
              type: 301,
            },
          ],
        }),
      );

    void request(app)
      .get("/source/wat?foo=bar&baz=qux")
      .expect(301)
      .expect("Location", "/wat/bar?foo=bar&baz=qux")
      .end(done);
  });
});


================================================
FILE: test/unit/middleware/rewrites.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import * as fs from "node:fs/promises";
import * as request from "supertest";
import * as connect from "connect";

import * as helpers from "../../helpers";
import * as rewritesPkg from "../../../src/middleware/rewrites";
const fsProvider = require("../../../src/providers/fs");
import * as Responder from "../../../src/responder";

const rewrites = helpers.decorator(rewritesPkg);

describe("static router", () => {
  const provider = fsProvider({
    public: ".tmp",
  });
  let app: connect.Server;

  beforeEach(async () => {
    await fs.mkdir(".tmp", { recursive: true });
    await fs.writeFile(".tmp/index.html", "index", "utf8");

    app = connect().use((req, res: any, next) => {
      res.superstatic = new Responder(req, res, { provider });
      next();
    });
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("serves a route", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            source: "/my-route",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/my-route")
      .expect(200)
      .expect("index")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves a route with a glob", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            source: "**",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/my-route")
      .expect(200)
      .expect("index")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves a route with a regex", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            regex: ".*",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/my-route")
      .expect(200)
      .expect("index")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves a route with an extension via a glob", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            source: "**",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/my-route.py")
      .expect(200)
      .expect("index")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves a route with an extension via a regex", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            regex: "/\\w+\\.py",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/myroute.py")
      .expect(200)
      .expect("index")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("serves a negated route", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            source: "!/no",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/my-route")
      .expect(200)
      .expect("index")
      .expect("content-type", "text/html; charset=utf-8");
  });

  it("skips if no match is found", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            source: "/skip",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app).get("/hi").expect(404);
  });

  it("serves the mime type of the rewritten file", async () => {
    app.use(
      rewrites({
        rewrites: [
          {
            source: "**",
            destination: "/index.html",
          },
        ],
      }),
    );

    await request(app)
      .get("/index.js")
      .expect("content-type", "text/html; charset=utf-8");
  });

  describe("uses first match", () => {
    beforeEach(async () => {
      await fs.mkdir(".tmp/admin", { recursive: true });
      await fs.writeFile(".tmp/admin/index.html", "admin index", "utf8");

      app.use(
        rewrites({
          rewrites: [
            { source: "/admin/**", destination: "/admin/index.html" },
            { source: "/something/**", destination: "/something/indexf.html" },
            { source: "**", destination: "/index.html" },
          ],
        }),
      );
    });

    it("first route with 1 depth route", async () => {
      await request(app)
        .get("/admin/anything")
        .expect(200)
        .expect("admin index");
    });

    it("first route with 2 depth route", async () => {
      await request(app)
        .get("/admin/anything/else")
        .expect(200)
        .expect("admin index");
    });

    it("second route", async () => {
      await request(app).get("/anything").expect(200).expect("index");
    });
  });

  describe("a relative rewrite", () => {
    beforeEach(() => {
      app.use(
        rewrites({
          rewrites: [{ source: "**", destination: "index.html" }],
        }),
      );
    });

    it("should never work", async () => {
      await request(app).get("/about").expect(404);
    });
  });
});


================================================
FILE: test/unit/providers/fs.spec.ts
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

import { use, expect } from "chai";
import * as chaiAsPromised from "chai-as-promised";
import * as path from "node:path";
import * as fs from "node:fs";
const concatStream = require("concat-stream");
use(chaiAsPromised);

const fsp = require("../../../src/providers/fs");

async function readStatStream(
  stat: {
    stream: fs.ReadStream;
  } | null,
): Promise<string> {
  if (!stat) {
    throw new Error("do not have stat");
  }
  const stream = stat?.stream;
  return new Promise((resolve, reject) => {
    stream.on("error", reject);
    stream.pipe(concatStream({ encoding: "string" }, resolve));
  });
}

describe("provider: fs", () => {
  let opts: { cwd?: string; public?: string[] | string } = {};

  beforeEach(() => {
    opts = {
      cwd: path.resolve(path.join(__dirname, "..", "..", "fixtures")),
      public: "a",
    };
  });

  it("should return stat information for a file that exists", async () => {
    await fsp(opts)({}, "/index.html")
      .then(readStatStream)
      .then((body: string) => {
        expect(body.trim()).to.eq("A");
      });
  });

  it("should return null if ../", async () => {
    await expect(fsp(opts)({}, "/../b/b.html")).to.eventually.be.null;
  });

  it("should return null if ..\\", async () => {
    await expect(fsp(opts)({}, "/..\\b\\b.html")).to.eventually.be.null;
  });

  it("should return null if ..%5c", async () => {
    await expect(fsp(opts)({}, "/..%5Cb%5cb.html")).to.eventually.be.null;
  });

  it("should return null if path has null bytes", async () => {
    await expect(fsp(opts)({}, "/\0a.html")).to.eventually.be.null;
  });

  it("should return null for a file that does not exist", async () => {
    await expect(fsp(opts)({}, "/bogus.html")).to.eventually.be.null;
  });

  describe("multiple publics", () => {
    beforeEach(() => {
      opts.public = ["a", "b"];
    });

    it("should return the first file found for multiple publics", async () => {
      await fsp(opts)({}, "/index.html")
        .then(readStatStream)
        .then((body: string) => {
          expect(body.trim()).to.eq("A");
        });

      await fsp(opts)({}, "/b.html")
        .then(readStatStream)
        .then((body: string) => {
          expect(body.trim()).to.eq("B");
        });
    });

    it("should return null if neither public has the file", async () => {
      await expect(fsp(opts)({}, "/bogus.html")).to.eventually.be.null;
    });
  });
});


================================================
FILE: test/unit/providers/memory.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const chai = require("chai");
chai.use(require("chai-as-promised"));
const expect = chai.expect;
const memoryProvider = require("../../../src/providers/memory");

describe("memory provider", () => {
  let store;
  let provider;
  beforeEach(() => {
    store = store ?? {};
    provider = memoryProvider({ store: store });
  });

  it("should resolve null if not in the store", () => {
    return expect(provider({}, "/whatever")).to.eventually.be.null;
  });

  it("should return a stream of the content if found", (done) => {
    store["/index.html"] = "foobar";
    provider({}, "/index.html").then((result) => {
      let out = "";
      result.stream.on("data", (data) => {
        out += data;
      });
      result.stream.on("end", () => {
        expect(out).to.eq("foobar");
        done();
      });
    }, done);
  });

  it("should return an etag of the content", async () => {
    store["/a.html"] = "foo";
    store["/b.html"] = "bar";
    return Promise.resolve({
      a: await provider({}, "/a.html"),
      b: await provider({}, "/b.html"),
    }).then((result) => {
      expect(result.a.etag).to.not.equal(null);
      expect(result.b.etag).to.not.equal(null);
      expect(result.a.etag).not.to.eq(result.b.etag);
    });
  });

  it("should return the length of content", () => {
    store["/index.html"] = "foobar";
    return expect(provider({}, "/index.html")).to.eventually.have.property(
      "size",
      6,
    );
  });

  afterEach(() => {
    store = null;
  });
});


================================================
FILE: test/unit/responder.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const Responder = require("../../src/responder");
const chai = require("chai");
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const sinon = require("sinon");
chai.use(require("chai-as-promised"));
chai.use(require("sinon-chai"));
const expect = chai.expect;

describe("Responder", () => {
  let responder;

  describe("#handle", () => {
    beforeEach(() => {
      responder = new Responder({}, { setHeader: noop, end: noop }, {});
    });

    it("should resolve as false with an empty stack", () => {
      return expect(responder.handle([])).to.eventually.eq(false);
    });

    it("should call the stack if an array is passed", () => {
      return expect(responder.handle([{ data: "abcdef" }])).to.eventually.eq(
        true,
      );
    });

    it("should call through to handleFile with a string", () => {
      const stub = sinon
        .stub(responder, "handleFile")
        .returns(Promise.resolve(true));
      responder.handle("abc/def.html");
      expect(stub).to.have.been.calledWith({ file: "abc/def.html" });
    });

    it("should call through to handleFile with a file object", () => {
      const stub = sinon
        .stub(responder, "handleFile")
        .returns(Promise.resolve(true));
      responder.handle({ file: "abc/def.html" });
      expect(stub).to.have.been.calledWith({ file: "abc/def.html" });
    });

    it("should call through to handleData with a data object", () => {
      const stub = sinon
        .stub(responder, "handleData")
        .returns(Promise.resolve(true));
      const obj = { data: "abc def" };
      responder.handle(obj);
      expect(stub).to.have.been.calledWith(obj);
    });

    it("should call through to handleRedirect with a redirect object", () => {
      const stub = sinon
        .stub(responder, "handleRedirect")
        .returns(Promise.resolve(true));
      const obj = { redirect: "/" };
      responder.handle(obj);
      expect(stub).to.have.been.calledWith(obj);
    });

    it("should call through to handleRewrite with a rewrite object", () => {
      const stub = sinon
        .stub(responder, "handleRewrite")
        .returns(Promise.resolve(true));
      const obj = { rewrite: {} };
      responder.handle(obj);
      expect(stub).to.have.been.calledWith(obj);
    });
  });

  describe("#_handle", () => {
    beforeEach(() => {
      responder = new Responder({}, { setHeader: noop, end: noop }, {});
    });

    it("should reject with an unrecognized payload", () => {
      return expect(responder._handle({ foo: "bar" })).to.be.rejectedWith(
        "is not a recognized responder directive",
      );
    });
  });

  describe("#handleRewrite", () => {
    it("should call through to a registered custom rewriter", () => {
      let out;
      responder = new Responder(
        {},
        {
          setHeader: noop,
          end: function (data) {
            out = data;
          },
        },
        {
          rewriters: {
            message: function (rewrite) {
              return Promise.resolve({
                data: rewrite.message,
                contentType: "text/plain",
                status: 200,
              });
            },
          },
        },
      );

      return responder
        .handleRewrite({ rewrite: { message: "hi" } })
        .then((result) => {
          expect(result).to.equal(true);
          expect(out).to.equal("hi");
        });
    });
  });

  describe("#handleMiddleware", () => {
    let rq;
    beforeEach(() => {
      rq = {};
      responder = new Responder(rq, { setHeader: noop, end: noop }, {});
    });

    it("should call the middleware", (done) => {
      responder.handleMiddleware(() => {
        done();
      });
    });

    it("should resolve false if next is called", () => {
      return responder
        .handleMiddleware((req, res, next) => {
          next();
        })
        .then((result) => {
          expect(result).to.equal(false);
        });
    });
  });

  describe("#handleFile", () => {
    const req = {};
    const res = {};
    let stub;

    beforeEach(() => {
      stub = sinon.stub();
      responder = new Responder(req, res, {
        provider: stub,
      });
    });

    it("should call through to provider", async () => {
      stub.returns(Promise.resolve());
      await responder.handleFile({ file: "abc/def.html" });
      expect(stub).to.have.been.calledWithExactly(req, "abc/def.html");
    });
  });

  describe("#isNotModified", () => {
    let result;

    beforeEach(() => {
      responder = new Responder({ headers: {} }, {}, {});
      result = {
        modified: Date.now(),
        etag: "abcdef",
      };
    });

    it("should be false if there are no if-modified-since or if-none-match headers", () => {
      expect(responder.isNotModified(result)).to.equal(false);
    });

    it("should be false if there is a non-matching etag", () => {
      responder.req.headers["if-none-match"] = "defabc";
      expect(responder.isNotModified(result)).to.equal(false);
    });

    it("should be true if there is a matching etag", () => {
      responder.req.headers["if-none-match"] = "abcdef";
      expect(responder.isNotModified(result)).to.equal(true);
    });

    it("should be true if there is an if-modified-since after the modified", () => {
      responder.req.headers["if-modified-since"] = new Date(
        result.modified + 30000,
      ).toUTCString();
      expect(responder.isNotModified(result)).to.equal(true);
    });

    it("should be false if there is an if-modified-since before the modified", () => {
      responder.req.headers["if-modified-since"] = new Date(
        result.modified - 30000,
      ).toUTCString();
      expect(responder.isNotModified(result)).to.equal(false);
    });
  });

  describe("#handleNotModified", () => {
    it("should return true, indicating it responded", () => {
      responder = new Responder({}, { removeHeader: noop, end: noop }, {});

      const r = responder.handleNotModified();
      expect(r).to.equal(true);
    });
  });
});


================================================
FILE: test/unit/server.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

const path = require("path");

const fs = require("node:fs/promises");
const request = require("supertest");
const expect = require("chai").expect;
const stdMocks = require("std-mocks");

const server = require("../../src/server");

// NOTE: skipping these tests because of how
// supertest runs a connect server. The Superstatic
// server runs with a #listen() method, while the
// supertest runner uses the connect app object in
// a bare http.createServer() method. This
// doesn't work with how we are loading services.
describe.skip("server", () => {
  beforeEach(async () => {
    await fs.mkdir(".tmp");
    await fs.writeFile(".tmp/index.html", "index file content");
    await fs.writeFile(".tmp/.env.json", '{"key": "value"}');
  });

  afterEach(async () => {
    await fs.rm(".tmp", { recursive: true, force: true });
  });

  it("starts a server", (done) => {
    const app = server();

    void request(app).get("/").end(done);
  });

  it("with config", (done) => {
    const app = server({
      config: {
        public: ".tmp",
      },
    });

    void request(app).get("/").expect("index file content").end(done);
  });

  it("with port", (done) => {
    const app = server({
      port: 9876,
    });

    const s = app.listen(() => {
      expect(s.address().port).to.equal(9876);

      s.close(done);
    });
  });

  it("with hostname", (done) => {
    const app = server({
      hostname: "127.0.0.1",
    });

    const s = app.listen(() => {
      expect(s.address().address).to.equal("127.0.0.1");

      s.close(done);
    });
  });

  it("with host", (done) => {
    const app = server({
      host: "127.0.0.1",
    });

    const s = app.listen(() => {
      expect(s.address().address).to.equal("127.0.0.1");

      s.close(done);
    });
  });

  it("with debug", (done) => {
    let output;
    const app = server({
      debug: true,
    });

    stdMocks.use();

    void request(app)
      .get("/")
      .end(() => {
        stdMocks.restore();
        output = stdMocks.flush();

        expect(
          output.stdout.toString().indexOf('"GET / HTTP/1.1" 404'),
        ).to.be.greaterThan(-1);
        done();
      });
  });

  it("with env filename", (done) => {
    const app = server({
      env: ".tmp/.env.json",
      config: {
        public: ".tmp",
      },
    });

    void request(app)
      .get("/__/env.json")
      .expect({
        key: "value",
      })
      .end(done);
  });

  it("with env object", (done) => {
    const app = server({
      env: {
        type: "object",
      },
      config: {
        public: ".tmp",
      },
    });

    void request(app)
      .get("/__/env.json")
      .expect({
        type: "object",
      })
      .end(done);
  });

  it("default error page", async () => {
    const p = path.resolve(__dirname, "../../templates/assets/not_found.html");
    const notFoundContent = await fs.readFile(p, "utf8");

    const app = server();

    return void request(app).get("/nope").expect(404).expect(notFoundContent);
  });

  it("overriden default error page", async () => {
    await fs.writeFile(".tmp/error.html", "error page");

    const app = server({
      errorPage: ".tmp/error.html",
      config: {
        public: ".tmp",
      },
    });

    return void request(app).get("/nope").expect(404).expect("error page");
  });
});


================================================
FILE: test/unit/superstatic.spec.js
================================================
/**
 * Copyright (c) 2022 Google LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

// TODO:
// test loading config file in various forms
// test loading env file in various forms


================================================
FILE: test/unit/utils/i18n.spec.ts
================================================
import { expect } from "chai";
import { i18nContentOptions } from "../../../src/utils/i18n";

describe("i18nContentOptions", () => {
  it("should return no files with no i18n config", () => {
    const paths = i18nContentOptions("/index.html", {
      s
Download .txt
gitextract_0ny9bt5u/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── node-test.yml
├── .gitignore
├── .mocharc.yaml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── changelog.txt
├── eslint.config.mjs
├── examples/
│   ├── middleware/
│   │   ├── app/
│   │   │   └── index.html
│   │   └── index.js
│   └── server/
│       ├── app/
│       │   └── index.html
│       ├── error.html
│       └── index.js
├── package.json
├── src/
│   ├── activator.js
│   ├── bin/
│   │   └── server.ts
│   ├── cli/
│   │   └── index.ts
│   ├── config.ts
│   ├── index.ts
│   ├── loaders/
│   │   └── config-file.js
│   ├── middleware/
│   │   ├── env.ts
│   │   ├── files.js
│   │   ├── headers.js
│   │   ├── index.js
│   │   ├── missing.js
│   │   ├── not-found.js
│   │   ├── protect.js
│   │   ├── redirects.js
│   │   └── rewrites.ts
│   ├── options.ts
│   ├── providers/
│   │   ├── fs.ts
│   │   └── memory.js
│   ├── responder.js
│   ├── server.js
│   ├── superstatic.js
│   └── utils/
│       ├── i18n.ts
│       ├── objectutils.ts
│       ├── pathutils.ts
│       ├── patterns.js
│       └── promiseback.js
├── templates/
│   ├── env.js.template
│   └── not_found.html
├── test/
│   ├── fixtures/
│   │   ├── a/
│   │   │   └── index.html
│   │   └── b/
│   │       ├── b.html
│   │       └── index.html
│   ├── helpers.js
│   ├── integration/
│   │   ├── clean-urls.spec.ts
│   │   ├── error-pages.spec.ts
│   │   ├── i18n.spec.ts
│   │   └── serving-files.spec.ts
│   └── unit/
│       ├── loaders/
│       │   └── config-file.spec.js
│       ├── middleware/
│       │   ├── env.spec.js
│       │   ├── files.spec.ts
│       │   ├── headers.spec.js
│       │   ├── missing.spec.ts
│       │   ├── not-found.spec.js
│       │   ├── redirects.spec.js
│       │   └── rewrites.spec.ts
│       ├── providers/
│       │   ├── fs.spec.ts
│       │   └── memory.spec.js
│       ├── responder.spec.js
│       ├── server.spec.js
│       ├── superstatic.spec.js
│       └── utils/
│           ├── i18n.spec.ts
│           ├── pathutils.spec.ts
│           └── promiseback.spec.js
├── tsconfig.base.json
├── tsconfig.json
└── tsconfig.publish.json
Download .txt
SYMBOL INDEX (49 symbols across 21 files)

FILE: src/cli/index.ts
  constant PORT (line 29) | const PORT = "3474";
  constant HOSTNAME (line 30) | const HOSTNAME = "localhost";
  constant CONFIG_FILENAME (line 31) | const CONFIG_FILENAME = ["superstatic.json", "firebase.json"];
  constant ENV_FILENAME (line 32) | const ENV_FILENAME = ".env.json";

FILE: src/config.ts
  type Configuration (line 1) | interface Configuration {
  type Rewrite (line 13) | interface Rewrite {
  type Redirect (line 18) | interface Redirect {
  type Header (line 24) | interface Header {

FILE: src/loaders/config-file.js
  constant CONFIG_FILE (line 28) | const CONFIG_FILE = ["superstatic.json", "firebase.json"];

FILE: src/middleware/env.ts
  type SuperstaticRequest (line 29) | interface SuperstaticRequest {
  type SuperstaticResponse (line 35) | interface SuperstaticResponse {
  function env (line 47) | function env(spec: { env: Record<string, string> }) {

FILE: src/middleware/files.js
  function normalizeRedirectPath (line 31) | function normalizeRedirectPath(path) {
  function providerResult (line 196) | function providerResult(req, res, p) {

FILE: src/middleware/not-found.js
  constant DEFAULT_ERROR_PAGE (line 24) | const DEFAULT_ERROR_PAGE = __dirname + "/../../templates/not_found.html";

FILE: src/middleware/redirects.js
  function formatExternalUrl (line 28) | function formatExternalUrl(u) {
  function addQuery (line 36) | function addQuery(url, qs) {

FILE: src/middleware/rewrites.ts
  function matcher (line 31) | function matcher(rewrites: Rewrite[]) {

FILE: src/options.ts
  type MiddlewareOptions (line 4) | interface MiddlewareOptions {
  type ServerOptions (line 18) | interface ServerOptions extends MiddlewareOptions {

FILE: src/providers/fs.ts
  function multiStat (line 27) | async function multiStat(
  function fetchEtag (line 51) | async function fetchEtag(pathname: string, stat: fs.Stats): Promise<stri...

FILE: src/superstatic.js
  constant CWD (line 33) | const CWD = process.cwd();

FILE: src/utils/i18n.ts
  function i18nContentOptions (line 36) | function i18nContentOptions(p: string, req: any): string[] {
  function join (line 60) | function join(...arr: string[]): string {
  function getCountryCode (line 70) | function getCountryCode(headers: Record<string, string>): string {
  function getI18nLanguages (line 86) | function getI18nLanguages(headers: Record<string, string>): string[] {
  function cookieValue (line 123) | function cookieValue(cookieString: string, key: string): string {

FILE: src/utils/objectutils.ts
  function isPlainObject (line 27) | function isPlainObject(val: unknown): val is Record<string, unknown> {

FILE: src/utils/pathutils.ts
  constant INDEX_FILE (line 22) | const INDEX_FILE = "index.html";
  function asDirectoryIndex (line 28) | function asDirectoryIndex(pathname: string): string {
  function isDirectoryIndex (line 39) | function isDirectoryIndex(pathname: string): boolean {
  function hasTrailingSlash (line 47) | function hasTrailingSlash(pathname: string): boolean {
  function addTrailingSlash (line 55) | function addTrailingSlash(pathname: string): string {
  function removeTrailingSlash (line 63) | function removeTrailingSlash(pathname: string): string {
  function normalizeMultiSlashes (line 71) | function normalizeMultiSlashes(pathname: string): string {
  function removeTrailingString (line 80) | function removeTrailingString(string: string, rm: string): string {

FILE: src/utils/patterns.js
  constant RE2 (line 22) | let RE2;
  function configMatcher (line 48) | function configMatcher(path, config) {
  function createRaw (line 69) | function createRaw(pattern) {
  function re2Available (line 77) | function re2Available() {
  function containsRE2Capture (line 88) | function containsRE2Capture(pattern) {
  function containsPCRECapture (line 99) | function containsPCRECapture(pattern) {

FILE: test/integration/clean-urls.spec.ts
  function options (line 31) | function options(): MiddlewareOptions & { config: Configuration } {

FILE: test/integration/error-pages.spec.ts
  function options (line 30) | function options(): MiddlewareOptions & { config: Configuration } {

FILE: test/integration/i18n.spec.ts
  function options (line 30) | function options(): MiddlewareOptions & { config: Configuration } {

FILE: test/integration/serving-files.spec.ts
  function options (line 31) | function options(): MiddlewareOptions & { config: Configuration } {

FILE: test/unit/middleware/headers.spec.js
  function okay (line 41) | function okay(req, res) {

FILE: test/unit/providers/fs.spec.ts
  function readStatStream (line 31) | async function readStatStream(
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (210K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 205,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  "
  },
  {
    "path": ".github/workflows/node-test.yml",
    "chars": 1427,
    "preview": "name: CI Tests\n\non:\n  - pull_request\n  - push\n\njobs:\n  check-license:\n    runs-on: ubuntu-latest\n    steps:\n      - uses"
  },
  {
    "path": ".gitignore",
    "chars": 239,
    "preview": "lib-cov\n*.seed\n*.log\n*.csv\n*.dat\n*.out\n*.pid\n*.gz\n\npids\nlogs\nresults\n\ncoverage\n.nyc_output\nnpm-debug.log\nnode_modules\n.t"
  },
  {
    "path": ".mocharc.yaml",
    "chars": 62,
    "preview": "require:\n  - ts-node/register\n  - source-map-support/register\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1451,
    "preview": "Want to contribute? Great! First, read this page (including the small print at the end).\n\n### Before you contribute\n\nBef"
  },
  {
    "path": "Dockerfile",
    "chars": 218,
    "preview": "FROM node:0.10\nADD ./package.json /superstatic/package.json\nWORKDIR /superstatic\nRUN npm install\nADD . /superstatic\n\nVOL"
  },
  {
    "path": "LICENSE",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2022 Google LLC\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "README.md",
    "chars": 10626,
    "preview": "# Superstatic   [![NPM Module](http://img.shields.io/npm/v/superstatic.svg?style=flat-square)](https://npmjs.org/package"
  },
  {
    "path": "changelog.txt",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "eslint.config.mjs",
    "chars": 3149,
    "preview": "import eslint from \"@eslint/js\";\nimport eslintConfigGoogle from \"eslint-config-google\";\nimport eslintConfigPrettier from"
  },
  {
    "path": "examples/middleware/app/index.html",
    "chars": 1346,
    "preview": "<!--\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "examples/middleware/index.js",
    "chars": 1578,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "examples/server/app/index.html",
    "chars": 1338,
    "preview": "<!--\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "examples/server/error.html",
    "chars": 1322,
    "preview": "<!--\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "examples/server/index.js",
    "chars": 1498,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "package.json",
    "chars": 2737,
    "preview": "{\n  \"name\": \"superstatic\",\n  \"version\": \"10.0.0\",\n  \"description\": \"A static file server for fancy apps\",\n  \"main\": \"./l"
  },
  {
    "path": "src/activator.js",
    "chars": 2596,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/bin/server.ts",
    "chars": 1481,
    "preview": "#!/usr/bin/env node\n\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any per"
  },
  {
    "path": "src/cli/index.ts",
    "chars": 2597,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/config.ts",
    "chars": 547,
    "preview": "export interface Configuration {\n  // Defaults to the current working directory.\n  public?: string;\n  cleanUrls?: boolea"
  },
  {
    "path": "src/index.ts",
    "chars": 1341,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/loaders/config-file.js",
    "chars": 2449,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/env.ts",
    "chars": 2504,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/files.js",
    "chars": 8643,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/headers.js",
    "chars": 2602,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/index.js",
    "chars": 1375,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/missing.js",
    "chars": 2234,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/not-found.js",
    "chars": 1726,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/protect.js",
    "chars": 1476,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/redirects.js",
    "chars": 4398,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/middleware/rewrites.ts",
    "chars": 2284,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/options.ts",
    "chars": 627,
    "preview": "import { HandleFunction } from \"connect\";\nimport { Configuration } from \"./config\";\n\nexport interface MiddlewareOptions "
  },
  {
    "path": "src/providers/fs.ts",
    "chars": 4280,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/providers/memory.js",
    "chars": 1805,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/responder.js",
    "chars": 6344,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/server.js",
    "chars": 1850,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/superstatic.js",
    "chars": 3178,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/utils/i18n.ts",
    "chars": 4178,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/utils/objectutils.ts",
    "chars": 1556,
    "preview": "/**\n * Copyright (c) 2026 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/utils/pathutils.ts",
    "chars": 2783,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/utils/patterns.js",
    "chars": 3993,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "src/utils/promiseback.js",
    "chars": 1664,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "templates/env.js.template",
    "chars": 577,
    "preview": "(function(root, factory) {\n    if (typeof define === 'function' && define.amd) {\n        // AMD. Register as an anonymou"
  },
  {
    "path": "templates/not_found.html",
    "chars": 1002,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    This is the Superstatic default 404 page.\n    If you're seeing it, that means y"
  },
  {
    "path": "test/fixtures/a/index.html",
    "chars": 2,
    "preview": "A\n"
  },
  {
    "path": "test/fixtures/b/b.html",
    "chars": 2,
    "preview": "B\n"
  },
  {
    "path": "test/fixtures/b/index.html",
    "chars": 2,
    "preview": "B\n"
  },
  {
    "path": "test/helpers.js",
    "chars": 253,
    "preview": "module.exports = {\n  decorator: function (middleware) {\n    return function (config, spec) {\n      return function (req,"
  },
  {
    "path": "test/integration/clean-urls.spec.ts",
    "chars": 3333,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/integration/error-pages.spec.ts",
    "chars": 2990,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/integration/i18n.spec.ts",
    "chars": 3265,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/integration/serving-files.spec.ts",
    "chars": 10872,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/loaders/config-file.spec.js",
    "chars": 3311,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/env.spec.js",
    "chars": 2196,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/files.spec.ts",
    "chars": 15668,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/headers.spec.js",
    "chars": 3565,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/missing.spec.ts",
    "chars": 3650,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/not-found.spec.js",
    "chars": 2552,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/redirects.spec.js",
    "chars": 13894,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/middleware/rewrites.spec.ts",
    "chars": 6188,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/providers/fs.spec.ts",
    "chars": 3542,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/providers/memory.spec.js",
    "chars": 2615,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/responder.spec.js",
    "chars": 7190,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/server.spec.js",
    "chars": 4453,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/superstatic.spec.js",
    "chars": 1210,
    "preview": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy "
  },
  {
    "path": "test/unit/utils/i18n.spec.ts",
    "chars": 1421,
    "preview": "import { expect } from \"chai\";\nimport { i18nContentOptions } from \"../../../src/utils/i18n\";\n\ndescribe(\"i18nContentOptio"
  },
  {
    "path": "test/unit/utils/pathutils.spec.ts",
    "chars": 2198,
    "preview": "import { expect } from \"chai\";\n\nimport * as pathutils from \"../../../src/utils/pathutils\";\n\ndescribe(\"pathutils\", () => "
  },
  {
    "path": "test/unit/utils/promiseback.spec.js",
    "chars": 1088,
    "preview": "const { expect, use } = require(\"chai\");\nuse(require(\"chai-as-promised\"));\n\nconst promiseback = require(\"../../../src/ut"
  },
  {
    "path": "tsconfig.base.json",
    "chars": 223,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"declaration\": true,\n    \"module\": \"commonjs\",\n    \"moduleResolution\":"
  },
  {
    "path": "tsconfig.json",
    "chars": 113,
    "preview": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"include\": [\n    \"examples/**/*\",\n    \"src/**/*\",\n    \"test/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.publish.json",
    "chars": 126,
    "preview": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"sourceMap\": false\n  },\n  \"include\": [\n    \"src/**/*\"\n"
  }
]

About this extraction

This page contains the full source code of the divshot/superstatic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (191.7 KB), approximately 48.0k tokens, and a symbol index with 49 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!