diff --git a/package-lock.json b/package-lock.json index ae00e6c..e260c90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "compression": "^1.7.0", "connect": "^3.7.0", "destroy": "^1.0.4", - "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", "mime-types": "^2.1.35", @@ -3388,21 +3387,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glob-slash/-/glob-slash-1.0.0.tgz", - "integrity": "sha512-ZwFh34WZhZX28ntCMAP1mwyAJkn8+Omagvt/GvA+JQM/qgT0+MR2NPF3vhvgdshfdvDyGZXs8fPXW84K32Wjuw==" - }, - "node_modules/glob-slasher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", - "integrity": "sha512-5MUzqFiycIKLMD1B0dYOE4hGgLLUZUNGGYO4BExdwT32wUwW3DBOE7lMQars7vB1q43Fb3Tyt+HmgLKsJhDYdg==", - "dependencies": { - "glob-slash": "^1.0.0", - "lodash.isobject": "^2.4.1", - "toxic": "^1.0.0" - } - }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4174,17 +4158,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, - "node_modules/lodash._objecttypes": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", - "integrity": "sha512-XpqGh1e7hhkOzftBfWE7zt+Yn9mVHFkDhicVttvKLsoCMLVVL+xTQjfjB4X4vtznauxv0QZ5ZAeqjvat0dh62Q==" - }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -4197,14 +4170,6 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, - "node_modules/lodash.isobject": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", - "integrity": "sha512-sTebg2a1PoicYEZXD5PBdQcTlIJ6hUslrlWr7iV0O7n+i4596s2NQ9I5CaZ5FbXSfya/9WQsrYLANUJv9paYVA==", - "dependencies": { - "lodash._objecttypes": "~2.4.1" - } - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6445,14 +6410,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/toxic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toxic/-/toxic-1.0.1.tgz", - "integrity": "sha512-WI3rIGdcaKULYg7KVoB0zcjikqvcYYvcuT6D89bFPz2rVR0Rl0PK6x8/X62rtdLtBKIE985NzVf/auTtGegIIg==", - "dependencies": { - "lodash": "^4.17.10" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", diff --git a/package.json b/package.json index 12a110f..da9ffc2 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "compression": "^1.7.0", "connect": "^3.7.0", "destroy": "^1.0.4", - "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", "mime-types": "^2.1.35", diff --git a/src/middleware/headers.js b/src/middleware/headers.js index cf36ed3..16575e6 100644 --- a/src/middleware/headers.js +++ b/src/middleware/headers.js @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const slasher = require("glob-slasher"); +const { slasher } = require("../utils/slasher"); const urlParser = require("url"); const onHeaders = require("on-headers"); const patterns = require("../utils/patterns"); diff --git a/src/middleware/redirects.js b/src/middleware/redirects.js index d1fb25d..e2a1c80 100644 --- a/src/middleware/redirects.js +++ b/src/middleware/redirects.js @@ -23,7 +23,7 @@ const isUrl = require("is-url"); const patterns = require("../utils/patterns"); const pathToRegexp = require("path-to-regexp"); -const slasher = require("glob-slasher"); +const { slasher } = require("../utils/slasher"); function formatExternalUrl(u) { const cleaned = u diff --git a/src/middleware/rewrites.ts b/src/middleware/rewrites.ts index 8f17da7..76ba89b 100644 --- a/src/middleware/rewrites.ts +++ b/src/middleware/rewrites.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const slasher = require("glob-slasher"); +const { slasher } = require("../utils/slasher"); import * as urlParser from "url"; import { NextFunction } from "connect"; import { IncomingMessage, ServerResponse } from "http"; diff --git a/src/utils/slasher.ts b/src/utils/slasher.ts new file mode 100644 index 0000000..be06adc --- /dev/null +++ b/src/utils/slasher.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2026 Google LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import * as path from "path"; + +/** + * Adds a leading slash to a glob pattern and normalizes it using POSIX path + * separators. Handles negated globs (e.g. `!src` → `!/src`). + * Uses path.posix to ensure forward slashes on all platforms, including Windows. + * @param spec glob pattern to normalize + * @returns normalized glob pattern with a leading slash + */ +export function slasher( + spec: string | null | undefined, +): string | null | undefined { + if (!spec) { + return spec; + } + + if (spec.startsWith("!")) { + return "!" + path.posix.normalize(path.posix.join("/", spec.slice(1))); + } + + return path.posix.normalize(path.posix.join("/", spec)); +} diff --git a/test/unit/utils/slasher.spec.ts b/test/unit/utils/slasher.spec.ts new file mode 100644 index 0000000..dfbd9cd --- /dev/null +++ b/test/unit/utils/slasher.spec.ts @@ -0,0 +1,31 @@ +import { expect } from "chai"; +import { slasher } from "../../../src/utils/slasher"; + +describe("slasher", () => { + it("adds a leading slash to a plain path", () => { + expect(slasher("pathname")).to.equal("/pathname"); + }); + + it("is idempotent on already-slashed paths", () => { + expect(slasher("/pathname")).to.equal("/pathname"); + }); + + it("handles glob wildcards", () => { + expect(slasher("**")).to.equal("/**"); + expect(slasher("api/**")).to.equal("/api/**"); + }); + + it("handles negated globs", () => { + expect(slasher("!**/something")).to.equal("!/**/something"); + expect(slasher("!/already/slashed")).to.equal("!/already/slashed"); + }); + + it("normalizes double leading slashes", () => { + expect(slasher("//double")).to.equal("/double"); + }); + + it("passes through falsy values", () => { + expect(slasher(null)).to.equal(null); + expect(slasher(undefined)).to.equal(undefined); + }); +});