[
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/node-test.yml",
    "content": "name: CI Tests\n\non:\n  - pull_request\n  - push\n\njobs:\n  check-license:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-go@v6\n        with:\n          go-version: \"^1.18.0\"\n      - run: go install github.com/google/addlicense@latest\n      - run: env bash -c 'addlicense --check -c \"Google LLC\" -l MIT src/**/* bin/* examples/**/*'\n\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version:\n          - \"20\"\n          - \"22\"\n          - \"24\"\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: npm\n          cache-dependency-path: package-lock.json\n\n      - run: npm i --global npm@^11.7.0\n      - run: npm ci\n      - run: npm test\n      - run: npm outdated\n        continue-on-error: true\n\n  check-package-lock:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version: \"24\"\n          cache: npm\n          cache-dependency-path: package-lock.json\n\n      - run: npm i --global npm@^11.7.0\n      - run: npm install --package-lock-only --ignore-scripts\n      - 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)\"\n"
  },
  {
    "path": ".gitignore",
    "content": "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.tmp\n.tmp_root\n\nspikes\n\ntest/__testing\n\n*.0\nasdf.txt\ntodo.txt\n\n# Errors from testing\nfirebase.json\nsuperstatic.json\n\nlib\n"
  },
  {
    "path": ".mocharc.yaml",
    "content": "require:\n  - ts-node/register\n  - source-map-support/register\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Want to contribute? Great! First, read this page (including the small print at the end).\n\n### Before you contribute\n\nBefore we can use your code, you must sign the\n[Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual)\n(CLA), which you can do online. The CLA is necessary mainly because you own the\ncopyright to your changes, even after your contribution becomes part of our\ncodebase, so we need your permission to use and distribute your code. We also\nneed to be sure of various other things—for instance that you'll tell us if you\nknow that your code infringes on other people's patents. You don't have to sign\nthe CLA until after you've submitted your code for review and a member has\napproved it, but you must do it before we can put your code into our codebase.\nBefore you start working on a larger contribution, you should get in touch with\nus first through the issue tracker with your idea so that we can help out and\npossibly guide you. Coordinating up front makes it much easier to avoid\nfrustration later on.\n\n### Code reviews\n\nAll submissions, including submissions by project members, require review. We\nuse Github pull requests for this purpose.\n\n### The small print\n\nContributions made by corporations are covered by a different agreement than\nthe one above, the\n[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:0.10\nADD ./package.json /superstatic/package.json\nWORKDIR /superstatic\nRUN npm install\nADD . /superstatic\n\nVOLUME /data\nWORKDIR /data\n\nEXPOSE 80\nENTRYPOINT [\"/superstatic/bin/server\",\"-p\",\"80\",\"-o\",\"0.0.0.0\"]"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2022 Google LLC\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 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)\n\nSuperstatic is an enhanced static web server that was built to power.\nIt has fantastic support for HTML5 pushState applications, clean URLs,\ncaching, and many other goodies.\n\n## Documentation\n\n* [Installation](#installation)\n* [Usage](#usage)\n* [Configuration](#configuration)\n* [API](#api)\n  * [Middleware](#middleware)\n  * [Server](#server)\n* [Providers](#providers)\n  * [Authoring Providers](#authoring-providers)\n* [Run Tests](#run-tests)\n* [Changelog](https://github.com/firebase/superstatic/blob/master/CHANGELOG.md)\n* [Contributing](#contributing)\n\n## Installation\n\nSuperstatic should be installed globally using npm:\n\nFor use via CLI\n\n```\n$ npm install -g superstatic\n```\n\nFor use via API\n\n```\nnpm install superstatic --save\n```\n\n## Usage\n\nBy default, Superstatic will simply serve the current directory on port\n`3474`. This works just like any other static server:\n\n```\n$ superstatic\n```\n\nYou can optionally specify the directory, port and hostname of the server:\n\n```\n$ superstatic public --port 8080 --host 127.0.0.1\n```\n\n## Configuration\n\nSuperstatic reads special configuration from a JSON file (either `superstatic.json`\nor `firebase.json` by default, configurable with `-c`). This JSON file enables\nenhanced static server functionality beyond simply serving files.\n\n**public:** by default, Superstatic will serve the current working directory (or the\nancestor of the current directory that contains the configuration json being used).\nThis configuration key specifies a directory *relative to the configuration file* that\nshould be served. For example, if serving a Jekyll app, this might be set to `\"_site\"`.\nA directory passed as an argument into the command line app supercedes this configuration\ndirective.\n\n**cleanUrls:** if `true`, all `.html` files will automatically have their extensions\ndropped. If `.html` is used at the end of a filename, it will perform a 301 redirect\nto the same path with `.html` dropped.\n\nAll paths have clean urls\n\n```json\n{\n  \"cleanUrls\": true\n}\n```\n\n**rewrites:** you can specify custom route recognition for your application by supplying\nan object to the routes key. Use a single star `*` to replace one URL segment or a\ndouble star to replace an arbitrary piece of URLs. This works great for single page\napps. An example:\n\n```json\n{\n  \"rewrites\": [\n    {\"source\":\"app/**\",\"destination\":\"/application.html\"},\n    {\"source\":\"projects/*/edit\",\"destination\":\"/projects.html\"}\n  ]\n}\n```\n\n**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.\n\n```json\n{\n  \"redirects\": [\n    {\"source\":\"/some/old/path\", \"destination\":\"/some/new/path\"},\n    {\"source\":\"/firebase/*\", \"destination\":\"https://www.firebase.com\", \"type\": 302}\n  ]\n}\n```\n\nRoute segments are also supported in the `redirects` configuration. Segmented `redirects` also support custom status codes (see above):\n\n```json\n{\n  \"redirects\": [\n    {\"source\":\"/old/:segment/path\", \"destination\":\"/new/path/:segment\"}\n  ]\n}\n```\n\nIn this example, `/old/custom-segment/path` redirects to `/new/path/custom-segment`\n\n**headers:** Superstatic allows you to set the response headers for certain paths as well:\n\n```json\n{\n  \"headers\": [\n    {\n      \"source\" : \"**/*.@(eot|otf|ttf|ttc|woff|font.css)\",\n      \"headers\" : [{\n        \"key\" : \"Access-Control-Allow-Origin\",\n        \"value\" : \"*\"\n      }]\n    }, {\n      \"source\" : \"**/*.@(jpg|jpeg|gif|png)\",\n      \"headers\" : [{\n        \"key\" : \"Cache-Control\",\n        \"value\" : \"max-age=7200\"\n      }]\n    }, {\n      \"source\" : \"404.html\",\n      \"headers\" : [{\n        \"key\" : \"Cache-Control\",\n        \"value\" : \"max-age=300\"\n      }]\n    }]\n  }\n}\n```\n\n**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.\n\n```json\n{\n  \"trailingSlash\": true\n}\n```\n\n**i18n:** Internationalized content can be served based on `accept-language` or `x-country-code` headers.\n\nImagine a setup with the following files:\n\n```\n- public/\n  - index.html\n  - i18n/\n    - fr_ca/\n      - index.html\n    - ALL_ca/\n      - index.html\n    - fr/\n      - index.html\n```\n\nWith `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.\n\nIf `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`.\n\nFor 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).\n\n```json\n{\n  \"i18n\": {\n    \"root\": \"/intl\"\n  }\n}\n```\n\n## API\n\nSuperstatic 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.\n\n\n### Middleware\n\n```js\nvar superstatic = require('superstatic')\nvar connect = require('connect');\n\nvar app = connect()\n\t.use(superstatic(/* options */));\n\napp.listen(3000, function() {\n\n});\n\n```\n\n### `superstatic([options])`\n\nInstantiates middleware. See an [example](https://github.com/firebase/superstatic/tree/master/examples) for detail on real world use.\n\n* `options` - Optional configuration:\n  * `fallthrough` - When `false`, render a 404 page from within Superstatic rather than calling through to the next middleware. Defaults to `true`.\n  * `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`.\n  * `protect` - Adds HTTP basic auth. Example:  `username:password`\n  * `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).\n  * `cwd` - The current working directory to set as the root. Your application's `public` configuration option will be used relative to this.\n  * `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.\n\n### Server\n\n```js\nvar superstatic = require('superstatic').server;\n\nvar app = superstatic(/* options */);\n\nvar server = app.listen(function() {\n\n});\n```\n\nSince 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.\n\n### `superstatic([options])`\n\nInstantiates a Connect server, setting up Superstatic middleware, port, host, debugging, compression, etc.\n\n* `options` - Optional configuration. Uses the same options as the middleware, plus a few more options:\n  * `port` - The port of the server. Defaults to `3474`.\n  * `host` or `hostname` - The hostname of the server. Defaults to `localhost`.\n  * `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).\n  * `debug` - A boolean value that tells Superstatic to show or hide network logging in the console. Defaults to `false`.\n  * `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`.\n  * `gzip` **[DEPRECATED]** - A boolean value which is now equivalent in behavior to `compression`. Defaults to `false`.\n\n## Providers\n\nSuperstatic reads content from **providers**. The default provider for Superstatic\nreads from the local filesystem. Other providers can be substituted when initializing\nSuperstatic:\n\n```js\nsuperstatic({\n  provider: require('superstatic-someprovider')({\n    provider: 'options'\n  })\n});\n```\n\n### Authoring Providers\n\nImplementing a new provider is quite simple. You simply need to create a function\nthat takes a request and pathname and returns a Promise. The Promise should:\n\n1. Resolve `null` when content isn't found (i.e. a 404 response).\n2. Resolve with a metadata object as described below when content is found.\n3. Reject when an error occurs in the content-fetching process.\n\nThe metadata object returned by a provider needs the following properties:\n\n* **stream:** A readable stream for the content.\n* **size:** The length of the content.\n* **etag:** (optional) a content-unique string such as an MD5 hash computed from the content\n* **modified:** (optional) a Date object for when the content was last modified\n\nA simple in-memory store provider can be found at `lib/providers/memory.js` in\nthis repo as a simple reference example of a provider.\n\n**Note:** The pathname will be URL-encoded. You should make sure your provider\nproperly handles files with non-standard characters (spaces, unicode, etc).\n\n## Run Tests\n\nIn superstatic module directory:\n\n```\nnpm install\nnpm test\n```\n\n## Contributing\n\nWe 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.\n"
  },
  {
    "path": "changelog.txt",
    "content": "\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import eslint from \"@eslint/js\";\nimport eslintConfigGoogle from \"eslint-config-google\";\nimport eslintConfigPrettier from \"eslint-config-prettier\";\nimport eslintConfigJSDoc from \"eslint-plugin-jsdoc\";\nimport eslintPluginPrettierRecommended from \"eslint-plugin-prettier/recommended\";\nimport tseslint from \"typescript-eslint\";\nimport globals from \"globals\";\n\nexport default [\n  eslint.configs.recommended,\n  ...tseslint.configs.recommendedTypeChecked,\n  ...tseslint.configs.stylisticTypeChecked,\n  eslintConfigJSDoc.configs[\"flat/recommended\"],\n  eslintConfigGoogle,\n  eslintConfigPrettier,\n  eslintPluginPrettierRecommended,\n  {\n    ignores: [\"eslint.config.mjs\", \"lib/**/*\", \"coverage/**/*\"],\n  },\n  {\n    rules: {\n      \"jsdoc/newline-after-description\": \"off\",\n      \"jsdoc/require-jsdoc\": [\"warn\", { publicOnly: true }],\n      \"jsdoc/require-param-type\": \"off\",\n      \"jsdoc/require-returns-type\": \"off\",\n\n      \"@typescript-eslint/no-require-imports\": \"warn\", // TODO(bkendall): remove allow to error.\n\n      \"no-unused-vars\": \"off\", // Turned off in favor of @typescript-eslint/no-unused-vars.\n      \"require-atomic-updates\": \"off\", // This rule is so noisy and isn't useful: https://github.com/eslint/eslint/issues/11899\n      \"require-jsdoc\": \"off\", // This rule is deprecated and superseded by jsdoc/require-jsdoc.\n      \"valid-jsdoc\": \"off\", // This is deprecated but included in recommended configs.\n    },\n    languageOptions: {\n      ecmaVersion: 2020,\n      globals: {\n        ...globals.node,\n        ...globals.mocha,\n      },\n      parserOptions: {\n        ecmaVersion: \"es2020\",\n        projectService: {\n          project: \"./tsconfig.json\",\n          allowDefaultProject: [\"eslint.config.js\"],\n        },\n        tsconfigRootDir: import.meta.dirname,\n      },\n    },\n  },\n  {\n    files: [\"**/*.ts\"],\n    rules: {\n      \"@typescript-eslint/no-explicit-any\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-argument\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-assignment\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-call\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-member-access\": \"warn\", // TODO(bkendall): remove allow to error.\n    },\n  },\n  {\n    files: [\"**/*.js\"],\n    rules: {\n      \"@typescript-eslint/no-this-alias\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-argument\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-assignment\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-call\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-member-access\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-unsafe-return\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/no-var-requires\": \"warn\", // TODO(bkendall): remove allow to error.\n      \"@typescript-eslint/unbound-method\": \"warn\", // TODO(bkendall): remove allow to error.\n    },\n  },\n];\n"
  },
  {
    "path": "examples/middleware/app/index.html",
    "content": "<!--\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n -->\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>Superstatic Middleware</title>\n\n</head>\n<body>\n\n  Hello using Superstatic middleware.\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/middleware/index.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst { default: superstatic } = require(\"../../\");\nconst connect = require(\"connect\");\n\nconst spec = {\n  config: {\n    public: \"./app\",\n    rewrites: [\n      {\n        source: \"**\",\n        destination: \"/index.html\",\n      },\n    ],\n  },\n  cwd: process.cwd(),\n  compression: true,\n};\n\nconst app = connect().use(superstatic(spec));\n\napp.listen(3474, (err) => {\n  if (err) {\n    console.log(err);\n  }\n  console.log(\"Superstatic now serving on port 3474 ...\");\n});\n"
  },
  {
    "path": "examples/server/app/index.html",
    "content": "<!--\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n -->\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>Superstatic Server</title>\n\n</head>\n<body>\n\n  Hello using Superstatic server.\n\n</body>\n</html>\n"
  },
  {
    "path": "examples/server/error.html",
    "content": "<!--\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n -->\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>404 Not Found</title>\n\n</head>\n<body>\n  Nothing here. Move on.\n</body>\n</html>\n"
  },
  {
    "path": "examples/server/index.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst { server: superstaticServer } = require(\"../../\");\n\nconst spec = {\n  port: 3474,\n  config: {\n    public: \"./app\",\n  },\n  cwd: __dirname,\n  errorPage: __dirname + \"/error.html\",\n  compression: true,\n  debug: true,\n};\n\nconst app = superstaticServer(spec);\napp.listen((err) => {\n  if (err) {\n    console.log(err);\n  }\n  console.log(\"Superstatic now serving on port 3474 ...\");\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"superstatic\",\n  \"version\": \"10.0.0\",\n  \"description\": \"A static file server for fancy apps\",\n  \"main\": \"./lib/index.js\",\n  \"types\": \"./lib/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"build:watch\": \"tsc --watch\",\n    \"clean\": \"rimraf lib\",\n    \"prepare\": \"npm run clean && npm run build -- --build tsconfig.publish.json\",\n    \"format\": \"npm run lint -- --fix --quiet\",\n    \"test\": \"npm run lint && npm run coverage\",\n    \"test-unit\": \"mocha \\\"test/unit/**\\\"\",\n    \"test-integration\": \"mocha \\\"test/integration/**\\\"\",\n    \"lint\": \"eslint\",\n    \"coverage\": \"nyc mocha \\\"test/unit/**\\\" \\\"test/integration/**\\\"\",\n    \"watch\": \"mocha -w \\\"test/unit/**\\\" \\\"test/integration/**\\\"\"\n  },\n  \"author\": \"Firebase (https://www.firebase.com/)\",\n  \"license\": \"MIT\",\n  \"bin\": {\n    \"superstatic\": \"./lib/bin/server.js\"\n  },\n  \"files\": [\n    \"bin\",\n    \"lib\",\n    \"templates\"\n  ],\n  \"engines\": {\n    \"node\": \"20 || 22 || 24\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/firebase/superstatic.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/firebase/superstatic/issues\"\n  },\n  \"keywords\": [\n    \"static\",\n    \"server\",\n    \"firebase\",\n    \"hosting\",\n    \"pushstate\",\n    \"html5\",\n    \"router\",\n    \"file\",\n    \"directory\",\n    \"hash\",\n    \"hashbang\"\n  ],\n  \"dependencies\": {\n    \"basic-auth-connect\": \"^1.1.0\",\n    \"commander\": \"^10.0.0\",\n    \"compression\": \"^1.7.0\",\n    \"connect\": \"^3.7.0\",\n    \"destroy\": \"^1.0.4\",\n    \"glob-slasher\": \"^1.0.1\",\n    \"is-url\": \"^1.2.2\",\n    \"join-path\": \"^1.1.1\",\n    \"mime-types\": \"^2.1.35\",\n    \"minimatch\": \"^6.1.6\",\n    \"morgan\": \"^1.8.2\",\n    \"on-finished\": \"^2.2.0\",\n    \"on-headers\": \"^1.0.0\",\n    \"path-to-regexp\": \"^1.9.0\",\n    \"router\": \"^2.0.0\",\n    \"update-notifier-cjs\": \"^5.1.6\"\n  },\n  \"optionalDependencies\": {\n    \"re2\": \"^1.17.7\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.35.0\",\n    \"@types/chai\": \"^4.3.3\",\n    \"@types/chai-as-promised\": \"^7.1.5\",\n    \"@types/connect\": \"^3.4.35\",\n    \"@types/mime-types\": \"^2.1.1\",\n    \"@types/mocha\": \"^10.0.0\",\n    \"@types/node\": \"^24.3.1\",\n    \"@types/supertest\": \"^6.0.2\",\n    \"chai\": \"^4.4.1\",\n    \"chai-as-promised\": \"^7.1.2\",\n    \"concat-stream\": \"^2.0.0\",\n    \"eslint\": \"^9.35.0\",\n    \"eslint-config-google\": \"^0.14.0\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-jsdoc\": \"^60.6.0\",\n    \"eslint-plugin-prettier\": \"^5.5.4\",\n    \"globals\": \"^16.3.0\",\n    \"mocha\": \"^11.7.2\",\n    \"nyc\": \"^17.0.0\",\n    \"prettier\": \"^3.1.1\",\n    \"rimraf\": \"^6.0.1\",\n    \"sinon\": \"^17.0.1\",\n    \"sinon-chai\": \"^3.7.0\",\n    \"source-map-support\": \"^0.5.21\",\n    \"std-mocks\": \"^2.0.0\",\n    \"supertest\": \"^7.0.0\",\n    \"ts-node\": \"^10.9.1\",\n    \"typescript\": \"^5.9.2\",\n    \"typescript-eslint\": \"^8.43.0\"\n  }\n}\n"
  },
  {
    "path": "src/activator.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst middleware = require(\"./middleware\");\nconst promiseback = require(\"./utils/promiseback\");\n\nconst Activator = function (spec, provider) {\n  this.spec = spec;\n  this.provider = provider;\n  this.stack = this.buildStack();\n\n  if (typeof spec.config === \"function\") {\n    this.awaitConfig = spec.config;\n  } else {\n    this.awaitConfig = function () {\n      return Promise.resolve(spec.config);\n    };\n  }\n};\n\nActivator.prototype.buildStack = function () {\n  const self = this;\n\n  const stack = this.spec.stack.slice(0);\n  Object.entries(this.spec.before ?? {}).forEach(([name, wares]) => {\n    stack.splice(...[stack.indexOf(name), 0].concat(wares));\n  });\n\n  Object.entries(this.spec.after ?? {}).forEach(([name, wares]) => {\n    stack.splice(...[stack.indexOf(name) + 1, 0].concat(wares));\n  });\n\n  return stack.map((ware) => {\n    return typeof ware === \"function\" ? ware : middleware[ware](self.spec);\n  });\n};\n\nActivator.prototype.build = function () {\n  const self = this;\n\n  return function (req, res, next) {\n    promiseback(self.awaitConfig, 2)(req, res).then((config) => {\n      req.superstatic = config ?? {};\n\n      const stack = self.stack.slice(0).reverse();\n      const _run = function () {\n        if (!stack.length) {\n          return next();\n        }\n        const fn = stack.pop();\n        return fn(req, res, _run);\n      };\n\n      _run();\n    }, next);\n  };\n};\n\nmodule.exports = function (spec, provider) {\n  return new Activator(spec, provider).build();\n};\n"
  },
  {
    "path": "src/bin/server.ts",
    "content": "#!/usr/bin/env node\n\n/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst updateNotifier = require(\"update-notifier-cjs\");\n\nimport cli from \"../cli\";\nconst pkg = require(\"../../package.json\");\n\nconst updateCheckInterval = 1000 * 60 * 60 * 24 * 7; // 1 week\n\nconst notifier = updateNotifier({\n  pkg,\n  updateCheckInterval: updateCheckInterval,\n  shouldNotifyInNpmScript: true,\n});\n\nnotifier.notify();\n\ncli.parse();\n"
  },
  {
    "path": "src/cli/index.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport { Command } from \"commander\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nconst pkg = require(\"../../package.json\");\nimport server = require(\"../server\");\n\nconst PORT = \"3474\";\nconst HOSTNAME = \"localhost\";\nconst CONFIG_FILENAME = [\"superstatic.json\", \"firebase.json\"];\nconst ENV_FILENAME = \".env.json\";\n\nlet env: Record<string, string> | undefined = undefined;\ntry {\n  env = JSON.parse(fs.readFileSync(path.resolve(ENV_FILENAME), \"utf8\"));\n} catch {\n  // do nothing\n}\n\nconst cli = new Command();\n\ncli.name(\"superstatic\").version(pkg.version, \"-v, --version\");\n\ncli\n  .command(\"serve\", { isDefault: true })\n  .argument(\"[folder]\")\n  .option(\"-p, --port <port>\", \"Port\", PORT)\n  .option(\"--host, --hostname <hostname>\", \"Hostname\", HOSTNAME)\n  .option(\"-c, --config <config>\", \"Filename of config\", CONFIG_FILENAME)\n  .option(\"--debug\")\n  .option(\"--gzip\")\n  .option(\"--compression\")\n  .description(\"start server\")\n  .action((folder, options) => {\n    return new Promise((resolve) => {\n      const app = server({\n        cwd: path.join(process.cwd(), folder),\n        config: options.config,\n        port: options.port,\n        hostname: options.hostname,\n        compression: options.compression,\n        debug: options.debug,\n        env: env,\n      });\n      app.listen(() => resolve());\n      console.log(\n        `Superstatic started.\\nVisit http://${options.hostname}:${options.port} to view your app.`,\n      );\n    });\n  });\n\nexport default cli;\n"
  },
  {
    "path": "src/config.ts",
    "content": "export interface Configuration {\n  // Defaults to the current working directory.\n  public?: string;\n  cleanUrls?: boolean | string[];\n  rewrites?: Rewrite[];\n  redirects?: Redirect[];\n  headers?: Header[];\n  trailingSlash?: boolean;\n  i18n?: { root: string };\n  errorPage?: string;\n}\n\nexport interface Rewrite {\n  source: string;\n  destination: string;\n}\n\nexport interface Redirect {\n  source: string;\n  destination: string;\n  type?: number;\n}\n\nexport interface Header {\n  source: string;\n  headers: {\n    key: string;\n    value: string;\n  }[];\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport superstatic = require(\"./superstatic\");\nimport server = require(\"./server\");\nimport patterns = require(\"./utils/patterns\");\nconst RE2mode = patterns.re2Available;\n\nexport default superstatic;\nexport { server, RE2mode };\n"
  },
  {
    "path": "src/loaders/config-file.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst fs = require(\"fs\");\n\nconst join = require(\"join-path\");\nconst path = require(\"path\");\nconst { isPlainObject } = require(\"../utils/objectutils\");\n\nconst CONFIG_FILE = [\"superstatic.json\", \"firebase.json\"];\n\nmodule.exports = function (filename) {\n  if (typeof filename === \"function\") {\n    return filename;\n  }\n\n  filename = filename ?? CONFIG_FILE;\n\n  let configObject = {};\n  let config = {};\n\n  // From custom config data passed in\n  try {\n    configObject = JSON.parse(filename);\n  } catch {\n    if (isPlainObject(filename)) {\n      configObject = filename;\n      filename = CONFIG_FILE;\n    }\n  }\n\n  if (Array.isArray(filename)) {\n    filename = filename.find((name) => {\n      return fs.existsSync(join(process.cwd(), name));\n    });\n  }\n\n  // Set back to default config file if stringified object is\n  // given as config. With this, we override values in the config file\n  if (isPlainObject(filename)) {\n    filename = CONFIG_FILE;\n  }\n\n  // A file name or array of file names\n  if (typeof filename === \"string\" && filename.endsWith(\"json\")) {\n    try {\n      config = JSON.parse(fs.readFileSync(path.resolve(filename)));\n      config = config.hosting ?? config;\n    } catch {\n      // do nothing\n    }\n  }\n\n  // Passing an object as the config value merges\n  // the config data\n  return { ...config, ...configObject };\n};\n"
  },
  {
    "path": "src/middleware/env.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"fs\";\nimport * as mime from \"mime-types\";\n\nconst template = fs\n  .readFileSync(`${__dirname}/../../templates/env.js.template`)\n  .toString();\n\ninterface SuperstaticRequest {\n  superstatic: {\n    env: Record<string, string>;\n  };\n}\n\ninterface SuperstaticResponse {\n  superstatic: {\n    handleData: (d: Record<string, string>) => void;\n  };\n}\n\n/**\n * Returns middleware for `/__/env.json|js`.\n * @param spec superstatic options.\n * @param spec.env environment variables.\n * @returns middleware.\n */\nexport function env(spec: { env: Record<string, string> }) {\n  return (\n    req: Request & SuperstaticRequest,\n    res: Response & SuperstaticResponse,\n    next: () => void,\n  ): void => {\n    // const config = req.superstatic.env;\n    let env = undefined;\n    if (spec.env || req.superstatic.env) {\n      env = Object.assign({}, req.superstatic.env, spec.env);\n    } else {\n      return next();\n    }\n\n    if (req.url === \"/__/env.json\") {\n      res.superstatic.handleData({\n        data: JSON.stringify(env, null, 2),\n        contentType: mime.contentType(\"json\") || \"\",\n      });\n    } else if (req.url === \"/__/env.js\") {\n      const payload = template.replace(\"{{ENV}}\", JSON.stringify(env));\n      res.superstatic.handleData({\n        data: payload,\n        contentType: mime.contentType(\"js\") || \"\",\n      });\n    }\n\n    return next();\n  };\n}\n\nmodule.exports = env;\n"
  },
  {
    "path": "src/middleware/files.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst { i18nContentOptions } = require(\"../utils/i18n\");\nconst pathutils = require(\"../utils/pathutils\");\nconst url = require(\"url\");\n\n/**\n * We cannot redirect to \"\", redirect to \"/\" instead.\n * @param {string} path path\n * @returns {string} noramlized path\n */\nfunction normalizeRedirectPath(path) {\n  return path || \"/\";\n}\n\nmodule.exports = function () {\n  return function (req, res, next) {\n    const config = req.superstatic;\n    const trailingSlashBehavior = config.trailingSlash;\n\n    const parsedUrl = url.parse(req.url);\n    const pathname = pathutils.normalizeMultiSlashes(parsedUrl.pathname);\n    const search = parsedUrl.search ?? \"\";\n\n    const cleanUrlRules = !!req?.superstatic?.cleanUrls;\n\n    // Exact file always wins.\n    return providerResult(req, res, pathname)\n      .then((result) => {\n        if (result) {\n          // If we are using cleanURLs, we'll trim off any `.html` (or `/index.html`), if it exists.\n          if (cleanUrlRules) {\n            if (pathname.endsWith(\".html\")) {\n              let redirPath = pathutils.removeTrailingString(pathname, \".html\");\n              if (redirPath.endsWith(\"/index\")) {\n                redirPath = pathutils.removeTrailingString(redirPath, \"/index\");\n              }\n              if (trailingSlashBehavior === true) {\n                redirPath = pathutils.addTrailingSlash(redirPath);\n              }\n              return res.superstatic.handle({\n                redirect: normalizeRedirectPath(redirPath + search),\n              });\n            }\n          }\n          return res.superstatic.handleFileStream({ file: pathname }, result);\n        }\n\n        // Now, let's consider the trailing slash.\n        const hasTrailingSlash = pathutils.hasTrailingSlash(pathname);\n\n        // We want to check for some other files, namely an `index.html` if this were a directory.\n        const pathAsDirectoryWithIndex = pathutils.asDirectoryIndex(\n          pathutils.addTrailingSlash(pathname),\n        );\n        return providerResult(req, res, pathAsDirectoryWithIndex).then(\n          (pathAsDirectoryWithIndexResult) => {\n            // If an exact file wins now, we know that this path leads us to a directory.\n            if (pathAsDirectoryWithIndexResult) {\n              if (\n                trailingSlashBehavior === undefined &&\n                !hasTrailingSlash &&\n                !cleanUrlRules\n              ) {\n                return res.superstatic.handle({\n                  redirect: pathutils.addTrailingSlash(pathname) + search,\n                });\n              }\n              if (\n                trailingSlashBehavior === false &&\n                hasTrailingSlash &&\n                pathname !== \"/\"\n              ) {\n                // No infinite redirects\n                return res.superstatic.handle({\n                  redirect: normalizeRedirectPath(\n                    pathutils.removeTrailingSlash(pathname) + search,\n                  ),\n                });\n              }\n              if (trailingSlashBehavior === true && !hasTrailingSlash) {\n                return res.superstatic.handle({\n                  redirect: pathutils.addTrailingSlash(pathname) + search,\n                });\n              }\n              // If we haven't returned yet, our path is \"correct\" and we should be serving a file, not redirecting.\n              return res.superstatic.handleFileStream(\n                { file: pathAsDirectoryWithIndex },\n                pathAsDirectoryWithIndexResult,\n              );\n            }\n\n            // Let's check on the clean URLs property.\n            // We want to know if a specific mutation of the path exists.\n            if (cleanUrlRules) {\n              let appendedPath = pathname;\n              if (hasTrailingSlash) {\n                if (trailingSlashBehavior !== undefined) {\n                  // We want to remove the trailing slash and see if a file exists with an .html attached.\n                  appendedPath =\n                    pathutils.removeTrailingString(pathname, \"/\") + \".html\";\n                }\n              } else {\n                // Let's see if our path is a simple clean URL missing a .HTML5\n                appendedPath += \".html\";\n              }\n\n              return providerResult(req, res, appendedPath).then(\n                (appendedPathResult) => {\n                  if (appendedPathResult) {\n                    // Okay, back to trailing slash behavior\n                    if (trailingSlashBehavior === false && hasTrailingSlash) {\n                      // If we had a slash to begin with, and we could be serving a file without it, we'll remove the slash.\n                      // (This works because we are in the cleanURL block.)\n                      return res.superstatic.handle({\n                        redirect: normalizeRedirectPath(\n                          pathutils.removeTrailingSlash(pathname) + search,\n                        ),\n                      });\n                    }\n                    if (trailingSlashBehavior === true && !hasTrailingSlash) {\n                      // If we are missing a slash and need to add it, we want to make sure our appended path is cleaned up.\n                      appendedPath = pathutils.removeTrailingString(\n                        appendedPath,\n                        \".html\",\n                      );\n                      appendedPath = pathutils.removeTrailingString(\n                        appendedPath,\n                        \"/index\",\n                      );\n                      return res.superstatic.handle({\n                        redirect:\n                          pathutils.addTrailingSlash(appendedPath) + search,\n                      });\n                    }\n                    // If we've gotten this far and still have `/index.html` on the end, we want to remove it from the URL.\n                    if (appendedPath.endsWith(\"/index.html\")) {\n                      return res.superstatic.handle({\n                        redirect: normalizeRedirectPath(\n                          pathutils.removeTrailingString(\n                            appendedPath,\n                            \"/index.html\",\n                          ) + search,\n                        ),\n                      });\n                    }\n                    // And if we should be serving a file and we're at the right path, we'll serve the file.\n                    return res.superstatic.handleFileStream(\n                      { file: appendedPath },\n                      appendedPathResult,\n                    );\n                  }\n\n                  return next();\n                },\n              );\n            }\n\n            return next();\n          },\n        );\n      })\n      .catch((err) => {\n        res.superstatic.handleError(err);\n      });\n  };\n};\n\n/**\n * Uses the provider to look for a file given a path.\n * This also takes into account i18n settings.\n * @param {*} req the Request.\n * @param {*} res the Response.\n * @param {string} p the path to search for.\n * @returns {Promise<*>} a non-null value if a file is found.\n */\nfunction providerResult(req, res, p) {\n  const promises = [];\n\n  const i18n = req.superstatic.i18n;\n  if (i18n?.root) {\n    const paths = i18nContentOptions(p, req);\n    for (const pth of paths) {\n      promises.push(res.superstatic.provider(req, pth));\n    }\n  }\n  promises.push(res.superstatic.provider(req, p));\n\n  return Promise.all(promises).then((results) => {\n    for (const r of results) {\n      if (r) {\n        return r;\n      }\n    }\n  });\n}\n"
  },
  {
    "path": "src/middleware/headers.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst slasher = require(\"glob-slasher\");\nconst urlParser = require(\"url\");\nconst onHeaders = require(\"on-headers\");\nconst patterns = require(\"../utils/patterns\");\n\nconst normalizedConfigHeaders = function (spec, config) {\n  const out = config ?? [];\n  if (Array.isArray(config)) {\n    const _isAllowed = function (headerToSet) {\n      return spec.allowedHeaders.includes(headerToSet.key.toLowerCase());\n    };\n\n    for (const c of config) {\n      c.source = slasher(c.source);\n      c.headers = c.headers ?? [];\n      if (spec.allowedHeaders) {\n        c.headers = c.headers.filter(_isAllowed);\n      }\n    }\n  }\n\n  return out;\n};\n\nconst matcher = function (configHeaders) {\n  return function (url) {\n    return configHeaders\n      .filter((configHeader) => {\n        return patterns.configMatcher(url, configHeader);\n      })\n      .reduce((out, val) => {\n        val.headers.forEach((headerToSet) => {\n          out.push(headerToSet);\n        });\n        return out;\n      }, []);\n  };\n};\n\nmodule.exports = function (spec) {\n  return function (req, res, next) {\n    const config = req?.superstatic?.headers;\n    if (!config) {\n      return next();\n    }\n\n    const headers = matcher(normalizedConfigHeaders(spec, config));\n    const pathname = urlParser.parse(req.url).pathname;\n    const matches = headers(slasher(pathname));\n\n    onHeaders(res, () => {\n      matches.forEach((header) => {\n        res.setHeader(header.key, header.value);\n      });\n    });\n\n    return next();\n  };\n};\n"
  },
  {
    "path": "src/middleware/index.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n[\n  \"protect\",\n  \"redirects\",\n  \"headers\",\n  \"env\",\n  \"files\",\n  \"rewrites\",\n  \"missing\",\n].forEach((name) => {\n  exports[name] = function (spec, config) {\n    const mware = require(\"./\" + name)(spec, config);\n    mware._name = name;\n    return mware;\n  };\n});\n"
  },
  {
    "path": "src/middleware/missing.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst fs = require(\"fs\");\n\nconst setHeaders = require(\"./headers\");\nconst { i18nContentOptions } = require(\"../utils/i18n\");\n\nmodule.exports = function (spec) {\n  let defaultErrorContent = undefined;\n  if (spec.errorPage) {\n    defaultErrorContent = fs.readFileSync(spec.errorPage, \"utf8\");\n  }\n\n  return function (req, res, next) {\n    const config = req.superstatic;\n    const errorPage = config.errorPage ?? \"/404.html\";\n\n    setHeaders(spec)(\n      {\n        superstatic: config,\n        url: errorPage,\n      },\n      res,\n      () => {\n        const handles = [];\n        const i18n = req.superstatic.i18n;\n        // To handle i18n, we will try to resolve i18n paths first.\n        if (i18n?.root) {\n          const paths = i18nContentOptions(errorPage, req);\n          for (const pth of paths) {\n            handles.push({ file: pth, status: 404 });\n          }\n        }\n        handles.push({ file: errorPage, status: 404 });\n        if (defaultErrorContent) {\n          handles.push({ data: defaultErrorContent, status: 404 });\n        }\n        res.superstatic.handle(handles, next);\n      },\n    );\n  };\n};\n"
  },
  {
    "path": "src/middleware/not-found.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst fs = require(\"fs\");\n\nconst DEFAULT_ERROR_PAGE = __dirname + \"/../../templates/not_found.html\";\n\nmodule.exports = function (spec) {\n  const content = fs.readFileSync(spec.errorPage ?? DEFAULT_ERROR_PAGE);\n\n  return function (req, res) {\n    // NOTE: provider isn't used to serve the pages\n    // because this middleware should only serve local\n    // static pages around the same directory as the\n    // superstatic middleware\n\n    res.statusCode = 404;\n    res.setHeader(\"Content-Type\", \"text/html; charset=utf-8\");\n    res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n    res.end(content);\n  };\n};\n"
  },
  {
    "path": "src/middleware/protect.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst basicAuth = require(\"basic-auth-connect\");\n\nmodule.exports = function (spec) {\n  return function (req, res, next) {\n    const config = req.superstatic;\n\n    if (spec.protect || config.protect) {\n      return basicAuth.apply(\n        basicAuth,\n        (spec.protect ?? config.protect).split(\":\"),\n      )(req, res, next);\n    }\n\n    return next();\n  };\n};\n"
  },
  {
    "path": "src/middleware/redirects.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst isUrl = require(\"is-url\");\n\nconst patterns = require(\"../utils/patterns\");\nconst pathToRegexp = require(\"path-to-regexp\");\nconst slasher = require(\"glob-slasher\");\n\nfunction formatExternalUrl(u) {\n  const cleaned = u\n    .replace(\"/http:/\", \"http://\")\n    .replace(\"/https:/\", \"https://\");\n\n  return isUrl(cleaned) ? cleaned : u;\n}\n\nfunction addQuery(url, qs) {\n  if (url.indexOf(\"?\") >= 0) {\n    return url + \"&\" + qs;\n  } else if (qs?.length) {\n    return url + \"?\" + qs;\n  }\n  return url;\n}\n\nconst Redirect = function (glob, regex, destination, type) {\n  this.type = type ?? 301;\n  this.glob = slasher(glob);\n  this.regex = regex;\n  this.destination = destination;\n\n  if (this.destination.match(/(?:^|\\/):/)) {\n    this.captureKeys = [];\n    if (this.glob) {\n      this.engine = \"glob\";\n      this.capture = pathToRegexp(this.glob, this.captureKeys);\n    }\n    if (this.regex) {\n      this.engine = \"pattern\";\n      this.capture = patterns.createRaw(this.regex);\n    }\n    this.compileDestination = pathToRegexp.compile(this.destination);\n  }\n};\n\nRedirect.prototype.test = function (url) {\n  let qs = \"\";\n  if (url.indexOf(\"?\") >= 0) {\n    const parts = url.split(\"?\");\n    url = parts[0];\n    qs = parts[1];\n  }\n\n  let match = undefined;\n  if (this.capture) {\n    match = this.capture.exec(url);\n  }\n  if (match) {\n    let params = {};\n    if (this.engine === \"glob\") {\n      for (let i = 0; i < this.captureKeys.length; i++) {\n        let m = match[i + 1];\n        if (m?.includes(\"/\")) {\n          m = m.split(\"/\");\n        }\n\n        params[this.captureKeys[i].name] = m;\n      }\n    } else {\n      for (let j = 0; j < match.length; j++) {\n        params[j.toString()] = match[j];\n      }\n      if (match.groups) {\n        params = Object.assign(params, match.groups);\n      }\n    }\n\n    try {\n      const dest = decodeURIComponent(this.compileDestination(params));\n      return {\n        type: this.type,\n        destination: encodeURI(addQuery(dest, qs)),\n      };\n    } catch {\n      return undefined;\n    }\n  } else if (\n    patterns.configMatcher(url, { glob: this.glob, regex: this.regex })\n  ) {\n    return {\n      type: this.type,\n      destination: encodeURI(addQuery(this.destination, qs)),\n    };\n  }\n  return undefined;\n};\n\nmodule.exports = function () {\n  return function (req, res, next) {\n    const config = req?.superstatic?.redirects;\n    if (!config) {\n      return next();\n    }\n\n    const redirects = [];\n    if (Array.isArray(config)) {\n      config.forEach((redir) => {\n        const glob = redir.glob ?? redir.source;\n        redirects.push(\n          new Redirect(glob, redir.regex, redir.destination, redir.type),\n        );\n      });\n    } else {\n      throw new Error(\"redirects provided in an unrecognized format\");\n    }\n\n    const matcher = function (url) {\n      for (const redirect of redirects) {\n        const result = redirect.test(url);\n        if (result) {\n          return result;\n        }\n      }\n      return undefined;\n    };\n\n    const match = matcher(decodeURI(req.url));\n\n    if (!match) {\n      return next();\n    }\n\n    // Remove leading slash of a url\n    const redirectUrl = formatExternalUrl(match.destination);\n\n    return res.superstatic.handle({\n      redirect: redirectUrl,\n      status: match.type,\n    });\n  };\n};\n"
  },
  {
    "path": "src/middleware/rewrites.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst slasher = require(\"glob-slasher\");\nimport * as urlParser from \"url\";\nimport { NextFunction } from \"connect\";\nimport { IncomingMessage, ServerResponse } from \"http\";\n\nimport { Configuration, Rewrite } from \"../config\";\nimport Responder = require(\"../responder\");\nimport * as patterns from \"../utils/patterns\";\n\nfunction matcher(rewrites: Rewrite[]) {\n  return function (url: string) {\n    for (const rw of rewrites) {\n      if (patterns.configMatcher(url, rw)) {\n        return rw;\n      }\n    }\n    return;\n  };\n}\n\n/**\n * Looks for possible rewrites for the given req.url.\n * @returns middleware for handling rewrites.\n */\nmodule.exports = function () {\n  return function (\n    req: IncomingMessage & { superstatic: Configuration },\n    res: ServerResponse & { superstatic: Responder },\n    next: NextFunction,\n  ) {\n    const rewrites = matcher(req.superstatic.rewrites ?? []);\n    if (!req.url) {\n      return next();\n    }\n    const pathname = urlParser.parse(req.url).pathname;\n    const match = rewrites(slasher(pathname));\n\n    if (!match) {\n      return next();\n    }\n\n    res.statusCode = 200;\n    res.superstatic.handle({ rewrite: match }, next);\n  };\n};\n"
  },
  {
    "path": "src/options.ts",
    "content": "import { HandleFunction } from \"connect\";\nimport { Configuration } from \"./config\";\n\nexport interface MiddlewareOptions {\n  fallthrough?: boolean;\n  config?: string | Configuration;\n  protect?: string;\n  env?: string | Record<string, string>;\n  cwd?: string;\n  compression?: boolean;\n  stack?: string | string[];\n  after?: Record<string, HandleFunction>;\n  before?: Record<string, HandleFunction>;\n  rewriters?: Record<string, unknown>;\n  errorPage?: string;\n}\n\nexport interface ServerOptions extends MiddlewareOptions {\n  port?: number;\n  hostname?: string;\n  errorPage?: string;\n  debug?: boolean;\n  compression?: boolean;\n}\n"
  },
  {
    "path": "src/providers/fs.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as crypto from \"node:crypto\";\nimport { stat as fsStat } from \"node:fs/promises\";\nimport * as fs from \"node:fs\";\nconst pathjoin = require(\"join-path\");\n\nasync function multiStat(\n  paths: string[],\n): Promise<fs.Stats & { pathname: string }> {\n  // const pathname = paths.shift();\n  let err: any;\n  for (const pathname of paths) {\n    try {\n      const stat = await fsStat(pathname);\n      return { pathname, ...stat };\n    } catch (e: unknown) {\n      err = e;\n    }\n  }\n  throw err;\n}\n\nmodule.exports = function provider(options: any) {\n  const etagCache: Record<string, { timestamp: Date; value: string }> = {};\n  const cwd = options.cwd ?? process.cwd();\n  let publicPaths: string[] = options.public ?? [\".\"];\n  if (!Array.isArray(publicPaths)) {\n    publicPaths = [publicPaths];\n  }\n\n  async function fetchEtag(pathname: string, stat: fs.Stats): Promise<string> {\n    return new Promise((resolve, reject) => {\n      const cached = etagCache[pathname];\n      // Chaining doesn't work. If `stat.mtime` is undefined, the chained\n      // version is true and breaks the logic.\n      // eslint-disable-next-line @typescript-eslint/prefer-optional-chain\n      if (cached && cached.timestamp === stat.mtime) {\n        return resolve(cached.value);\n      }\n\n      // the file you want to get the hash\n      const fd = fs.createReadStream(pathname);\n      const hash = crypto.createHash(\"md5\");\n      hash.setEncoding(\"hex\");\n\n      fd.on(\"error\", reject);\n\n      fd.on(\"end\", () => {\n        hash.end();\n        const etag = hash.read();\n        etagCache[pathname] = {\n          timestamp: stat.mtime,\n          value: etag,\n        };\n        resolve(etag);\n      });\n\n      // read all file and pipe it (write it) to the hash object\n      return fd.pipe(hash);\n    });\n  }\n\n  return async function (\n    req: unknown,\n    pathname: string,\n  ): Promise<{\n    modified: number;\n    size: number;\n    etag: string;\n    stream: fs.ReadStream;\n  } | null> {\n    pathname = decodeURI(pathname);\n    // jumping to parent directories is not allowed\n    if (\n      pathname.includes(\"../\") ||\n      pathname.includes(\"..\\\\\") ||\n      pathname.toLowerCase().includes(\"..%5c\") ||\n      pathname.match(/\\0/g) !== null ||\n      // A path that didn't start with a slash is not valid.\n      !pathname.startsWith(\"/\")\n    ) {\n      return Promise.resolve(null);\n    }\n\n    const fullPathnames: string[] = publicPaths.map((p) =>\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n      pathjoin(cwd, p, pathname),\n    );\n\n    try {\n      const stat = await multiStat(fullPathnames);\n      return {\n        // stat.mtime is gone in node.js v22, so transition over to mtimeMs.\n        modified: stat.mtime?.getTime() ?? stat.mtimeMs,\n        size: stat.size,\n        etag: await fetchEtag(stat.pathname, stat),\n        stream: fs.createReadStream(stat.pathname),\n      };\n    } catch (err: any) {\n      if ([\"ENOENT\", \"ENOTDIR\", \"EISDIR\", \"EINVAL\"].includes(err.code)) {\n        return null;\n      }\n      if (err instanceof Error) {\n        return Promise.reject(err);\n      }\n      return Promise.reject(new Error(`Unknown error: ${err}`));\n    }\n  };\n};\n"
  },
  {
    "path": "src/providers/memory.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst crypto = require(\"crypto\");\nconst Readable = require(\"stream\").Readable;\n\nmodule.exports = function (options) {\n  const fn = function (req, pathname) {\n    pathname = decodeURI(pathname);\n\n    if (!options.store[pathname]) {\n      return Promise.resolve(null);\n    }\n\n    const content = options.store[pathname];\n\n    const stream = new Readable();\n    stream.push(content);\n    stream.push(null);\n\n    const hash = crypto.createHash(\"md5\");\n    hash.update(content);\n\n    return Promise.resolve({\n      modified: options.modified ?? null,\n      stream: stream,\n      size: content.length,\n      etag: hash.digest(\"hex\"),\n    });\n  };\n  fn.store = options.store ?? {};\n  return fn;\n};\n"
  },
  {
    "path": "src/responder.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst mime = require(\"mime-types\");\nconst { isPlainObject } = require(\"./utils/objectutils\");\nconst path = require(\"path\");\nconst onFinished = require(\"on-finished\");\nconst destroy = require(\"destroy\");\n\nconst awaitFinished = (res) => {\n  return new Promise((resolve) => {\n    onFinished(res, resolve);\n  });\n};\n\n/** @type Class */\nconst Responder = function (req, res, options) {\n  this.req = req;\n  this.res = res;\n  this.provider = options.provider;\n  this.config = options.config ?? {};\n  this.rewriters = options.rewriters ?? {};\n  this.compressor = options.compressor;\n};\n\nResponder.prototype.isNotModified = function (stats) {\n  if (stats.etag && stats.etag === this.req.headers[\"if-none-match\"]) {\n    return true;\n  }\n\n  let reqModified = this.req.headers[\"if-modified-since\"];\n  if (reqModified) {\n    reqModified = new Date(reqModified).getTime();\n  }\n  if (stats.modified && reqModified && stats.modified < reqModified) {\n    return true;\n  }\n\n  return false;\n};\n\nResponder.prototype.handle = function (item, next) {\n  const self = this;\n  return this._handle(item)\n    .then((responded) => {\n      if (!responded && next) {\n        next();\n      }\n      return responded;\n    })\n    .catch((err) => {\n      return self.handleError(err);\n    });\n};\n\nResponder.prototype._handle = function (item) {\n  if (Array.isArray(item)) {\n    return this.handleStack(item);\n  } else if (typeof item === \"string\") {\n    return this.handleFile({ file: item });\n  } else if (isPlainObject(item)) {\n    if (item.file) {\n      return this.handleFile(item);\n    } else if (item.redirect) {\n      return this.handleRedirect(item);\n    } else if (item.rewrite) {\n      return this.handleRewrite(item);\n    } else if (item.data) {\n      return this.handleData(item);\n    }\n  } else if (typeof item === \"function\") {\n    return this.handleMiddleware(item);\n  }\n\n  return Promise.reject(\n    new Error(\n      JSON.stringify(item) + \" is not a recognized responder directive\",\n    ),\n  );\n};\n\nResponder.prototype.handleError = function (err) {\n  this.res.statusCode = 500;\n  console.log(err.stack);\n  this.res.end(\"Unexpected error occurred.\");\n};\n\nResponder.prototype.handleStack = function (stack) {\n  const self = this;\n  if (stack.length) {\n    return this._handle(stack.shift()).then((responded) => {\n      return responded ? true : self.handleStack(stack);\n    });\n  }\n\n  return Promise.resolve(false);\n};\n\nResponder.prototype.handleFile = function (file) {\n  const self = this;\n  return this.provider(this.req, file.file).then((result) => {\n    if (!result) {\n      return false;\n    }\n\n    if (self.isNotModified(result)) {\n      return self.handleNotModified(result);\n    }\n\n    return self.handleFileStream(file, result);\n  });\n};\n\nResponder.prototype.handleFileStream = function (file, result) {\n  const self = this;\n\n  this.streamedFile = file;\n  this.res.statusCode = file.status ?? 200;\n  if (this.res.statusCode === 200 && file.file === this.config.errorPage) {\n    this.res.statusCode = 404;\n  }\n  this.res.setHeader(\n    \"Content-Type\",\n    result.contentType ?? mime.contentType(path.extname(file.file)),\n  );\n  if (result.size) {\n    this.res.setHeader(\"Content-Length\", result.size);\n  }\n  if (result.etag) {\n    this.res.setHeader(\"ETag\", result.etag);\n  }\n  if (result.modified) {\n    this.res.setHeader(\n      \"Last-Modified\",\n      new Date(result.modified).toUTCString(),\n    );\n  }\n\n  if (this.compressor) {\n    this.compressor(this.req, this.res, () => {\n      result.stream.pipe(self.res);\n    });\n  } else {\n    result.stream.pipe(self.res);\n  }\n\n  return awaitFinished(this.res).then(() => {\n    destroy(result.stream);\n    return true;\n  });\n};\n\nResponder.prototype.handleNotModified = function () {\n  this.res.statusCode = 304;\n  this.res.removeHeader(\"Content-Type\");\n  this.res.removeHeader(\"Content-Length\");\n  this.res.removeHeader(\"Transfer-Encoding\");\n  this.res.end();\n  return true;\n};\n\nResponder.prototype.handleRedirect = function (redirect) {\n  this.res.statusCode = redirect.status ?? 301;\n  this.res.setHeader(\"Location\", redirect.redirect);\n  this.res.setHeader(\"Content-Type\", \"text/html; charset=utf-8\");\n  this.res.end(\"Redirecting to \" + redirect.redirect);\n  return Promise.resolve(true);\n};\n\nResponder.prototype.handleMiddleware = function (middleware) {\n  const self = this;\n  return new Promise((resolve) => {\n    middleware(self.req, self.res, () => {\n      resolve(false);\n    });\n  });\n};\n\nResponder.prototype.handleRewrite = function (item) {\n  const self = this;\n  if (item.rewrite.destination) {\n    return self.handleFile({ file: item.rewrite.destination });\n  }\n\n  for (const key in this.rewriters) {\n    if (item.rewrite[key]) {\n      return this.rewriters[key](item.rewrite, this).then((result) => {\n        return self._handle(result);\n      });\n    }\n  }\n  return Promise.reject(\n    new Error(\n      \"Unable to find a matching rewriter for \" + JSON.stringify(item.rewrite),\n    ),\n  );\n};\n\nResponder.prototype.handleData = function (data) {\n  this.res.statusCode = data.status ?? 200;\n  this.res.setHeader(\n    \"Content-Type\",\n    data.contentType ?? \"text/html; charset=utf-8\",\n  );\n  this.res.end(data.data);\n  return Promise.resolve(true);\n};\n\nmodule.exports = Responder;\n"
  },
  {
    "path": "src/server.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst connect = require(\"connect\");\nconst networkLogger = require(\"morgan\");\n\nconst superstatic = require(\"./superstatic\");\n\n/**\n * @param {ServerOptions} spec superstatic options.\n * @returns unknown\n */\nmodule.exports = function (spec) {\n  spec.fallthrough ??= false;\n\n  const app = connect();\n  const listen = app.listen.bind(app);\n\n  // Override method because port and host are given\n  // in the spec object\n  app.listen = function (done) {\n    let server = {};\n\n    app.use(superstatic(spec));\n\n    // Start server\n    server = listen(spec.port, spec.hostname ?? spec.host, done);\n\n    return server;\n  };\n\n  // Console output for network requests\n  if (spec.debug) {\n    app.use(networkLogger(\"combined\"));\n  }\n\n  return app;\n};\n"
  },
  {
    "path": "src/superstatic.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst makerouter = require(\"router\");\n\nconst fsProvider = require(\"./providers/fs\");\nconst Responder = require(\"./responder\");\nconst activator = require(\"./activator\");\nconst notFound = require(\"./middleware/missing\");\n\nconst promiseback = require(\"./utils/promiseback\");\nconst loadConfigFile = require(\"./loaders/config-file\");\nconst defaultCompressor = require(\"compression\")();\n\nconst CWD = process.cwd();\n\n/**\n * Superstatic returns a router that can be used in a server.\n * @param {MiddlewareOptions} spec superstatic options.\n * @returns {HandleFunction} router handler.\n */\nconst superstatic = function (spec = {}) {\n  spec.stack ??= \"default\";\n\n  spec.fallthrough ??= true;\n\n  if (typeof spec.stack === \"string\" && superstatic.stacks[spec.stack]) {\n    spec.stack = superstatic.stacks[spec.stack];\n  }\n\n  const router = makerouter();\n  const cwd = spec.cwd ?? CWD;\n\n  // Load data\n  /** @type {Configuration} */\n  const config = (spec.config = loadConfigFile(spec.config));\n  config.errorPage = config.errorPage ?? \"/404.html\";\n\n  // Set up provider\n  const provider = spec.provider\n    ? promiseback(spec.provider, 2)\n    : fsProvider({ cwd, ...config });\n\n  // Select compression middleware\n  let compressor;\n  if (typeof spec.compression === \"function\") {\n    compressor = spec.compression;\n  } else if (spec.compression ?? spec.gzip) {\n    compressor = defaultCompressor;\n  } else {\n    compressor = null;\n  }\n\n  // Setup helpers\n  router.use((req, res, next) => {\n    res.superstatic = new Responder(req, res, {\n      provider: provider,\n      config: config,\n      compressor: compressor,\n      rewriters: spec.rewriters,\n    });\n\n    next();\n  });\n\n  router.use(activator(spec, provider));\n\n  // Handle not found pages\n  if (!spec.fallthrough) {\n    router.use(notFound(spec));\n  }\n\n  return router;\n};\n\nsuperstatic.stacks = {\n  default: [\n    \"protect\",\n    \"redirects\",\n    \"headers\",\n    \"env\",\n    \"files\",\n    \"rewrites\",\n    \"missing\",\n  ],\n  strict: [\"redirects\", \"headers\", \"files\", \"rewrites\", \"missing\"],\n};\n\nmodule.exports = superstatic;\n"
  },
  {
    "path": "src/utils/i18n.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport { normalizeMultiSlashes } from \"./pathutils\";\n\n/**\n * Returns the list of paths to check for i18n content.\n * The path order is:\n * (1) root/language_country/path (for each language)\n * (2) root/ALL_country/path (if country is set)\n * (3) root/language_ALL/path or root/language/path (for each language)\n *\n * If i18n is *not* configured, an empty list is returned.\n * @param p requested path.\n * @param req the request object containing superstatic and i18n configuration.\n * @returns list of paths to check for i18n content.\n */\nexport function i18nContentOptions(p: string, req: any): string[] {\n  const paths: string[] = [];\n  const i18n = req.superstatic.i18n;\n  if (!i18n?.root) {\n    return paths;\n  }\n  const country = getCountryCode(req.headers);\n  const languages = getI18nLanguages(req.headers);\n  // (1)\n  if (country) {\n    for (const l of languages) {\n      paths.push(join(i18n.root, `${l}_${country}`, p));\n    }\n    // (2)\n    paths.push(join(i18n.root, `ALL_${country}`, p));\n  }\n  // (3)\n  for (const l of languages) {\n    paths.push(join(i18n.root, `${l}_ALL`, p));\n    paths.push(join(i18n.root, `${l}`, p));\n  }\n  return paths;\n}\n\nfunction join(...arr: string[]): string {\n  arr.unshift(\"/\");\n  return normalizeMultiSlashes(arr.join(\"/\"));\n}\n\n/**\n * Fetches the country code from the headers object.\n * @param headers headers from the request.\n * @returns country code, or an empty string.\n */\nfunction getCountryCode(headers: Record<string, string>): string {\n  const overrideValue = cookieValue(\n    headers.cookie,\n    \"firebase-country-override\",\n  );\n  if (overrideValue) {\n    return overrideValue;\n  }\n  return headers[\"x-country-code\"] || \"\";\n}\n\n/**\n * Fetches the languages from the accept-language header.\n * @param headers headers from the request.\n * @returns ordered list of languages from the header.\n */\nfunction getI18nLanguages(headers: Record<string, string>): string[] {\n  const overrideValue = cookieValue(\n    headers.cookie,\n    \"firebase-language-override\",\n  );\n  if (overrideValue) {\n    return overrideValue.includes(\",\")\n      ? overrideValue.split(\",\")\n      : [overrideValue];\n  }\n\n  const acceptLanguage = headers[\"accept-language\"];\n  if (!acceptLanguage) {\n    return [];\n  }\n\n  const languagesSeen = new Set<string>();\n  const languagesOrdered = [];\n  for (const v of acceptLanguage.split(\",\")) {\n    const l = v.split(\"-\")[0];\n    if (!l) {\n      continue;\n    }\n    if (!languagesSeen.has(l)) {\n      languagesOrdered.push(l);\n    }\n    languagesSeen.add(l);\n  }\n  return languagesOrdered;\n}\n\n/**\n * Fetches a value from a cookie string.\n * @param cookieString full cookie string.\n * @param key key to look for.\n * @returns the value, or empty string;\n */\nfunction cookieValue(cookieString: string, key: string): string {\n  if (!cookieString) {\n    return \"\";\n  }\n  const cookies = cookieString.split(\";\").map((c) => c.trim());\n  for (const cookie of cookies) {\n    if (cookie.startsWith(key)) {\n      const s = cookie.split(\"=\", 2);\n      return s.length === 2 ? s[1] : \"\";\n    }\n  }\n  return \"\";\n}\n"
  },
  {
    "path": "src/utils/objectutils.ts",
    "content": "/**\n * Copyright (c) 2026 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n/**\n * Returns true for plain objects (`{}` or `Object.create(null)`).\n * @param val value to check\n * @returns value indicating whether the value is a plain object\n */\nexport function isPlainObject(val: unknown): val is Record<string, unknown> {\n  return (\n    val !== null &&\n    typeof val === \"object\" &&\n    !Array.isArray(val) &&\n    (Object.getPrototypeOf(val) === Object.prototype ||\n      Object.getPrototypeOf(val) === null)\n  );\n}\n"
  },
  {
    "path": "src/utils/pathutils.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst INDEX_FILE = \"index.html\";\n\n/**\n * @param pathname path to check.\n * @returns the path with \"/index.html\" appended, if required.\n */\nexport function asDirectoryIndex(pathname: string): string {\n  if (isDirectoryIndex(pathname)) {\n    return pathname;\n  }\n  return normalizeMultiSlashes(`${pathname}/${INDEX_FILE}`);\n}\n\n/**\n * @param pathname path to check.\n * @returns true if pathname ends with \"/index.html\".\n */\nexport function isDirectoryIndex(pathname: string): boolean {\n  return pathname.endsWith(`/${INDEX_FILE}`);\n}\n\n/**\n * @param pathname path to check.\n * @returns true if it ends with a slash.\n */\nexport function hasTrailingSlash(pathname: string): boolean {\n  return pathname.endsWith(\"/\");\n}\n\n/**\n * @param pathname path to check.\n * @returns pathname with a trailing slash.\n */\nexport function addTrailingSlash(pathname: string): string {\n  return hasTrailingSlash(pathname) ? pathname : pathname + \"/\";\n}\n\n/**\n * @param pathname path to check.\n * @returns pathname without a trailing slash.\n */\nexport function removeTrailingSlash(pathname: string): string {\n  return removeTrailingString(pathname, \"/\");\n}\n\n/**\n * @param pathname path to check.\n * @returns pathname with any \"//\" replaced with \"/\".\n */\nexport function normalizeMultiSlashes(pathname: string): string {\n  return pathname.replace(/\\/+/g, \"/\");\n}\n\n/**\n * @param string string to check.\n * @param rm string to search for.\n * @returns string with rm removed if it's the end of string. Else, string.\n */\nexport function removeTrailingString(string: string, rm: string): string {\n  if (!string.endsWith(rm)) {\n    return string;\n  }\n  return string.slice(0, string.lastIndexOf(rm));\n}\n"
  },
  {
    "path": "src/utils/patterns.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nlet RE2;\nconst minimatch = require(\"minimatch\");\ntry {\n  RE2 = require(\"re2\");\n} catch {\n  RE2 = null;\n}\n\n/**\n * Evaluates whether a configured redirect/rewrite/custom header should\n * be applied to a request against a specific path. All three features\n * are configured with a hash that contains either a Node-like glob path\n * specification as its `source` or `glob` field, or a RE2 regular\n * expression as its `regex` field.\n *\n * Since Javascript lacks a native library for RE2, Superstatic uses the C\n * bindings as an optional dependency, and falls over to PCRE if the import\n * is unavailable. Under most circumstances not involving named capturing\n * groups, the two libraries should have identical behavior.\n *\n * No special consideration is taken if the configuration hash contains both\n * a glob and a regex. normalizeConfig() will error in that case.\n * @param {string} path The URL path from the request.\n * @param {object} config A dictionary from a sanitized JSON configuration.\n * @returns {boolean} Whether the config should be applied to the request.\n */\nfunction configMatcher(path, config) {\n  const glob = config.glob ?? config.source;\n  const regex = config.regex;\n  if (glob) {\n    return minimatch(path, glob);\n  }\n  if (regex) {\n    const pattern = RE2 ? new RE2(regex, \"u\") : new RegExp(regex, \"u\");\n    return path.match(pattern) !== null;\n  }\n  return false;\n}\n\n/**\n * Creates either an RE2 or a Javascript RegExp from a provided string\n * pattern, depending on whether or not the RE2 library is available as an\n * import.\n * @param {string} pattern A regular expression pattern to test against.\n * @returns {RegExp} A regular expression object, created by either base\n *                  RegExp or RE2, which matches the RegExp prototype\n */\nfunction createRaw(pattern) {\n  return RE2 ? new RE2(pattern, \"u\") : new RegExp(pattern, \"u\");\n}\n\n/**\n * Returns true if RE2, which is an optional dependency, has been loaded.\n * @returns {boolean}\n */\nfunction re2Available() {\n  return RE2 ? true : false;\n}\n\n/**\n * Is truthy if the provided raw string pattern contains a RE2 named capture\n * group opening, ?P<, which is not interpretable when Superstatic is falling\n * back on the base Javascript RegExp implementation.\n * @param {string} pattern\n * @returns {boolean}\n */\nfunction containsRE2Capture(pattern) {\n  return pattern?.includes(\"?P<\");\n}\n\n/**\n * Is truthy if the provided raw string pattern contains a PCRE named capture\n * group opening, ?<, which is not interpretable when Superstatic has loaded\n * the RE2 bindings.\n * @param {string} pattern\n * @returns {boolean}\n */\nfunction containsPCRECapture(pattern) {\n  return pattern?.includes(\"?<\");\n}\n\nmodule.exports = {\n  configMatcher: configMatcher,\n  createRaw: createRaw,\n  re2Available: re2Available,\n  containsRE2Capture: containsRE2Capture,\n  containsPCRECapture: containsPCRECapture,\n};\n"
  },
  {
    "path": "src/utils/promiseback.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nmodule.exports = function promiseback(userFn, argCount) {\n  // if no callback argument is provided, assume the promise form\n  if (userFn.length <= argCount) {\n    return userFn;\n  }\n  // otherwise promise-ify the callback\n  return (...args) => {\n    return new Promise((resolve, reject) => {\n      userFn(...args, (err, res) => {\n        if (err) {\n          if (err instanceof Error) {\n            return reject(err);\n          }\n          return reject(new Error(`Unknown error: ${err}`));\n        }\n        resolve(res);\n      });\n    });\n  };\n};\n"
  },
  {
    "path": "templates/env.js.template",
    "content": "(function(root, factory) {\n    if (typeof define === 'function' && define.amd) {\n        // AMD. Register as an anonymous module.\n        define([''], function() {\n            return (root.__env = factory());\n        });\n    } else if (typeof exports === 'object') {\n        // Node. Does not work with strict CommonJS, but\n        // only CommonJS-like enviroments that support module.exports,\n        // like Node.\n        module.exports = factory();\n    } else {\n        // Browser globals\n        root.__env = factory();\n    }\n}(this, function() {\n    return {{ENV}};\n}));\n"
  },
  {
    "path": "templates/not_found.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <!--\n    This is the Superstatic default 404 page.\n    If you're seeing it, that means you haven't configured a custom error page.\n    See https://github.com/firebase/superstatic for more info.\n  -->\n  <meta charset=utf-8 />\n  <title>Page Not Found</title>\n  <style>\n    body {\n      font-family: Roboto, Arial, sans-serif;\n      background: white;\n    }\n\n    #message {\n      margin: 100px auto;\n      text-align: center;\n      padding: 20px;\n    }\n\n    #message h1 {\n      font-size: 60px;\n      font-weight: normal;\n      letter-spacing: -0.03em;\n      margin: 0 0 10px;\n    }\n\n    #message p {\n      margin: 10px 0 0;\n      color: #aaa;\n    }\n\n    #footer {\n      text-align: center;\n      margin-top: 100px;\n      font-size: 12px;\n      color: #aaa;\n    }\n    #footer a {\n      color: #aaa;\n    }\n  </style>\n</head>\n<body>\n  <div id=\"message\">\n    <h1>Page Not Found</h1>\n    <p>Sorry, we were unable to find anything at this location.</p>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "test/fixtures/a/index.html",
    "content": "A\n"
  },
  {
    "path": "test/fixtures/b/b.html",
    "content": "B\n"
  },
  {
    "path": "test/fixtures/b/index.html",
    "content": "B\n"
  },
  {
    "path": "test/helpers.js",
    "content": "module.exports = {\n  decorator: function (middleware) {\n    return function (config, spec) {\n      return function (req, res, next) {\n        req.superstatic = config ?? {};\n        return middleware(spec ?? {})(req, res, next);\n      };\n    };\n  },\n};\n"
  },
  {
    "path": "test/integration/clean-urls.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as connect from \"connect\";\nimport * as request from \"supertest\";\n\nimport superstatic from \"../../src/\";\nimport { MiddlewareOptions } from \"../../src/options\";\nimport { Configuration } from \"../../src/config\";\n\n// config will always exist, so let's make typing nicer.\nfunction options(): MiddlewareOptions & { config: Configuration } {\n  return {\n    config: {\n      public: \".tmp\",\n    },\n  };\n}\n\ndescribe(\"clean urls\", () => {\n  before(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n    await fs.mkdir(\".tmp\");\n    await fs.mkdir(\".tmp/dir\");\n    await fs.writeFile(\".tmp/index.html\", \"index\", \"utf8\");\n    await fs.writeFile(\".tmp/test.html\", \"test\", \"utf8\");\n    await fs.writeFile(\".tmp/app.js\", 'console.log(\"js\")', \"utf8\");\n    await fs.writeFile(\".tmp/dir/index.html\", \"dir index\", \"utf8\");\n    await fs.writeFile(\".tmp/dir/sub.html\", \"dir sub\", \"utf8\");\n  });\n\n  after(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"not configured\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app).get(\"/test\").expect(404);\n  });\n\n  it(\"redirects html file\", async () => {\n    const opts = options();\n\n    opts.config.cleanUrls = true;\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/test.html\")\n      .expect(301)\n      .expect(\"Location\", \"/test\");\n  });\n\n  it(\"serves html file\", async () => {\n    const opts = options();\n\n    opts.config.cleanUrls = true;\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app).get(\"/test\").expect(200).expect(\"test\");\n  });\n\n  it(\"redirects using globs\", async () => {\n    const opts = options();\n\n    opts.config.cleanUrls = [\"/*.html\"];\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/test.html\")\n      .expect(301)\n      .expect(\"Location\", \"/test\");\n  });\n\n  it(\"serves html file using globs\", async () => {\n    const opts = options();\n\n    opts.config.cleanUrls = [\"*.html\"];\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app).get(\"/test\").expect(200).expect(\"test\");\n  });\n});\n"
  },
  {
    "path": "test/integration/error-pages.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as connect from \"connect\";\nimport * as request from \"supertest\";\n\nimport superstatic from \"../../src/\";\nimport { MiddlewareOptions } from \"../../src/options\";\nimport { Configuration } from \"../../src/config\";\n\nfunction options(): MiddlewareOptions & { config: Configuration } {\n  return {\n    fallthrough: false,\n    config: {\n      public: \".tmp\",\n    },\n  };\n}\n\ndescribe(\"error page\", () => {\n  beforeEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/default-error.html\", \"default error\", \"utf8\");\n    await fs.writeFile(\".tmp/error.html\", \"config error\", \"utf8\");\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"from 404.html\", async () => {\n    await fs.writeFile(\".tmp/404.html\", \"404.html error\", \"utf8\");\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/does-not-exist\")\n      .expect(404)\n      .expect(\"404.html error\")\n      .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"from custom error page\", async () => {\n    const opts = options();\n    opts.config.errorPage = \"/error.html\";\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/does-not-exist\")\n      .expect(404)\n      .expect(\"config error\")\n      .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"falls back to default when configured error page does not exist\", async () => {\n    const opts = options();\n\n    opts.errorPage = \".tmp/default-error.html\";\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/does-not-exist\")\n      .expect(404)\n      .expect(\"default error\")\n      .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n  });\n});\n"
  },
  {
    "path": "test/integration/i18n.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as connect from \"connect\";\nimport * as request from \"supertest\";\n\nimport superstatic from \"../../src/\";\nimport { MiddlewareOptions } from \"../../src/options\";\nimport { Configuration } from \"../../src/config\";\n\nfunction options(): MiddlewareOptions & { config: Configuration } {\n  return {\n    fallthrough: false,\n    config: {\n      public: \".tmp\",\n      i18n: { root: \"intl\" },\n    },\n  };\n}\n\ndescribe(\"i18n resolution\", () => {\n  before(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/index.html\", \"normal index\", \"utf8\");\n    await fs.writeFile(\".tmp/404.html\", \"normal 404\", \"utf8\");\n    await fs.mkdir(\".tmp/intl\");\n    await fs.mkdir(\".tmp/intl/fr\");\n    await fs.writeFile(\".tmp/intl/fr/index.html\", \"french index\", \"utf8\");\n    await fs.writeFile(\".tmp/intl/fr/404.html\", \"french 404\", \"utf8\");\n  });\n\n  after(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"index.html\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app).get(\"/\").expect(200, \"normal index\");\n  });\n\n  it(\"index.html with language\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"accept-language\", \"fr\")\n      .expect(200, \"french index\");\n  });\n\n  it(\"index.html with unknown language\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"accept-language\", \"de\")\n      .expect(200, \"normal index\");\n  });\n\n  it(\"an unknown file\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app).get(\"/nope\").expect(404, \"normal 404\");\n  });\n\n  it(\"an unknown file with language\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/nope\")\n      .set(\"accept-language\", \"fr\")\n      .expect(404, \"french 404\");\n  });\n});\n"
  },
  {
    "path": "test/integration/serving-files.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"node:fs/promises\";\nimport { join } from \"path\";\nimport * as connect from \"connect\";\nimport * as request from \"supertest\";\n\nimport superstatic from \"../../src/\";\nimport { MiddlewareOptions } from \"../../src/options\";\nimport { Configuration } from \"../../src/config\";\n\nfunction options(): MiddlewareOptions & { config: Configuration } {\n  return {\n    fallthrough: false,\n    config: {\n      public: \".tmp\",\n    },\n  };\n}\n\ndescribe(\"serves\", () => {\n  beforeEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/index.html\", \"index\", \"utf-8\");\n    await fs.writeFile(\".tmp/test.html\", \"test\", \"utf-8\");\n    await fs.writeFile(\".tmp/app.js\", 'console.log(\"js\")', \"utf-8\");\n    await fs.mkdir(\".tmp/dir\");\n    await fs.writeFile(\".tmp/dir/index.html\", \"dir index\", \"utf-8\");\n    await fs.writeFile(\".tmp/dir/sub.html\", \"dir sub\", \"utf-8\");\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"static file\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/test.html\")\n      .expect(200)\n      .expect(\"test\")\n      .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"directory index file\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/dir/\")\n      .expect(200)\n      .expect(\"dir index\")\n      .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"cannot access files above the root\", async () => {\n    const app = connect().use(superstatic(options()));\n\n    await request(app).get(\"/../README.md\").expect(404);\n  });\n\n  it(\"missing directory index\", async () => {\n    const opts = options();\n\n    opts.config.public = \"./\";\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app).get(\"/\").expect(404);\n  });\n\n  it(\"javascript file\", async () => {\n    const opts = options();\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/app.js\")\n      .expect(200)\n      .expect('console.log(\"js\")')\n      .expect(\"Content-Type\", \"application/javascript; charset=utf-8\");\n  });\n\n  it(\"from custom current working directory\", async () => {\n    const opts = options();\n\n    opts.cwd = join(process.cwd(), \".tmp\");\n    opts.config.public = \"./dir\";\n\n    const app = connect().use(superstatic(opts));\n\n    await request(app)\n      .get(\"/index.html\")\n      .expect(200)\n      .expect(\"dir index\")\n      .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n  });\n\n  describe(\"redirects\", () => {\n    const opts = options();\n\n    opts.config.redirects = [\n      { source: \"/from\", destination: \"/to\" },\n      { source: \"/fromCustom\", destination: \"/toCustom\", type: 302 },\n      { source: \"/external\", destination: \"http://redirect.com\" },\n    ];\n\n    const app = connect().use(superstatic(opts));\n\n    it(\"301\", async () => {\n      await request(app).get(\"/from\").expect(301).expect(\"Location\", \"/to\");\n    });\n\n    it(\"custom\", async () => {\n      await request(app)\n        .get(\"/fromCustom\")\n        .expect(302)\n        .expect(\"Location\", \"/toCustom\");\n    });\n\n    it(\"external urls\", async () => {\n      await request(app)\n        .get(\"/external\")\n        .expect(301)\n        .expect(\"Location\", \"http://redirect.com\");\n    });\n  });\n\n  describe(\"trailing slash\", () => {\n    xit(\"removes trailling slash for file\", async () => {\n      const app = connect().use(superstatic(options()));\n\n      await request(app)\n        .get(\"/test.html/\")\n        .expect(301)\n        .expect(\"Location\", \"/test.html\");\n    });\n\n    it(\"add trailing slash with a directory index file\", async () => {\n      const app = connect().use(superstatic(options()));\n\n      await request(app).get(\"/dir\").expect(301).expect(\"Location\", \"/dir/\");\n    });\n  });\n\n  describe(\"basic auth\", () => {\n    it(\"protects\", async () => {\n      const opts = options();\n\n      opts.protect = \"username:passwords\";\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app)\n        .get(\"/\")\n        .expect(401)\n        .expect(\"www-authenticate\", 'Basic realm=\"Authorization Required\"');\n    });\n  });\n\n  describe(\"custom headers\", () => {\n    it(\"with globs\", async () => {\n      const opts = options();\n\n      opts.config.headers = [\n        {\n          source: \"/**/*.html\",\n          headers: [\n            {\n              key: \"x-custom\",\n              value: \"testing\",\n            },\n          ],\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/dir/sub.html\").expect(\"x-custom\", \"testing\");\n    });\n\n    it(\"exact\", async () => {\n      const opts = options();\n\n      opts.config.headers = [\n        {\n          source: \"/app.js\",\n          headers: [\n            {\n              key: \"x-custom\",\n              value: \"testing\",\n            },\n          ],\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/app.js\").expect(\"x-custom\", \"testing\");\n    });\n  });\n\n  xdescribe(\"environment variables\", () => {\n    it(\"json\", async () => {\n      const opts = options();\n\n      opts.env = {\n        key: \"value\",\n      };\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app)\n        .get(\"/__/env.json\")\n        .expect({ key: \"value\" })\n        .expect(\"Content-Type\", \"application/json; charset=utf-8\");\n    });\n\n    it(\"js\", async () => {\n      const opts = options();\n\n      opts.env = {\n        key: \"value\",\n      };\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app)\n        .get(\"/__/env.js\")\n        .expect(200)\n        .expect(\"Content-Type\", \"application/javascript; charset=utf-8\");\n    });\n\n    it(\"defaults to .env.json\", async () => {\n      await fs.writeFile(\".env.json\", '{\"key\":\"value\"}', \"utf8\");\n\n      const app = connect().use(superstatic());\n\n      await request(app).get(\"/__/env.json\").expect({ key: \"value\" });\n\n      await fs.rm(\".env.json\");\n    });\n\n    it(\"serves env file, overriding static routing\", async () => {\n      const opts = options();\n\n      opts.env = {\n        key: \"value\",\n      };\n\n      opts.config.rewrites = [\n        {\n          source: \"**\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app)\n        .get(\"/__/env.json\")\n        .expect({ key: \"value\" })\n        .expect(\"Content-Type\", \"application/json; charset=utf-8\");\n    });\n  });\n\n  describe(\"custom routes\", () => {\n    it(\"serves file\", async () => {\n      const opts = options();\n\n      opts.config.rewrites = [\n        {\n          source: \"/testing\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app)\n        .get(\"/testing\")\n        .expect(200)\n        .expect(\"index\")\n        .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n    });\n\n    it(\"serves file from custom route when clean urls are on and route matches an html as a clean url\", async () => {\n      const opts = options();\n\n      opts.config.cleanUrls = true;\n      opts.config.rewrites = [\n        {\n          source: \"/testing\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app)\n        .get(\"/testing\")\n        .expect(200)\n        .expect(\"index\")\n        .expect(\"Content-Type\", \"text/html; charset=utf-8\");\n    });\n\n    it(\"serves static file when no matching route\", async () => {\n      const opts = options();\n\n      opts.config.rewrites = [\n        {\n          source: \"/testing\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/test.html\").expect(200).expect(\"test\");\n    });\n\n    it(\"serves with negation\", async () => {\n      const opts = options();\n\n      opts.config.rewrites = [\n        {\n          source: \"!/no\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/no\").expect(404);\n    });\n\n    it(\"serves file if url matches exact file path\", async () => {\n      const opts = options();\n\n      opts.config.rewrites = [\n        {\n          source: \"**\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/test.html\").expect(200).expect(\"test\");\n    });\n  });\n\n  describe(\"rewrites\", () => {\n    beforeEach(async () => {\n      await fs.writeFile(\".tmp/404.html\", \"404\", \"utf8\");\n    });\n\n    it(\"rewrites unknown files to /index.html\", async () => {\n      const opts = options();\n      opts.config.rewrites = [\n        {\n          source: \"/**\",\n          destination: \"/index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/index.html\").expect(200).expect(\"index\");\n      await request(app).get(\"/dir/\").expect(200).expect(\"dir index\");\n      await request(app).get(\"/testing/\").expect(200).expect(\"index\");\n    });\n\n    it(\"never is able to rewrite to a relative path\", async () => {\n      const opts = options();\n      opts.config.rewrites = [\n        {\n          source: \"/**\",\n          destination: \"index.html\",\n        },\n      ];\n\n      const app = connect().use(superstatic(opts));\n\n      await request(app).get(\"/index.html\").expect(200).expect(\"index\");\n      await request(app).get(\"/foo\").expect(404).expect(\"404\");\n      await request(app).get(\"/page2/\").expect(404).expect(\"404\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/loaders/config-file.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst fs = require(\"node:fs/promises\");\nconst expect = require(\"chai\").expect;\n\nconst loadConfigFile = require(\"../../../src/loaders/config-file\");\n\ndescribe(\"loading config files\", () => {\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/file.json\", '{\"key\": \"value\"}', \"utf-8\");\n    await fs.writeFile(\n      \".tmp/package.json\",\n      JSON.stringify({\n        superstatic: {\n          key: \"value\",\n        },\n      }),\n    );\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"filename\", (done) => {\n    const data = loadConfigFile(\".tmp/file.json\");\n\n    expect(data).to.eql({\n      key: \"value\",\n    });\n    done();\n  });\n\n  it(\"loads first existing file in array\", (done) => {\n    const data = loadConfigFile([\"another.json\", \".tmp/file.json\"]);\n\n    expect(data).to.eql({\n      key: \"value\",\n    });\n    done();\n  });\n\n  it(\"empty object for when no file\", (done) => {\n    const data = loadConfigFile(\".tmp/nope.json\");\n    expect(data).to.eql({});\n    done();\n  });\n\n  it(\"loads object as config\", (done) => {\n    const config = loadConfigFile({\n      my: \"data\",\n    });\n\n    expect(config).to.eql({\n      my: \"data\",\n    });\n    done();\n  });\n\n  describe(\"extends the file config with the object passed\", () => {\n    it(\"superstatic.json\", async () => {\n      await fs.writeFile(\n        \"superstatic.json\",\n        '{\"firebase\": \"superstatic\", \"public\": \"./\"}',\n        \"utf-8\",\n      );\n\n      const config = loadConfigFile({\n        override: \"test\",\n        public: \"app\",\n      });\n\n      expect(config).to.eql({\n        firebase: \"superstatic\",\n        override: \"test\",\n        public: \"app\",\n      });\n\n      await fs.rm(\"superstatic.json\");\n    });\n\n    it(\"firebase.json\", async () => {\n      await fs.writeFile(\n        \"firebase.json\",\n        '{\"firebase\": \"example\", \"public\": \"./\"}',\n        \"utf-8\",\n      );\n\n      const config = loadConfigFile({\n        override: \"test\",\n        public: \"app\",\n      });\n\n      expect(config).to.eql({\n        firebase: \"example\",\n        override: \"test\",\n        public: \"app\",\n      });\n\n      await fs.rm(\"firebase.json\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/env.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst request = require(\"supertest\");\nconst connect = require(\"connect\");\n\nconst helpers = require(\"../../helpers\");\nconst env = helpers.decorator(require(\"../../../src/middleware/env\"));\nconst Responder = require(\"../../../src/responder\");\n\ndescribe(\"env\", () => {\n  let app;\n\n  beforeEach(() => {\n    app = connect().use((req, res, next) => {\n      res.superstatic = new Responder(req, res, {\n        provider: {},\n      });\n      next();\n    });\n  });\n\n  it(\"serves json\", (done) => {\n    app.use(\n      env({\n        env: {\n          key: \"value\",\n        },\n      }),\n    );\n\n    void request(app)\n      .get(\"/__/env.json\")\n      .expect(200)\n      .expect({\n        key: \"value\",\n      })\n      .expect(\"content-type\", \"application/json; charset=utf-8\")\n      .end(done);\n  });\n\n  it(\"serves javascript\", (done) => {\n    app.use(\n      env({\n        env: {\n          key: \"value\",\n        },\n      }),\n    );\n\n    void request(app)\n      .get(\"/__/env.js\")\n      .expect(200)\n      .expect(\"content-type\", \"application/javascript; charset=utf-8\")\n      .end(done);\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/files.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"node:fs/promises\";\nimport { expect } from \"chai\";\nimport * as request from \"supertest\";\nimport * as connect from \"connect\";\nimport { ServerResponse } from \"node:http\";\n\nimport * as helpers from \"../../helpers\";\nimport * as filesPkg from \"../../../src/middleware/files\";\nconst fsProvider = require(\"../../../src/providers/fs\");\nimport * as Responder from \"../../../src/responder\";\n\nconst files = helpers.decorator(filesPkg);\n\ndescribe(\"i18n\", () => {\n  const provider = fsProvider({ public: \".tmp\" });\n  let app: connect.Server;\n\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\", { recursive: true });\n    await fs.writeFile(\".tmp/foo.html\", \"foo.html content\", \"utf8\");\n    await fs.mkdir(\".tmp/foo\", { recursive: true });\n    await fs.writeFile(\".tmp/foo/index.html\", \"foo/index.html content\", \"utf8\");\n    await fs.writeFile(\".tmp/foo/bar.html\", \"foo/bar.html content\", \"utf8\");\n    await fs.mkdir(\".tmp/intl/es\", { recursive: true });\n    await fs.writeFile(\".tmp/intl/es/index.html\", \"hola\", \"utf8\");\n    await fs.mkdir(\".tmp/intl/fr\", { recursive: true });\n    await fs.writeFile(\".tmp/intl/fr/index.html\", \"French Index!\", \"utf8\");\n    await fs.mkdir(\".tmp/intl/jp_ALL\", { recursive: true });\n    await fs.writeFile(\".tmp/intl/jp_ALL/other.html\", \"Japanese!\", \"utf8\");\n    await fs.mkdir(\".tmp/intl/fr_ca\", { recursive: true });\n    await fs.writeFile(\".tmp/intl/fr_ca/index.html\", \"French CA!\", \"utf8\");\n    await fs.mkdir(\".tmp/intl/ALL_ca\", { recursive: true });\n    await fs.writeFile(\".tmp/intl/ALL_ca/index.html\", \"Oh Canada\", \"utf8\");\n    await fs.writeFile(\".tmp/intl/ALL_ca/hockey.html\", \"Only Canada\", \"utf8\");\n\n    app = connect().use(\n      (req, res: ServerResponse & { superstatic?: Responder }, next) => {\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n        res.superstatic = new Responder(req, res, { provider });\n        next();\n      },\n    );\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"should resolve i18n content by accept-language\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"accept-language\", \"es\")\n      .expect(200, \"hola\");\n  });\n\n  it(\"should resolve default files if nothing is provided\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/foo.html\")\n      .set(\"accept-language\", \"jp\")\n      .expect(200, \"foo.html content\");\n  });\n\n  it(\"should resolve i18n content by x-country-code\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"x-country-code\", \"ca\")\n      .expect(200, \"Oh Canada\");\n  });\n\n  it(\"should not show i18n content for other countries\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/hockey.html\")\n      .set(\"x-country-code\", \"jp\")\n      .expect(404);\n  });\n\n  it(\"should allow i18n content specific to a country\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/hockey.html\")\n      .set(\"x-country-code\", \"ca\")\n      .expect(200, \"Only Canada\");\n  });\n\n  it(\"should resolve i18n content by accept-language and x-country-code\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"accept-language\", \"fr\")\n      .set(\"x-country-code\", \"ca\")\n      .expect(200, \"French CA!\");\n  });\n\n  it(\"should override the content using cookies for location\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"accept-language\", \"es\")\n      .set(\"cookie\", \"firebase-language-override=fr\")\n      .expect(200, \"French Index!\");\n  });\n\n  it(\"should override the content using cookies for location and country\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/\")\n      .set(\"accept-language\", \"en\")\n      .set(\n        \"cookie\",\n        \"firebase-language-override=fr; firebase-country-override=ca\",\n      )\n      .expect(200, \"French CA!\");\n  });\n\n  it(\"should allow i18n resolution by language with _ALL\", async () => {\n    app.use(files({ i18n: { root: \"/intl\" } }, { provider }));\n\n    await request(app)\n      .get(\"/other.html\")\n      .set(\"accept-language\", \"jp\")\n      .expect(200, \"Japanese!\");\n  });\n});\n\ndescribe(\"static server with trailing slash customization\", () => {\n  const provider = fsProvider({\n    public: \".tmp\",\n  });\n  let app: connect.Server;\n\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\", { recursive: true });\n    await fs.writeFile(\".tmp/foo.html\", \"foo.html content\", \"utf8\");\n    await fs.mkdir(\".tmp/foo\", { recursive: true });\n    await fs.writeFile(\".tmp/foo/index.html\", \"foo/index.html content\", \"utf8\");\n    await fs.writeFile(\".tmp/foo/bar.html\", \"foo/bar.html content\", \"utf8\");\n\n    app = connect().use(\n      (req, res: ServerResponse & { superstatic?: Responder }, next) => {\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n        res.superstatic = new Responder(req, res, { provider });\n        next();\n      },\n    );\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"serves html file\", async () => {\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/foo.html\")\n      .expect(200)\n      .expect(\"foo.html content\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves html file with unicode name\", async () => {\n    await fs.writeFile(\".tmp/äää.html\", \"test\", \"utf8\");\n\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/äää.html\")\n      .expect(200)\n      .expect(\"test\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves css file\", async () => {\n    await fs.writeFile(\".tmp/style.css\", \"body {}\", \"utf8\");\n\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/style.css\")\n      .expect(200)\n      .expect(\"body {}\")\n      .expect(\"content-type\", \"text/css; charset=utf-8\");\n  });\n\n  it(\"serves a directory index file\", async () => {\n    await fs.writeFile(\".tmp/index.html\", \"test\", \"utf8\");\n\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/\")\n      .expect(200)\n      .expect(\"test\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves a file with query parameters\", async () => {\n    await fs.writeFile(\".tmp/superstatic.html\", \"test\", \"utf8\");\n\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/superstatic.html?key=value\")\n      .expect(200)\n      .expect(\"test\");\n  });\n\n  it(\"does not redirect the root url because of the trailing slash\", async () => {\n    await fs.writeFile(\".tmp/index.html\", \"an actual index\", \"utf8\");\n\n    app.use(files({}, { provider: provider }));\n\n    await request(app).get(\"/\").expect(200).expect(\"an actual index\");\n  });\n\n  it(\"does not redirect for directory index files\", async () => {\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/foo/\")\n      .expect(200)\n      .expect(\"foo/index.html content\");\n  });\n\n  it(\"function() directory index to have a trailing slash\", async () => {\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/foo\")\n      .expect((res) => {\n        expect(res.headers.location).to.equal(\"/foo/\");\n      })\n      .expect(301);\n  });\n\n  it(\"preserves query parameters and slash on subdirectory directory index redirect\", async () => {\n    app.use(files({}, { provider: provider }));\n\n    await request(app)\n      .get(\"/foo?query=params\")\n      .expect((req) => {\n        expect(req.headers.location).to.equal(\"/foo/?query=params\");\n      })\n      .expect(301);\n  });\n\n  describe(\"force trailing slash\", () => {\n    it(\"adds slash to url with no extension\", async () => {\n      app.use(files({ trailingSlash: true }, { provider: provider }));\n\n      await request(app).get(\"/foo\").expect(301).expect(\"Location\", \"/foo/\");\n    });\n  });\n\n  describe(\"force remove trailing slash\", () => {\n    it(\"removes trailing slash on urls with no file extension\", async () => {\n      app.use(files({ trailingSlash: false }, { provider: provider }));\n\n      await request(app).get(\"/foo/\").expect(301).expect(\"Location\", \"/foo\");\n    });\n\n    it(\"returns a 404 if a trailing slash was added to a valid path\", async () => {\n      app.use(files({ trailingSlash: false }, { provider: provider }));\n\n      await request(app).get(\"/foo.html/\").expect(404);\n    });\n\n    it(\"removes trailing slash on directory index urls\", async () => {\n      app.use(files({ trailingSlash: false }, { provider: provider }));\n\n      await request(app).get(\"/foo/\").expect(301).expect(\"Location\", \"/foo\");\n    });\n\n    it(\"normalizes multiple leading slashes on a redirect\", async () => {\n      app.use(files({ trailingSlash: false }, { provider: provider }));\n\n      await request(app).get(\"/foo////\").expect(301).expect(\"Location\", \"/foo\");\n    });\n  });\n\n  [\n    {\n      trailingSlashBehavior: undefined,\n      cleanUrls: false,\n      tests: [\n        { path: \"/foo\", wantRedirect: \"/foo/\" },\n        { path: \"/foo.html\", wantContent: \"foo.html content\" },\n        { path: \"/foo.html/\", wantNotFound: true },\n        { path: \"/foo/\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/bar\", wantNotFound: true },\n        { path: \"/foo/bar.html\", wantContent: \"foo/bar.html content\" },\n        { path: \"/foo/bar.html/\", wantNotFound: true },\n        { path: \"/foo/bar/\", wantNotFound: true },\n        { path: \"/foo/index\", wantNotFound: true },\n        { path: \"/foo/index.html\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/index.html/\", wantNotFound: true },\n      ],\n    },\n    {\n      trailingSlashBehavior: false,\n      cleanUrls: false,\n      tests: [\n        { path: \"/foo\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo.html\", wantContent: \"foo.html content\" },\n        { path: \"/foo.html/\", wantNotFound: true },\n        { path: \"/foo/\", wantRedirect: \"/foo\" },\n        { path: \"/foo/bar\", wantNotFound: true },\n        { path: \"/foo/bar.html\", wantContent: \"foo/bar.html content\" },\n        { path: \"/foo/bar.html/\", wantNotFound: true },\n        { path: \"/foo/bar/\", wantNotFound: true },\n        { path: \"/foo/index\", wantNotFound: true },\n        { path: \"/foo/index.html\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/index.html/\", wantNotFound: true },\n      ],\n    },\n    {\n      trailingSlashBehavior: true,\n      cleanUrls: false,\n      tests: [\n        { path: \"/foo\", wantRedirect: \"/foo/\" },\n        { path: \"/foo.html\", wantContent: \"foo.html content\" },\n        { path: \"/foo.html/\", wantNotFound: true },\n        { path: \"/foo/\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/bar\", wantNotFound: true },\n        { path: \"/foo/bar.html\", wantContent: \"foo/bar.html content\" },\n        { path: \"/foo/bar.html/\", wantNotFound: true },\n        { path: \"/foo/bar/\", wantNotFound: true },\n        { path: \"/foo/index\", wantNotFound: true },\n        { path: \"/foo/index.html\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/index.html/\", wantNotFound: true },\n      ],\n    },\n    {\n      trailingSlashBehavior: undefined,\n      cleanUrls: true,\n      tests: [\n        { path: \"/foo\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo.html\", wantRedirect: \"/foo\" },\n        { path: \"/foo.html/\", wantNotFound: true },\n        { path: \"/foo/\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/bar\", wantContent: \"foo/bar.html content\" },\n        { path: \"/foo/bar.html\", wantRedirect: \"/foo/bar\" },\n        { path: \"/foo/bar.html/\", wantNotFound: true },\n        { path: \"/foo/bar/\", wantNotFound: true },\n        { path: \"/foo/index\", wantRedirect: \"/foo\" },\n        { path: \"/foo/index.html\", wantRedirect: \"/foo\" },\n        { path: \"/foo/index.html/\", wantNotFound: true },\n      ],\n    },\n    {\n      trailingSlashBehavior: false,\n      cleanUrls: true,\n      tests: [\n        { path: \"/foo\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo.html\", wantRedirect: \"/foo\" },\n        { path: \"/foo.html/\", wantNotFound: true },\n        { path: \"/foo/\", wantRedirect: \"/foo\" },\n        { path: \"/foo/bar\", wantContent: \"foo/bar.html content\" },\n        { path: \"/foo/bar.html\", wantRedirect: \"/foo/bar\" },\n        { path: \"/foo/bar.html/\", wantNotFound: true },\n        { path: \"/foo/bar/\", wantRedirect: \"/foo/bar\" },\n        { path: \"/foo/index\", wantRedirect: \"/foo\" },\n        { path: \"/foo/index.html\", wantRedirect: \"/foo\" },\n        { path: \"/foo/index.html/\", wantNotFound: true },\n      ],\n    },\n    {\n      trailingSlashBehavior: true,\n      cleanUrls: true,\n      tests: [\n        { path: \"/foo\", wantRedirect: \"/foo/\" },\n        { path: \"/foo.html\", wantRedirect: \"/foo/\" },\n        { path: \"/foo.html/\", wantNotFound: true },\n        { path: \"/foo/\", wantContent: \"foo/index.html content\" },\n        { path: \"/foo/bar\", wantRedirect: \"/foo/bar/\" },\n        { path: \"/foo/bar.html\", wantRedirect: \"/foo/bar/\" },\n        { path: \"/foo/bar.html/\", wantNotFound: true },\n        { path: \"/foo/bar/\", wantContent: \"foo/bar.html content\" },\n        { path: \"/foo/index\", wantRedirect: \"/foo/\" },\n        { path: \"/foo/index.html\", wantRedirect: \"/foo/\" },\n        { path: \"/foo/index.html/\", wantNotFound: true },\n      ],\n    },\n  ].forEach((t) => {\n    const desc = `trailing slash ${t.trailingSlashBehavior} cleanUrls ${t.cleanUrls}`;\n    t.tests.forEach((tt) => {\n      const ttDesc = `${desc} ${JSON.stringify(tt)}`;\n      it(\"should behave correctly: \" + ttDesc, async () => {\n        app.use(\n          files(\n            { trailingSlash: t.trailingSlashBehavior, cleanUrls: t.cleanUrls },\n            { provider: provider },\n          ),\n        );\n\n        const r = request(app).get(tt.path);\n        if (tt.wantRedirect) {\n          await r.expect(301).expect(\"Location\", tt.wantRedirect);\n        } else if (tt.wantNotFound) {\n          await r.expect(404);\n        } else if (tt.wantContent) {\n          await r.expect(200).expect(tt.wantContent);\n        } else {\n          throw new Error(\"Test set up incorrectly\");\n        }\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/headers.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst helpers = require(\"../../helpers\");\nconst headers = helpers.decorator(require(\"../../../src/middleware/headers\"));\nconst connect = require(\"connect\");\nconst request = require(\"supertest\");\n\nconst defaultHeaders = [\n  { source: \"/test1\", headers: [{ key: \"Content-Type\", value: \"mime/type\" }] },\n  {\n    source: \"/test3\",\n    headers: [\n      { key: \"Access-Control-Allow-Origin\", value: \"https://www.example.net\" },\n    ],\n  },\n  {\n    source: \"/api/**\",\n    headers: [{ key: \"Access-Control-Allow-Origin\", value: \"*\" }],\n  },\n];\n\nfunction okay(req, res) {\n  res.writeHead(200);\n  res.end();\n}\n\ndescribe(\"cors middleware\", () => {\n  it(\"serves custom content types\", (done) => {\n    const app = connect()\n      .use(headers({ headers: defaultHeaders }))\n      .use(okay);\n\n    void request(app)\n      .get(\"/test1\")\n      .expect(200)\n      .expect(\"Content-Type\", \"mime/type\")\n      .end(done);\n  });\n\n  it(\"serves custom access control headers\", (done) => {\n    const app = connect()\n      .use(headers({ headers: defaultHeaders }))\n      .use(okay);\n\n    void request(app)\n      .get(\"/test3\")\n      .expect(200)\n      .expect(\"Access-Control-Allow-Origin\", \"https://www.example.net\")\n      .end(done);\n  });\n\n  it(\"uses routing rules\", (done) => {\n    const app = connect()\n      .use(headers({ headers: defaultHeaders }))\n      .use(okay);\n\n    void request(app)\n      .get(\"/api/whatever/you/wish\")\n      .expect(200)\n      .expect(\"Access-Control-Allow-Origin\", \"*\")\n      .end(done);\n  });\n\n  it(\"uses glob negation to set headers\", (done) => {\n    const app = connect()\n      .use(\n        headers({\n          headers: [\n            {\n              source: \"!/anything/**\",\n              headers: [{ key: \"custom-header\", value: \"for testing\" }],\n            },\n          ],\n        }),\n      )\n      .use(okay);\n\n    void request(app)\n      .get(\"/something\")\n      .expect(200)\n      .expect(\"custom-header\", \"for testing\")\n      .end(done);\n  });\n\n  it(\"uses regular expressions to set headers\", (done) => {\n    const app = connect()\n      .use(\n        headers({\n          headers: [\n            {\n              regex: \"/resources/\\\\d+\\\\.jpg\",\n              headers: [{ key: \"custom-header\", value: \"for testing\" }],\n            },\n          ],\n        }),\n      )\n      .use(okay);\n\n    void request(app)\n      .get(\"/resources/281.jpg\")\n      .expect(200)\n      .expect(\"custom-header\", \"for testing\")\n      .end(done);\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/missing.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as connect from \"connect\";\nimport * as fs from \"node:fs/promises\";\nimport * as request from \"supertest\";\n\nconst fsProvider = require(\"../../../src/providers/fs\");\nimport * as helpers from \"../../helpers\";\nimport * as missingModule from \"../../../src/middleware/missing\";\nimport * as Responder from \"../../../src/responder\";\n\nconst missing = helpers.decorator(missingModule);\n\ndescribe(\"custom not found\", () => {\n  const provider = fsProvider({\n    public: \".tmp\",\n  });\n  let app: connect.Server;\n\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/not-found.html\", \"custom not found file\");\n\n    app = connect().use(\n      (\n        req: connect.IncomingMessage,\n        res: any, // TODO(bkendall): extend http.ServerResponse.\n        next: connect.NextFunction,\n      ): void => {\n        res.superstatic = new Responder(req, res, { provider: provider });\n        next();\n      },\n    );\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true });\n  });\n\n  it(\"serves the file\", async () => {\n    app.use(missing({ errorPage: \"/not-found.html\" }, { provider: provider }));\n\n    await request(app)\n      .get(\"/anything\")\n      .expect(404)\n      .expect(\"custom not found file\");\n  });\n\n  it(\"skips middleware on file serve error\", async () => {\n    app\n      .use(\n        missing({ errorPage: \"/does-not-exist.html\" }, { provider: provider }),\n      )\n      .use((req, res) => {\n        res.end(\"does not exist\");\n      });\n\n    await request(app).get(\"/anything\").expect(\"does not exist\");\n  });\n\n  describe(\"with i18n files\", () => {\n    beforeEach(async () => {\n      await fs.mkdir(\".tmp/i18n/fr\", { recursive: true });\n      await fs.writeFile(\n        \".tmp/i18n/fr/not-found.html\",\n        \"my custom 404, in French\",\n      );\n    });\n\n    it(\"should resolve to the normal error page by default\", async () => {\n      app.use(\n        missing(\n          { errorPage: \"/not-found.html\", i18n: { root: \"/i18n\" } },\n          { provider: provider },\n        ),\n      );\n\n      await request(app).get(\"/anything\").expect(404, \"custom not found file\");\n    });\n\n    it(\"should resolve the i18n missing page if one was provided and matches\", async () => {\n      app.use(\n        missing(\n          { errorPage: \"/not-found.html\", i18n: { root: \"/i18n\" } },\n          { provider: provider },\n        ),\n      );\n\n      await request(app)\n        .get(\"/anything\")\n        .set(\"accept-language\", \"fr\")\n        .expect(404, \"my custom 404, in French\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/not-found.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst fs = require(\"node:fs/promises\");\nconst request = require(\"supertest\");\nconst connect = require(\"connect\");\nconst { join } = require(\"path\");\nconst { expect } = require(\"chai\");\n\nconst notFound = require(\"../../../src/middleware/not-found\");\nconst Responder = require(\"../../../src/responder\");\n\ndescribe(\"not found\", () => {\n  let app;\n\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/not-found.html\", \"not found file\", \"utf8\");\n\n    app = connect().use((req, res, next) => {\n      res.superstatic = new Responder(req, res, {\n        provider: {},\n      });\n      next();\n    });\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"serves the file\", (done) => {\n    app.use(\n      notFound({\n        errorPage: \".tmp/not-found.html\",\n      }),\n    );\n\n    void request(app)\n      .get(\"/anything\")\n      .expect(404)\n      .expect(\"not found file\")\n      .end(done);\n  });\n\n  it(\"throws on file read error\", () => {\n    expect(() => {\n      notFound({\n        errorPage: \".tmp/does-not-exist.html\",\n      });\n    }).to.throw(\"ENOENT\");\n  });\n\n  it(\"caches for one hour\", (done) => {\n    app.use(\n      notFound({\n        errorPage: join(process.cwd(), \".tmp/not-found.html\"),\n      }),\n    );\n\n    void request(app)\n      .get(\"/anything\")\n      .expect(404)\n      .expect(\"Cache-Control\", \"public, max-age=3600\")\n      .end(done);\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/redirects.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst helpers = require(\"../../helpers\");\nconst redirect = helpers.decorator(\n  require(\"../../../src/middleware/redirects\"),\n);\nconst connect = require(\"connect\");\nconst request = require(\"supertest\");\nconst Responder = require(\"../../../src/responder\");\nconst patterns = require(\"../../../src/utils/patterns\");\nconst superstaticSetup = function (req, res, next) {\n  res.superstatic = new Responder(req, res, { provider: {} });\n  next();\n};\n\ndescribe(\"redirect middleware\", () => {\n  it(\"skips the middleware if there are no redirects configured\", (done) => {\n    const app = connect().use(redirect({ redirects: [] }));\n\n    void request(app).get(\"/\").expect(404).end(done);\n  });\n\n  it(\"skips middleware when there are no matching redirects\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"/redirect\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app).get(\"/none\").expect(404).end(done);\n  });\n\n  it(\"redirects to a configured path\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"/redirect\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(301)\n      .expect(\"location\", \"/redirect\")\n      .end(done);\n  });\n\n  it(\"recognizes glob as synonymous with source\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              glob: \"/source\",\n              destination: \"/redirect\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(301)\n      .expect(\"location\", \"/redirect\")\n      .end(done);\n  });\n\n  it(\"redirects to a configured regexp path\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              regex: \"/source\",\n              destination: \"/redirect\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(301)\n      .expect(\"location\", \"/redirect\")\n      .end(done);\n  });\n\n  it(\"redirects to a configured path with a custom status code\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"/redirect\",\n              type: 302,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(302)\n      .expect(\"location\", \"/redirect\")\n      .end(done);\n  });\n\n  it(\"adds leading slash to all redirect paths\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"source\",\n              destination: \"/redirect\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(301)\n      .expect(\"location\", \"/redirect\")\n      .end(done);\n  });\n\n  it(\"redirects using glob negation\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"!source\",\n              destination: \"/redirect\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/anthing\")\n      .expect(301)\n      .expect(\"location\", \"/redirect\")\n      .end(done);\n  });\n\n  it(\"redirects using segments in the url path\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/old/:value/path/:loc\",\n              destination: \"/new/:value/path/:loc\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/old/redirect/path/there\")\n      .expect(301)\n      .expect(\"location\", \"/new/redirect/path/there\")\n      .end(done);\n  });\n\n  it(\"uses capturing groups as segments when given a regex\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              regex: \"/old/(.+)/group/(.+)\",\n              destination: \"/new/:1/path/:2\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/old/capture/group/there\")\n      .expect(301)\n      .expect(\"location\", \"/new/capture/path/there\")\n      .end(done);\n  });\n\n  it(\"handles Unicode codepoints in regexes\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              regex: \"/äöü\",\n              destination: \"/aou\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/äöü\")\n      .expect(301)\n      .expect(\"location\", \"/aou\")\n      .end(done);\n  });\n\n  it(\"percent encodes the redirect location\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              regex: \"/aou\",\n              destination: \"/ć\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/aou\")\n      .expect(301)\n      .expect(\"location\", \"/%C4%87\")\n      .end(done);\n  });\n\n  it(\"redirects using regexp captures inside path segments\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              regex: \"/foo/(.+)bar/baz\",\n              destination: \"/:1\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/foo/barbar/baz\")\n      .expect(301)\n      .expect(\"location\", \"/bar\")\n      .end(done);\n  });\n\n  it(\"redirects using regexp captures across path segments\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              regex: \"/foo/(.+)/bar\",\n              destination: \"/:1\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/foo/1/2/3/4/bar\")\n      .expect(301)\n      .expect(\"location\", \"/1/2/3/4\")\n      .end(done);\n  });\n\n  if (patterns.re2Available()) {\n    it(\"redirects using RE2 capturing groups\", (done) => {\n      const app = connect()\n        .use(superstaticSetup)\n        .use(\n          redirect({\n            redirects: [\n              {\n                regex: \"/(?P<asdf>foo)/bar\",\n                destination: \"/:asdf\",\n                type: 301,\n              },\n            ],\n          }),\n        );\n\n      void request(app)\n        .get(\"/foo/bar\")\n        .expect(301)\n        .expect(\"location\", \"/foo\")\n        .end(done);\n    });\n\n    it(\"redirects using both named and unnamed capture groups\", (done) => {\n      const app = connect()\n        .use(superstaticSetup)\n        .use(\n          redirect({\n            redirects: [\n              {\n                regex: \"/(?P<asdf>.+)/(.+)/(?P<jkl>.+)\",\n                destination: \"/:asdf/:2/:jkl\",\n                type: 301,\n              },\n            ],\n          }),\n        );\n\n      void request(app)\n        .get(\"/mixed/capture/types\")\n        .expect(301)\n        .expect(\"location\", \"/mixed/capture/types\")\n        .end(done);\n    });\n  }\n\n  it(\"redirects a missing optional segment\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/old/:value?\",\n              destination: \"/new/:value?\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/old/\")\n      .expect(301)\n      .expect(\"location\", \"/new\")\n      .end(done);\n  });\n\n  it(\"redirects a present optional segment\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/old/:value?\",\n              destination: \"/new/:value?\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/old/derp\")\n      .expect(301)\n      .expect(\"location\", \"/new/derp\")\n      .end(done);\n  });\n\n  it(\"redirects a splat segment\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/blog/:post*\",\n              destination: \"/new/:post*\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/blog/this/old/post\")\n      .expect(301)\n      .expect(\"location\", \"/new/this/old/post\")\n      .end(done);\n  });\n\n  it(\"redirects using segments in the url path with a 302\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/old/:value/path/:loc\",\n              destination: \"/new/:value/path/:loc\",\n              type: 302,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/old/redirect/path/there\")\n      .expect(302)\n      .expect(\"location\", \"/new/redirect/path/there\")\n      .end(done);\n  });\n\n  it(\"redirects to external http url\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"http://redirectedto.com\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(301)\n      .expect(\"Location\", \"http://redirectedto.com\")\n      .end(done);\n  });\n\n  it(\"redirects to external https url\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"https://redirectedto.com\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source\")\n      .expect(301)\n      .expect(\"Location\", \"https://redirectedto.com\")\n      .end(done);\n  });\n\n  it(\"preserves query params when redirecting\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"/destination\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source?foo=bar&baz=qux\")\n      .expect(301)\n      .expect(\"Location\", \"/destination?foo=bar&baz=qux\")\n      .end(done);\n  });\n\n  it(\"appends query params to the destination when redirecting\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"/destination?hello=world\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source?foo=bar&baz=qux\")\n      .expect(301)\n      .expect(\"Location\", \"/destination?hello=world&foo=bar&baz=qux\")\n      .end(done);\n  });\n\n  it(\"preserves query params when redirecting to external urls\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source\",\n              destination: \"http://example.com/destination\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source?foo=bar&baz=qux\")\n      .expect(301)\n      .expect(\"Location\", \"http://example.com/destination?foo=bar&baz=qux\")\n      .end(done);\n  });\n\n  it(\"preserves query params when redirecting with captures\", (done) => {\n    const app = connect()\n      .use(superstaticSetup)\n      .use(\n        redirect({\n          redirects: [\n            {\n              source: \"/source/:foo\",\n              destination: \"/:foo/bar\",\n              type: 301,\n            },\n          ],\n        }),\n      );\n\n    void request(app)\n      .get(\"/source/wat?foo=bar&baz=qux\")\n      .expect(301)\n      .expect(\"Location\", \"/wat/bar?foo=bar&baz=qux\")\n      .end(done);\n  });\n});\n"
  },
  {
    "path": "test/unit/middleware/rewrites.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport * as fs from \"node:fs/promises\";\nimport * as request from \"supertest\";\nimport * as connect from \"connect\";\n\nimport * as helpers from \"../../helpers\";\nimport * as rewritesPkg from \"../../../src/middleware/rewrites\";\nconst fsProvider = require(\"../../../src/providers/fs\");\nimport * as Responder from \"../../../src/responder\";\n\nconst rewrites = helpers.decorator(rewritesPkg);\n\ndescribe(\"static router\", () => {\n  const provider = fsProvider({\n    public: \".tmp\",\n  });\n  let app: connect.Server;\n\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\", { recursive: true });\n    await fs.writeFile(\".tmp/index.html\", \"index\", \"utf8\");\n\n    app = connect().use((req, res: any, next) => {\n      res.superstatic = new Responder(req, res, { provider });\n      next();\n    });\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"serves a route\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            source: \"/my-route\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/my-route\")\n      .expect(200)\n      .expect(\"index\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves a route with a glob\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            source: \"**\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/my-route\")\n      .expect(200)\n      .expect(\"index\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves a route with a regex\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            regex: \".*\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/my-route\")\n      .expect(200)\n      .expect(\"index\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves a route with an extension via a glob\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            source: \"**\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/my-route.py\")\n      .expect(200)\n      .expect(\"index\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves a route with an extension via a regex\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            regex: \"/\\\\w+\\\\.py\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/myroute.py\")\n      .expect(200)\n      .expect(\"index\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"serves a negated route\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            source: \"!/no\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/my-route\")\n      .expect(200)\n      .expect(\"index\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  it(\"skips if no match is found\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            source: \"/skip\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app).get(\"/hi\").expect(404);\n  });\n\n  it(\"serves the mime type of the rewritten file\", async () => {\n    app.use(\n      rewrites({\n        rewrites: [\n          {\n            source: \"**\",\n            destination: \"/index.html\",\n          },\n        ],\n      }),\n    );\n\n    await request(app)\n      .get(\"/index.js\")\n      .expect(\"content-type\", \"text/html; charset=utf-8\");\n  });\n\n  describe(\"uses first match\", () => {\n    beforeEach(async () => {\n      await fs.mkdir(\".tmp/admin\", { recursive: true });\n      await fs.writeFile(\".tmp/admin/index.html\", \"admin index\", \"utf8\");\n\n      app.use(\n        rewrites({\n          rewrites: [\n            { source: \"/admin/**\", destination: \"/admin/index.html\" },\n            { source: \"/something/**\", destination: \"/something/indexf.html\" },\n            { source: \"**\", destination: \"/index.html\" },\n          ],\n        }),\n      );\n    });\n\n    it(\"first route with 1 depth route\", async () => {\n      await request(app)\n        .get(\"/admin/anything\")\n        .expect(200)\n        .expect(\"admin index\");\n    });\n\n    it(\"first route with 2 depth route\", async () => {\n      await request(app)\n        .get(\"/admin/anything/else\")\n        .expect(200)\n        .expect(\"admin index\");\n    });\n\n    it(\"second route\", async () => {\n      await request(app).get(\"/anything\").expect(200).expect(\"index\");\n    });\n  });\n\n  describe(\"a relative rewrite\", () => {\n    beforeEach(() => {\n      app.use(\n        rewrites({\n          rewrites: [{ source: \"**\", destination: \"index.html\" }],\n        }),\n      );\n    });\n\n    it(\"should never work\", async () => {\n      await request(app).get(\"/about\").expect(404);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/providers/fs.spec.ts",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nimport { use, expect } from \"chai\";\nimport * as chaiAsPromised from \"chai-as-promised\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\nconst concatStream = require(\"concat-stream\");\nuse(chaiAsPromised);\n\nconst fsp = require(\"../../../src/providers/fs\");\n\nasync function readStatStream(\n  stat: {\n    stream: fs.ReadStream;\n  } | null,\n): Promise<string> {\n  if (!stat) {\n    throw new Error(\"do not have stat\");\n  }\n  const stream = stat?.stream;\n  return new Promise((resolve, reject) => {\n    stream.on(\"error\", reject);\n    stream.pipe(concatStream({ encoding: \"string\" }, resolve));\n  });\n}\n\ndescribe(\"provider: fs\", () => {\n  let opts: { cwd?: string; public?: string[] | string } = {};\n\n  beforeEach(() => {\n    opts = {\n      cwd: path.resolve(path.join(__dirname, \"..\", \"..\", \"fixtures\")),\n      public: \"a\",\n    };\n  });\n\n  it(\"should return stat information for a file that exists\", async () => {\n    await fsp(opts)({}, \"/index.html\")\n      .then(readStatStream)\n      .then((body: string) => {\n        expect(body.trim()).to.eq(\"A\");\n      });\n  });\n\n  it(\"should return null if ../\", async () => {\n    await expect(fsp(opts)({}, \"/../b/b.html\")).to.eventually.be.null;\n  });\n\n  it(\"should return null if ..\\\\\", async () => {\n    await expect(fsp(opts)({}, \"/..\\\\b\\\\b.html\")).to.eventually.be.null;\n  });\n\n  it(\"should return null if ..%5c\", async () => {\n    await expect(fsp(opts)({}, \"/..%5Cb%5cb.html\")).to.eventually.be.null;\n  });\n\n  it(\"should return null if path has null bytes\", async () => {\n    await expect(fsp(opts)({}, \"/\\0a.html\")).to.eventually.be.null;\n  });\n\n  it(\"should return null for a file that does not exist\", async () => {\n    await expect(fsp(opts)({}, \"/bogus.html\")).to.eventually.be.null;\n  });\n\n  describe(\"multiple publics\", () => {\n    beforeEach(() => {\n      opts.public = [\"a\", \"b\"];\n    });\n\n    it(\"should return the first file found for multiple publics\", async () => {\n      await fsp(opts)({}, \"/index.html\")\n        .then(readStatStream)\n        .then((body: string) => {\n          expect(body.trim()).to.eq(\"A\");\n        });\n\n      await fsp(opts)({}, \"/b.html\")\n        .then(readStatStream)\n        .then((body: string) => {\n          expect(body.trim()).to.eq(\"B\");\n        });\n    });\n\n    it(\"should return null if neither public has the file\", async () => {\n      await expect(fsp(opts)({}, \"/bogus.html\")).to.eventually.be.null;\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/providers/memory.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst chai = require(\"chai\");\nchai.use(require(\"chai-as-promised\"));\nconst expect = chai.expect;\nconst memoryProvider = require(\"../../../src/providers/memory\");\n\ndescribe(\"memory provider\", () => {\n  let store;\n  let provider;\n  beforeEach(() => {\n    store = store ?? {};\n    provider = memoryProvider({ store: store });\n  });\n\n  it(\"should resolve null if not in the store\", () => {\n    return expect(provider({}, \"/whatever\")).to.eventually.be.null;\n  });\n\n  it(\"should return a stream of the content if found\", (done) => {\n    store[\"/index.html\"] = \"foobar\";\n    provider({}, \"/index.html\").then((result) => {\n      let out = \"\";\n      result.stream.on(\"data\", (data) => {\n        out += data;\n      });\n      result.stream.on(\"end\", () => {\n        expect(out).to.eq(\"foobar\");\n        done();\n      });\n    }, done);\n  });\n\n  it(\"should return an etag of the content\", async () => {\n    store[\"/a.html\"] = \"foo\";\n    store[\"/b.html\"] = \"bar\";\n    return Promise.resolve({\n      a: await provider({}, \"/a.html\"),\n      b: await provider({}, \"/b.html\"),\n    }).then((result) => {\n      expect(result.a.etag).to.not.equal(null);\n      expect(result.b.etag).to.not.equal(null);\n      expect(result.a.etag).not.to.eq(result.b.etag);\n    });\n  });\n\n  it(\"should return the length of content\", () => {\n    store[\"/index.html\"] = \"foobar\";\n    return expect(provider({}, \"/index.html\")).to.eventually.have.property(\n      \"size\",\n      6,\n    );\n  });\n\n  afterEach(() => {\n    store = null;\n  });\n});\n"
  },
  {
    "path": "test/unit/responder.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst Responder = require(\"../../src/responder\");\nconst chai = require(\"chai\");\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\nconst sinon = require(\"sinon\");\nchai.use(require(\"chai-as-promised\"));\nchai.use(require(\"sinon-chai\"));\nconst expect = chai.expect;\n\ndescribe(\"Responder\", () => {\n  let responder;\n\n  describe(\"#handle\", () => {\n    beforeEach(() => {\n      responder = new Responder({}, { setHeader: noop, end: noop }, {});\n    });\n\n    it(\"should resolve as false with an empty stack\", () => {\n      return expect(responder.handle([])).to.eventually.eq(false);\n    });\n\n    it(\"should call the stack if an array is passed\", () => {\n      return expect(responder.handle([{ data: \"abcdef\" }])).to.eventually.eq(\n        true,\n      );\n    });\n\n    it(\"should call through to handleFile with a string\", () => {\n      const stub = sinon\n        .stub(responder, \"handleFile\")\n        .returns(Promise.resolve(true));\n      responder.handle(\"abc/def.html\");\n      expect(stub).to.have.been.calledWith({ file: \"abc/def.html\" });\n    });\n\n    it(\"should call through to handleFile with a file object\", () => {\n      const stub = sinon\n        .stub(responder, \"handleFile\")\n        .returns(Promise.resolve(true));\n      responder.handle({ file: \"abc/def.html\" });\n      expect(stub).to.have.been.calledWith({ file: \"abc/def.html\" });\n    });\n\n    it(\"should call through to handleData with a data object\", () => {\n      const stub = sinon\n        .stub(responder, \"handleData\")\n        .returns(Promise.resolve(true));\n      const obj = { data: \"abc def\" };\n      responder.handle(obj);\n      expect(stub).to.have.been.calledWith(obj);\n    });\n\n    it(\"should call through to handleRedirect with a redirect object\", () => {\n      const stub = sinon\n        .stub(responder, \"handleRedirect\")\n        .returns(Promise.resolve(true));\n      const obj = { redirect: \"/\" };\n      responder.handle(obj);\n      expect(stub).to.have.been.calledWith(obj);\n    });\n\n    it(\"should call through to handleRewrite with a rewrite object\", () => {\n      const stub = sinon\n        .stub(responder, \"handleRewrite\")\n        .returns(Promise.resolve(true));\n      const obj = { rewrite: {} };\n      responder.handle(obj);\n      expect(stub).to.have.been.calledWith(obj);\n    });\n  });\n\n  describe(\"#_handle\", () => {\n    beforeEach(() => {\n      responder = new Responder({}, { setHeader: noop, end: noop }, {});\n    });\n\n    it(\"should reject with an unrecognized payload\", () => {\n      return expect(responder._handle({ foo: \"bar\" })).to.be.rejectedWith(\n        \"is not a recognized responder directive\",\n      );\n    });\n  });\n\n  describe(\"#handleRewrite\", () => {\n    it(\"should call through to a registered custom rewriter\", () => {\n      let out;\n      responder = new Responder(\n        {},\n        {\n          setHeader: noop,\n          end: function (data) {\n            out = data;\n          },\n        },\n        {\n          rewriters: {\n            message: function (rewrite) {\n              return Promise.resolve({\n                data: rewrite.message,\n                contentType: \"text/plain\",\n                status: 200,\n              });\n            },\n          },\n        },\n      );\n\n      return responder\n        .handleRewrite({ rewrite: { message: \"hi\" } })\n        .then((result) => {\n          expect(result).to.equal(true);\n          expect(out).to.equal(\"hi\");\n        });\n    });\n  });\n\n  describe(\"#handleMiddleware\", () => {\n    let rq;\n    beforeEach(() => {\n      rq = {};\n      responder = new Responder(rq, { setHeader: noop, end: noop }, {});\n    });\n\n    it(\"should call the middleware\", (done) => {\n      responder.handleMiddleware(() => {\n        done();\n      });\n    });\n\n    it(\"should resolve false if next is called\", () => {\n      return responder\n        .handleMiddleware((req, res, next) => {\n          next();\n        })\n        .then((result) => {\n          expect(result).to.equal(false);\n        });\n    });\n  });\n\n  describe(\"#handleFile\", () => {\n    const req = {};\n    const res = {};\n    let stub;\n\n    beforeEach(() => {\n      stub = sinon.stub();\n      responder = new Responder(req, res, {\n        provider: stub,\n      });\n    });\n\n    it(\"should call through to provider\", async () => {\n      stub.returns(Promise.resolve());\n      await responder.handleFile({ file: \"abc/def.html\" });\n      expect(stub).to.have.been.calledWithExactly(req, \"abc/def.html\");\n    });\n  });\n\n  describe(\"#isNotModified\", () => {\n    let result;\n\n    beforeEach(() => {\n      responder = new Responder({ headers: {} }, {}, {});\n      result = {\n        modified: Date.now(),\n        etag: \"abcdef\",\n      };\n    });\n\n    it(\"should be false if there are no if-modified-since or if-none-match headers\", () => {\n      expect(responder.isNotModified(result)).to.equal(false);\n    });\n\n    it(\"should be false if there is a non-matching etag\", () => {\n      responder.req.headers[\"if-none-match\"] = \"defabc\";\n      expect(responder.isNotModified(result)).to.equal(false);\n    });\n\n    it(\"should be true if there is a matching etag\", () => {\n      responder.req.headers[\"if-none-match\"] = \"abcdef\";\n      expect(responder.isNotModified(result)).to.equal(true);\n    });\n\n    it(\"should be true if there is an if-modified-since after the modified\", () => {\n      responder.req.headers[\"if-modified-since\"] = new Date(\n        result.modified + 30000,\n      ).toUTCString();\n      expect(responder.isNotModified(result)).to.equal(true);\n    });\n\n    it(\"should be false if there is an if-modified-since before the modified\", () => {\n      responder.req.headers[\"if-modified-since\"] = new Date(\n        result.modified - 30000,\n      ).toUTCString();\n      expect(responder.isNotModified(result)).to.equal(false);\n    });\n  });\n\n  describe(\"#handleNotModified\", () => {\n    it(\"should return true, indicating it responded\", () => {\n      responder = new Responder({}, { removeHeader: noop, end: noop }, {});\n\n      const r = responder.handleNotModified();\n      expect(r).to.equal(true);\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/server.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\nconst path = require(\"path\");\n\nconst fs = require(\"node:fs/promises\");\nconst request = require(\"supertest\");\nconst expect = require(\"chai\").expect;\nconst stdMocks = require(\"std-mocks\");\n\nconst server = require(\"../../src/server\");\n\n// NOTE: skipping these tests because of how\n// supertest runs a connect server. The Superstatic\n// server runs with a #listen() method, while the\n// supertest runner uses the connect app object in\n// a bare http.createServer() method. This\n// doesn't work with how we are loading services.\ndescribe.skip(\"server\", () => {\n  beforeEach(async () => {\n    await fs.mkdir(\".tmp\");\n    await fs.writeFile(\".tmp/index.html\", \"index file content\");\n    await fs.writeFile(\".tmp/.env.json\", '{\"key\": \"value\"}');\n  });\n\n  afterEach(async () => {\n    await fs.rm(\".tmp\", { recursive: true, force: true });\n  });\n\n  it(\"starts a server\", (done) => {\n    const app = server();\n\n    void request(app).get(\"/\").end(done);\n  });\n\n  it(\"with config\", (done) => {\n    const app = server({\n      config: {\n        public: \".tmp\",\n      },\n    });\n\n    void request(app).get(\"/\").expect(\"index file content\").end(done);\n  });\n\n  it(\"with port\", (done) => {\n    const app = server({\n      port: 9876,\n    });\n\n    const s = app.listen(() => {\n      expect(s.address().port).to.equal(9876);\n\n      s.close(done);\n    });\n  });\n\n  it(\"with hostname\", (done) => {\n    const app = server({\n      hostname: \"127.0.0.1\",\n    });\n\n    const s = app.listen(() => {\n      expect(s.address().address).to.equal(\"127.0.0.1\");\n\n      s.close(done);\n    });\n  });\n\n  it(\"with host\", (done) => {\n    const app = server({\n      host: \"127.0.0.1\",\n    });\n\n    const s = app.listen(() => {\n      expect(s.address().address).to.equal(\"127.0.0.1\");\n\n      s.close(done);\n    });\n  });\n\n  it(\"with debug\", (done) => {\n    let output;\n    const app = server({\n      debug: true,\n    });\n\n    stdMocks.use();\n\n    void request(app)\n      .get(\"/\")\n      .end(() => {\n        stdMocks.restore();\n        output = stdMocks.flush();\n\n        expect(\n          output.stdout.toString().indexOf('\"GET / HTTP/1.1\" 404'),\n        ).to.be.greaterThan(-1);\n        done();\n      });\n  });\n\n  it(\"with env filename\", (done) => {\n    const app = server({\n      env: \".tmp/.env.json\",\n      config: {\n        public: \".tmp\",\n      },\n    });\n\n    void request(app)\n      .get(\"/__/env.json\")\n      .expect({\n        key: \"value\",\n      })\n      .end(done);\n  });\n\n  it(\"with env object\", (done) => {\n    const app = server({\n      env: {\n        type: \"object\",\n      },\n      config: {\n        public: \".tmp\",\n      },\n    });\n\n    void request(app)\n      .get(\"/__/env.json\")\n      .expect({\n        type: \"object\",\n      })\n      .end(done);\n  });\n\n  it(\"default error page\", async () => {\n    const p = path.resolve(__dirname, \"../../templates/assets/not_found.html\");\n    const notFoundContent = await fs.readFile(p, \"utf8\");\n\n    const app = server();\n\n    return void request(app).get(\"/nope\").expect(404).expect(notFoundContent);\n  });\n\n  it(\"overriden default error page\", async () => {\n    await fs.writeFile(\".tmp/error.html\", \"error page\");\n\n    const app = server({\n      errorPage: \".tmp/error.html\",\n      config: {\n        public: \".tmp\",\n      },\n    });\n\n    return void request(app).get(\"/nope\").expect(404).expect(\"error page\");\n  });\n});\n"
  },
  {
    "path": "test/unit/superstatic.spec.js",
    "content": "/**\n * Copyright (c) 2022 Google LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of\n * this software and associated documentation files (the \"Software\"), to deal in\n * the Software without restriction, including without limitation the rights to\n * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n * the Software, and to permit persons to whom the Software is furnished to do so,\n * subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\n\n// TODO:\n// test loading config file in various forms\n// test loading env file in various forms\n"
  },
  {
    "path": "test/unit/utils/i18n.spec.ts",
    "content": "import { expect } from \"chai\";\nimport { i18nContentOptions } from \"../../../src/utils/i18n\";\n\ndescribe(\"i18nContentOptions\", () => {\n  it(\"should return no files with no i18n config\", () => {\n    const paths = i18nContentOptions(\"/index.html\", {\n      superstatic: {},\n      headers: { \"accept-language\": \"fr\" },\n    });\n\n    expect(paths).to.have.members([]);\n  });\n\n  it(\"should return a list of files for language fr\", () => {\n    const paths = i18nContentOptions(\"/index.html\", {\n      superstatic: { i18n: { root: \"/i18n\" } },\n      headers: { \"accept-language\": \"fr\" },\n    });\n\n    expect(paths).to.have.members([\n      \"/i18n/fr_ALL/index.html\",\n      \"/i18n/fr/index.html\",\n    ]);\n  });\n\n  it(\"should return a list of files for country ca\", () => {\n    const paths = i18nContentOptions(\"/index.html\", {\n      superstatic: { i18n: { root: \"/i18n\" } },\n      headers: { \"x-country-code\": \"ca\" },\n    });\n\n    expect(paths).to.have.members([\"/i18n/ALL_ca/index.html\"]);\n  });\n\n  it(\"should return a list of files for language fr and country ca\", () => {\n    const paths = i18nContentOptions(\"/index.html\", {\n      superstatic: { i18n: { root: \"/i18n\" } },\n      headers: { \"accept-language\": \"fr\", \"x-country-code\": \"ca\" },\n    });\n\n    expect(paths).to.have.members([\n      \"/i18n/fr_ca/index.html\",\n      \"/i18n/ALL_ca/index.html\",\n      \"/i18n/fr_ALL/index.html\",\n      \"/i18n/fr/index.html\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/pathutils.spec.ts",
    "content": "import { expect } from \"chai\";\n\nimport * as pathutils from \"../../../src/utils/pathutils\";\n\ndescribe(\"pathutils\", () => {\n  describe(\"asDirectoryIndex\", () => {\n    it(\"should append `index.html` if it does not already\", () => {\n      expect(pathutils.asDirectoryIndex(\"path/to/dir\")).to.equal(\n        \"path/to/dir/index.html\",\n      );\n      expect(pathutils.asDirectoryIndex(\"path/to/index.html\")).to.equal(\n        \"path/to/index.html\",\n      );\n    });\n  });\n\n  describe(\"isDirectoryIndex\", () => {\n    it(\"should return true if the path ends with `index.html`\", () => {\n      expect(pathutils.isDirectoryIndex(\"path/to/file.txt\")).to.equal(false);\n      expect(pathutils.isDirectoryIndex(\"path/to/index.html\")).to.equal(true);\n    });\n  });\n\n  describe(\"addTrailingSlash\", () => {\n    it(\"should return the path with a trailing slash\", () => {\n      expect(pathutils.addTrailingSlash(\"path/to/file\")).to.equal(\n        \"path/to/file/\",\n      );\n      expect(pathutils.addTrailingSlash(\"path/to/slash/\")).to.equal(\n        \"path/to/slash/\",\n      );\n    });\n  });\n\n  describe(\"removeTrailingSlash\", () => {\n    it(\"should return the path without any trailing slash\", () => {\n      expect(pathutils.removeTrailingSlash(\"path/to/file\")).to.equal(\n        \"path/to/file\",\n      );\n      expect(pathutils.removeTrailingSlash(\"path/to/slash/\")).to.equal(\n        \"path/to/slash\",\n      );\n    });\n  });\n\n  describe(\"normalizeMultiSlashes\", () => {\n    it(\"should return the path without double slashes\", () => {\n      expect(pathutils.normalizeMultiSlashes(\"path/to///file\")).to.equal(\n        \"path/to/file\",\n      );\n      expect(pathutils.normalizeMultiSlashes(\"path//to/slash//\")).to.equal(\n        \"path/to/slash/\",\n      );\n      expect(pathutils.normalizeMultiSlashes(\"path/to/slash/\")).to.equal(\n        \"path/to/slash/\",\n      );\n    });\n  });\n\n  describe(\"removeTrailingString\", () => {\n    it(\"should return the path without double slashes\", () => {\n      expect(pathutils.removeTrailingString(\"hello/world\", \"nothing\")).to.equal(\n        \"hello/world\",\n      );\n      expect(pathutils.removeTrailingString(\"hello/world\", \"/world\")).to.equal(\n        \"hello\",\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "test/unit/utils/promiseback.spec.js",
    "content": "const { expect, use } = require(\"chai\");\nuse(require(\"chai-as-promised\"));\n\nconst promiseback = require(\"../../../src/utils/promiseback\");\n\ndescribe(\"promiseback\", () => {\n  it(\"should resolve a promise if one is returned\", () => {\n    return expect(\n      promiseback((a1, a2) => {\n        return Promise.resolve({\n          a: a1,\n          b: a2,\n        });\n      }, 2)(\"foo\", \"bar\"),\n    ).to.eventually.deep.eq({\n      a: \"foo\",\n      b: \"bar\",\n    });\n  });\n\n  it(\"should reject a promise if one is rejected\", () => {\n    return expect(\n      promiseback(() => {\n        return Promise.reject(new Error(\"broken\"));\n      }, 2)(\"foo\", \"bar\"),\n    ).to.be.rejectedWith(\"broken\");\n  });\n\n  it(\"should reject an errback if one is used and errors\", () => {\n    return expect(\n      promiseback((a1, a2, cb) => {\n        cb(a2);\n      }, 2)(\"foo\", \"bar\"),\n    ).to.be.rejectedWith(\"bar\");\n  });\n\n  it(\"should resolve an errback if one is used and resolves\", () => {\n    return expect(\n      promiseback((a1, a2, cb) => {\n        cb(null, a2);\n      }, 2)(\"foo\", \"bar\"),\n    );\n  });\n});\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"declaration\": true,\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"Node\",\n    \"outDir\": \"lib\",\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"target\": \"ES2020\"\n  },\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"include\": [\n    \"examples/**/*\",\n    \"src/**/*\",\n    \"test/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.publish.json",
    "content": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"sourceMap\": false\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  }
]