From 877ce3801597e7df217bfdd133df727f2da6bece Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 6 Sep 2026 23:15:16 -0700 Subject: [PATCH] Upgrade EJS and replace Moment --- package-lock.json | 83 +++----------------------------- package.json | 3 +- src/user/system/BackupManager.ts | 10 ++-- src/utils/DateUtils.ts | 56 +++++++++++++++++++++ src/utils/Logger.ts | 4 +- tests/DateUtils.test.ts | 27 +++++++++++ tests/EjsTemplates.test.ts | 74 ++++++++++++++++++++++++++++ 7 files changed, 171 insertions(+), 86 deletions(-) create mode 100644 src/utils/DateUtils.ts create mode 100644 tests/DateUtils.test.ts create mode 100644 tests/EjsTemplates.test.ts diff --git a/package-lock.json b/package-lock.json index 89a73264..73dd8f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,14 +16,13 @@ "cron": "^4.3.1", "debug": "~4.4.1", "dockerode": "^5.0.1", - "ejs": "^3.1.10", + "ejs": "^6.0.1", "express": "^5.1.0", "fs-extra": "^11.3.0", "http-proxy": "^1.18.1", "is-valid-path": "^0.1.1", "js-base64": "^3.7.7", "jsonwebtoken": "^9.0.2", - "moment": "^2.30.1", "morgan": "^1.10.0", "multer": "^2.0.1", "on-finished": "^2.4.1", @@ -3404,12 +3403,6 @@ "node": ">=18" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3564,12 +3557,6 @@ "@babel/core": "^7.11.0 || ^8.0.0-beta.1 || ^8.0.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4660,18 +4647,15 @@ "license": "MIT" }, "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-6.0.1.tgz", + "integrity": "sha512-UaaM14yby8U3k02ihS1Bmj5Kz2d7CCQM1scxpgs4Mhkq8F1wR2gl3+Ts4h5Ne4Mnt7M9m4Dw7jsuMr3+xO4vZA==", "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, "bin": { "ejs": "bin/cli.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12.18" } }, "node_modules/electron-to-chromium": { @@ -5220,36 +5204,6 @@ "flat-cache": "^6.1.23" } }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/filing-cabinet": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/filing-cabinet/-/filing-cabinet-5.5.1.tgz", @@ -6246,23 +6200,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest": { "version": "30.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.1.tgz", @@ -7353,15 +7290,6 @@ "node": ">=18" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/morgan": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.0.tgz", @@ -7862,6 +7790,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { diff --git a/package.json b/package.json index cf8dddf2..b0684fd4 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,13 @@ "cron": "^4.3.1", "debug": "~4.4.1", "dockerode": "^5.0.1", - "ejs": "^3.1.10", + "ejs": "^6.0.1", "express": "^5.1.0", "fs-extra": "^11.3.0", "http-proxy": "^1.18.1", "is-valid-path": "^0.1.1", "js-base64": "^3.7.7", "jsonwebtoken": "^9.0.2", - "moment": "^2.30.1", "morgan": "^1.10.0", "multer": "^2.0.1", "on-finished": "^2.4.1", diff --git a/src/user/system/BackupManager.ts b/src/user/system/BackupManager.ts index 359da8d0..59b44fb9 100644 --- a/src/user/system/BackupManager.ts +++ b/src/user/system/BackupManager.ts @@ -1,7 +1,6 @@ import SshClientImport = require('ssh2') import { exec } from 'child_process' import * as fs from 'fs-extra' -import moment from 'moment' import * as path from 'path' import * as tar from 'tar' import ApiStatusCodes from '../../api/ApiStatusCodes' @@ -12,6 +11,7 @@ import { BackupMeta, RestoringInfo } from '../../models/BackupMeta' import { IHashMapGeneric } from '../../models/ICacheGeneric' import { ServerDockerInfo } from '../../models/ServerDockerInfo' import CaptainConstants from '../../utils/CaptainConstants' +import { formatBackupDate } from '../../utils/DateUtils' import Logger from '../../utils/Logger' import Utils from '../../utils/Utils' import Authenticator from '../Authenticator' @@ -738,12 +738,12 @@ export default class BackupManager { // https://github.com/caprover/caprover/issues/1257 const hostSegment = hostname ? `-host-${hostname}` : '' - const now = moment() + const now = new Date() const newName = `${ CaptainConstants.captainDownloadsDirectory - }/${namespace}/caprover-backup-${`${now.format( - 'YYYY_MM_DD-HH_mm_ss' - )}-${now.valueOf()}`}${`-ip-${mainIP}${hostSegment}.tar`}` + }/${namespace}/caprover-backup-${`${formatBackupDate( + now + )}-${now.getTime()}`}${`-ip-${mainIP}${hostSegment}.tar`}` fs.moveSync(tarFilePath, newName) setTimeout( diff --git a/src/utils/DateUtils.ts b/src/utils/DateUtils.ts new file mode 100644 index 00000000..a3a442d1 --- /dev/null +++ b/src/utils/DateUtils.ts @@ -0,0 +1,56 @@ +const MONTHS = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +] + +function pad(value: number, length = 2) { + return value.toString().padStart(length, '0') +} + +function getOrdinalSuffix(day: number) { + if (day >= 11 && day <= 13) { + return 'th' + } + + switch (day % 10) { + case 1: + return 'st' + case 2: + return 'nd' + case 3: + return 'rd' + default: + return 'th' + } +} + +export function formatLogDate(date: Date) { + const day = date.getDate() + const hours = date.getHours() + const twelveHour = hours % 12 || 12 + const meridiem = hours < 12 ? 'am' : 'pm' + + return `${MONTHS[date.getMonth()]} ${day}${getOrdinalSuffix( + day + )} ${date.getFullYear()}, ${twelveHour}:${pad(date.getMinutes())}:${pad( + date.getSeconds() + )}.${pad(date.getMilliseconds(), 3)} ${meridiem} ` +} + +export function formatBackupDate(date: Date) { + return `${date.getFullYear()}_${pad(date.getMonth() + 1)}_${pad( + date.getDate() + )}-${pad(date.getHours())}_${pad(date.getMinutes())}_${pad( + date.getSeconds() + )}` +} diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 653d0f37..ef8652a2 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,6 +1,6 @@ -import moment from 'moment' import { AnyError } from '../models/OtherTypes' import CaptainConstants from './CaptainConstants' +import { formatLogDate } from './DateUtils' function errorize(error: AnyError) { if (!(error instanceof Error)) { @@ -11,7 +11,7 @@ function errorize(error: AnyError) { } function getTime() { - return `${moment().format('MMMM Do YYYY, h:mm:ss.SSS a ')}` + return `${formatLogDate(new Date())}` } class Logger { diff --git a/tests/DateUtils.test.ts b/tests/DateUtils.test.ts new file mode 100644 index 00000000..b883c470 --- /dev/null +++ b/tests/DateUtils.test.ts @@ -0,0 +1,27 @@ +import { formatBackupDate, formatLogDate } from '../src/utils/DateUtils' + +describe('DateUtils', () => { + test.each([ + [1, 'January 1st 2026, 12:05:04.009 am '], + [2, 'January 2nd 2026, 12:05:04.009 am '], + [3, 'January 3rd 2026, 12:05:04.009 am '], + [11, 'January 11th 2026, 12:05:04.009 am '], + [12, 'January 12th 2026, 12:05:04.009 am '], + [13, 'January 13th 2026, 12:05:04.009 am '], + [21, 'January 21st 2026, 12:05:04.009 am '], + ])('formats log timestamps for day %i', (day, expected) => { + expect(formatLogDate(new Date(2026, 0, day, 0, 5, 4, 9))).toBe(expected) + }) + + test('formats afternoon log timestamps', () => { + expect(formatLogDate(new Date(2026, 8, 7, 19, 5, 4, 9))).toBe( + 'September 7th 2026, 7:05:04.009 pm ' + ) + }) + + test('formats backup timestamps', () => { + expect(formatBackupDate(new Date(2026, 8, 7, 19, 5, 4, 9))).toBe( + '2026_09_07-19_05_04' + ) + }) +}) diff --git a/tests/EjsTemplates.test.ts b/tests/EjsTemplates.test.ts new file mode 100644 index 00000000..28f215fc --- /dev/null +++ b/tests/EjsTemplates.test.ts @@ -0,0 +1,74 @@ +import ejs = require('ejs') +import fs = require('fs') +import path = require('path') + +const templates: [string, Record][] = [ + [ + 'base-nginx-conf.ejs', + { + base: { dhparamsFilePath: '/etc/nginx/dhparam.pem' }, + }, + ], + [ + 'default-page.ejs', + { + message_title: 'CapRover', + message_body: 'Ready', + message_link: 'https://caprover.com', + message_link_title: 'Documentation', + }, + ], + [ + 'root-nginx-conf.ejs', + { + fake: { crtPath: '/fake.crt', keyPath: '/fake.key' }, + captain: { + hasRootSsl: true, + crtPath: '/captain.crt', + keyPath: '/captain.key', + domain: 'captain.example.com', + serviceName: 'captain-captain', + serviceContainerPort3000: 3000, + staticWebRoot: '/captain/static', + defaultHtmlDir: '/captain/default', + }, + registry: { + hasRootSsl: false, + domain: 'registry.example.com', + staticWebRoot: '/registry/static', + }, + }, + ], + [ + 'server-block-conf.ejs', + { + s: { + forceSsl: true, + publicDomain: 'app.example.com', + staticWebRoot: '/app/static', + hasSsl: true, + crtPath: '/app.crt', + keyPath: '/app.key', + logAccessPath: '', + gzipOn: false, + localDomain: 'srv-captain--app', + containerHttpPort: 3000, + redirectToPath: '', + httpBasicAuthPath: '', + websocketSupport: true, + customErrorPagesDirectory: '/captain/error-pages', + }, + }, + ], +] + +describe('EJS nginx templates', () => { + test.each(templates)('renders %s', (filename, data) => { + const template = fs.readFileSync( + path.join(__dirname, '..', 'template', filename), + 'utf8' + ) + + expect(ejs.render(template, data)).toBeTruthy() + }) +})