diff --git a/package-lock.json b/package-lock.json index cea356b..ae00e6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", - "lodash": "^4.17.19", "mime-types": "^2.1.35", "minimatch": "^6.1.6", "morgan": "^1.8.2", @@ -35,7 +34,6 @@ "@types/chai": "^4.3.3", "@types/chai-as-promised": "^7.1.5", "@types/connect": "^3.4.35", - "@types/lodash": "^4.14.186", "@types/mime-types": "^2.1.1", "@types/mocha": "^10.0.0", "@types/node": "^24.3.1", @@ -1243,13 +1241,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", diff --git a/package.json b/package.json index 876e9a4..12a110f 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,6 @@ "glob-slasher": "^1.0.1", "is-url": "^1.2.2", "join-path": "^1.1.1", - "lodash": "^4.17.19", "mime-types": "^2.1.35", "minimatch": "^6.1.6", "morgan": "^1.8.2", @@ -77,7 +76,6 @@ "@types/chai": "^4.3.3", "@types/chai-as-promised": "^7.1.5", "@types/connect": "^3.4.35", - "@types/lodash": "^4.14.186", "@types/mime-types": "^2.1.1", "@types/mocha": "^10.0.0", "@types/node": "^24.3.1", diff --git a/src/activator.js b/src/activator.js index f44293b..737e2a0 100644 --- a/src/activator.js +++ b/src/activator.js @@ -20,7 +20,6 @@ */ const middleware = require("./middleware"); -const _ = require("lodash"); const promiseback = require("./utils/promiseback"); const Activator = function (spec, provider) { @@ -28,7 +27,7 @@ const Activator = function (spec, provider) { this.provider = provider; this.stack = this.buildStack(); - if (_.isFunction(spec.config)) { + if (typeof spec.config === "function") { this.awaitConfig = spec.config; } else { this.awaitConfig = function () { @@ -41,16 +40,16 @@ Activator.prototype.buildStack = function () { const self = this; const stack = this.spec.stack.slice(0); - _.forEach(this.spec.before, (wares, name) => { + Object.entries(this.spec.before ?? {}).forEach(([name, wares]) => { stack.splice(...[stack.indexOf(name), 0].concat(wares)); }); - _.forEach(this.spec.after, (wares, name) => { + Object.entries(this.spec.after ?? {}).forEach(([name, wares]) => { stack.splice(...[stack.indexOf(name) + 1, 0].concat(wares)); }); return stack.map((ware) => { - return _.isFunction(ware) ? ware : middleware[ware](self.spec); + return typeof ware === "function" ? ware : middleware[ware](self.spec); }); }; diff --git a/src/loaders/config-file.js b/src/loaders/config-file.js index e27ca9b..9b158a8 100644 --- a/src/loaders/config-file.js +++ b/src/loaders/config-file.js @@ -21,14 +21,14 @@ const fs = require("fs"); -const _ = require("lodash"); const join = require("join-path"); const path = require("path"); +const { isPlainObject } = require("../utils/objectutils"); const CONFIG_FILE = ["superstatic.json", "firebase.json"]; module.exports = function (filename) { - if (_.isFunction(filename)) { + if (typeof filename === "function") { return filename; } @@ -41,26 +41,26 @@ module.exports = function (filename) { try { configObject = JSON.parse(filename); } catch { - if (_.isPlainObject(filename)) { + if (isPlainObject(filename)) { configObject = filename; filename = CONFIG_FILE; } } - if (_.isArray(filename)) { - filename = _.find(filename, (name) => { + if (Array.isArray(filename)) { + filename = filename.find((name) => { return fs.existsSync(join(process.cwd(), name)); }); } // Set back to default config file if stringified object is // given as config. With this, we override values in the config file - if (_.isPlainObject(filename)) { + if (isPlainObject(filename)) { filename = CONFIG_FILE; } // A file name or array of file names - if (_.isString(filename) && _.endsWith(filename, "json")) { + if (typeof filename === "string" && filename.endsWith("json")) { try { config = JSON.parse(fs.readFileSync(path.resolve(filename))); config = config.hosting ?? config; @@ -71,5 +71,5 @@ module.exports = function (filename) { // Passing an object as the config value merges // the config data - return _.assign(config, configObject); + return { ...config, ...configObject }; }; diff --git a/src/middleware/files.js b/src/middleware/files.js index 609f8d3..3b14c70 100644 --- a/src/middleware/files.js +++ b/src/middleware/files.js @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const _ = require("lodash"); const { i18nContentOptions } = require("../utils/i18n"); const pathutils = require("../utils/pathutils"); const url = require("url"); @@ -42,7 +41,7 @@ module.exports = function () { const pathname = pathutils.normalizeMultiSlashes(parsedUrl.pathname); const search = parsedUrl.search ?? ""; - const cleanUrlRules = !!_.get(req, "superstatic.cleanUrls"); + const cleanUrlRules = !!req?.superstatic?.cleanUrls; // Exact file always wins. return providerResult(req, res, pathname) @@ -50,9 +49,9 @@ module.exports = function () { if (result) { // If we are using cleanURLs, we'll trim off any `.html` (or `/index.html`), if it exists. if (cleanUrlRules) { - if (_.endsWith(pathname, ".html")) { + if (pathname.endsWith(".html")) { let redirPath = pathutils.removeTrailingString(pathname, ".html"); - if (_.endsWith(redirPath, "/index")) { + if (redirPath.endsWith("/index")) { redirPath = pathutils.removeTrailingString(redirPath, "/index"); } if (trailingSlashBehavior === true) { @@ -154,7 +153,7 @@ module.exports = function () { }); } // If we've gotten this far and still have `/index.html` on the end, we want to remove it from the URL. - if (_.endsWith(appendedPath, "/index.html")) { + if (appendedPath.endsWith("/index.html")) { return res.superstatic.handle({ redirect: normalizeRedirectPath( pathutils.removeTrailingString( diff --git a/src/middleware/headers.js b/src/middleware/headers.js index cae67ec..cf36ed3 100644 --- a/src/middleware/headers.js +++ b/src/middleware/headers.js @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const _ = require("lodash"); const slasher = require("glob-slasher"); const urlParser = require("url"); const onHeaders = require("on-headers"); @@ -27,9 +26,9 @@ const patterns = require("../utils/patterns"); const normalizedConfigHeaders = function (spec, config) { const out = config ?? []; - if (_.isArray(config)) { + if (Array.isArray(config)) { const _isAllowed = function (headerToSet) { - return _.includes(spec.allowedHeaders, headerToSet.key.toLowerCase()); + return spec.allowedHeaders.includes(headerToSet.key.toLowerCase()); }; for (const c of config) { @@ -61,7 +60,7 @@ const matcher = function (configHeaders) { module.exports = function (spec) { return function (req, res, next) { - const config = _.get(req, "superstatic.headers"); + const config = req?.superstatic?.headers; if (!config) { return next(); } @@ -71,7 +70,7 @@ module.exports = function (spec) { const matches = headers(slasher(pathname)); onHeaders(res, () => { - _.forEach(matches, (header) => { + matches.forEach((header) => { res.setHeader(header.key, header.value); }); }); diff --git a/src/middleware/index.js b/src/middleware/index.js index 0ca976a..96a1f11 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const _ = require("lodash"); - [ "protect", "redirects", @@ -31,7 +29,7 @@ const _ = require("lodash"); "missing", ].forEach((name) => { exports[name] = function (spec, config) { - const mware = require("./" + _.kebabCase(name))(spec, config); + const mware = require("./" + name)(spec, config); mware._name = name; return mware; }; diff --git a/src/middleware/redirects.js b/src/middleware/redirects.js index c4705c7..d1fb25d 100644 --- a/src/middleware/redirects.js +++ b/src/middleware/redirects.js @@ -20,7 +20,6 @@ */ const isUrl = require("is-url"); -const _ = require("lodash"); const patterns = require("../utils/patterns"); const pathToRegexp = require("path-to-regexp"); @@ -117,13 +116,13 @@ Redirect.prototype.test = function (url) { module.exports = function () { return function (req, res, next) { - const config = _.get(req, "superstatic.redirects"); + const config = req?.superstatic?.redirects; if (!config) { return next(); } const redirects = []; - if (_.isArray(config)) { + if (Array.isArray(config)) { config.forEach((redir) => { const glob = redir.glob ?? redir.source; redirects.push( diff --git a/src/responder.js b/src/responder.js index 764c2e0..066fd7b 100644 --- a/src/responder.js +++ b/src/responder.js @@ -19,8 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const _ = require("lodash"); const mime = require("mime-types"); +const { isPlainObject } = require("./utils/objectutils"); const path = require("path"); const onFinished = require("on-finished"); const destroy = require("destroy"); @@ -72,11 +72,11 @@ Responder.prototype.handle = function (item, next) { }; Responder.prototype._handle = function (item) { - if (_.isArray(item)) { + if (Array.isArray(item)) { return this.handleStack(item); - } else if (_.isString(item)) { + } else if (typeof item === "string") { return this.handleFile({ file: item }); - } else if (_.isPlainObject(item)) { + } else if (isPlainObject(item)) { if (item.file) { return this.handleFile(item); } else if (item.redirect) { @@ -86,7 +86,7 @@ Responder.prototype._handle = function (item) { } else if (item.data) { return this.handleData(item); } - } else if (_.isFunction(item)) { + } else if (typeof item === "function") { return this.handleMiddleware(item); } diff --git a/src/superstatic.js b/src/superstatic.js index 9196374..f18e042 100644 --- a/src/superstatic.js +++ b/src/superstatic.js @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const _ = require("lodash"); const makerouter = require("router"); const fsProvider = require("./providers/fs"); @@ -58,18 +57,11 @@ const superstatic = function (spec = {}) { // Set up provider const provider = spec.provider ? promiseback(spec.provider, 2) - : fsProvider( - _.extend( - { - cwd: cwd, // default current working directory - }, - config, - ), - ); + : fsProvider({ cwd, ...config }); // Select compression middleware let compressor; - if (_.isFunction(spec.compression)) { + if (typeof spec.compression === "function") { compressor = spec.compression; } else if (spec.compression ?? spec.gzip) { compressor = defaultCompressor; diff --git a/src/utils/objectutils.ts b/src/utils/objectutils.ts new file mode 100644 index 0000000..9aaa2d8 --- /dev/null +++ b/src/utils/objectutils.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Google LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * Returns true for plain objects (`{}` or `Object.create(null)`). + * @param val value to check + * @returns value indicating whether the value is a plain object + */ +export function isPlainObject(val: unknown): val is Record { + return ( + val !== null && + typeof val === "object" && + !Array.isArray(val) && + (Object.getPrototypeOf(val) === Object.prototype || + Object.getPrototypeOf(val) === null) + ); +} diff --git a/test/unit/responder.spec.js b/test/unit/responder.spec.js index b9c2096..2949467 100644 --- a/test/unit/responder.spec.js +++ b/test/unit/responder.spec.js @@ -20,8 +20,9 @@ */ const Responder = require("../../src/responder"); -const _ = require("lodash"); const chai = require("chai"); +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; const sinon = require("sinon"); chai.use(require("chai-as-promised")); chai.use(require("sinon-chai")); @@ -32,7 +33,7 @@ describe("Responder", () => { describe("#handle", () => { beforeEach(() => { - responder = new Responder({}, { setHeader: _.noop, end: _.noop }, {}); + responder = new Responder({}, { setHeader: noop, end: noop }, {}); }); it("should resolve as false with an empty stack", () => { @@ -91,7 +92,7 @@ describe("Responder", () => { describe("#_handle", () => { beforeEach(() => { - responder = new Responder({}, { setHeader: _.noop, end: _.noop }, {}); + responder = new Responder({}, { setHeader: noop, end: noop }, {}); }); it("should reject with an unrecognized payload", () => { @@ -107,7 +108,7 @@ describe("Responder", () => { responder = new Responder( {}, { - setHeader: _.noop, + setHeader: noop, end: function (data) { out = data; }, @@ -138,7 +139,7 @@ describe("Responder", () => { let rq; beforeEach(() => { rq = {}; - responder = new Responder(rq, { setHeader: _.noop, end: _.noop }, {}); + responder = new Responder(rq, { setHeader: noop, end: noop }, {}); }); it("should call the middleware", (done) => { @@ -219,7 +220,7 @@ describe("Responder", () => { describe("#handleNotModified", () => { it("should return true, indicating it responded", () => { - responder = new Responder({}, { removeHeader: _.noop, end: _.noop }, {}); + responder = new Responder({}, { removeHeader: noop, end: noop }, {}); const r = responder.handleNotModified(); expect(r).to.equal(true);