From 023418b06b2d08753a83c065bcd8250dd757e5e5 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 18 Mar 2026 16:17:14 +0100 Subject: [PATCH] fix: replace date-format dependency with custom dateFormat utility --- .github/workflows/build.yml | 34 +-- package.json | 6 +- .../src/common/PatternLayout.spec.ts | 10 +- .../src/common/components/date.ts | 26 +- .../logger-pattern-layout/src/common/index.ts | 2 +- packages/logger/package.json | 1 - packages/logger/src/index.ts | 3 +- .../utils/timestampLevelAndCategory.ts | 4 +- packages/logger/src/utils/dateFormat.ts | 243 ++++++++++++++++++ vitest.config.mts | 10 +- yarn.lock | 1 - 11 files changed, 295 insertions(+), 45 deletions(-) create mode 100644 packages/logger/src/utils/dateFormat.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4b9fa11..58d5220a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,23 +37,23 @@ jobs: run: yarn install --immutable - name: Run test 🔍 run: yarn test:coverage - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel: true - flag-name: "@tsed/logger" - path-to-lcov: './packages/logger/coverage/lcov.info' - - coveralls: - needs: test - runs-on: ubuntu-latest - steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true +# - name: Coveralls +# uses: coverallsapp/github-action@master +# with: +# github-token: ${{ secrets.GITHUB_TOKEN }} +# parallel: true +# flag-name: "@tsed/logger" +# path-to-lcov: './packages/logger/coverage/lcov.info' +# +# coveralls: +# needs: test +# runs-on: ubuntu-latest +# steps: +# - name: Coveralls Finished +# uses: coverallsapp/github-action@master +# with: +# github-token: ${{ secrets.GITHUB_TOKEN }} +# parallel-finished: true build: runs-on: ubuntu-latest diff --git a/package.json b/package.json index 5c092b52..4bc1090c 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "postinstall": "cd docs && yarn install && cd ..", "clean": "monorepo clean workspace", "test": "yarn test:lint && yarn test:coverage", - "test:unit": "lerna run test", - "test:ci": "lerna run test:ci", - "test:coverage": "yarn test:unit --stream", + "test:unit": "vitest run", + "test:ci": "vitest run --coverage --coverage.thresholds.autoUpdate=true", + "test:coverage": "vitest run --coverage --coverage.thresholds.autoUpdate=true", "test:lint": "eslint '**/*.{ts,js}'", "test:lint:fix": "yarn test:lint --fix", "build": "monorepo build --verbose", diff --git a/packages/logger-pattern-layout/src/common/PatternLayout.spec.ts b/packages/logger-pattern-layout/src/common/PatternLayout.spec.ts index e9925b24..4b54c00d 100644 --- a/packages/logger-pattern-layout/src/common/PatternLayout.spec.ts +++ b/packages/logger-pattern-layout/src/common/PatternLayout.spec.ts @@ -1,9 +1,7 @@ import "../node/index.js"; -import {LogEvent} from "@tsed/logger"; +import {dateFormat, ISO8601_WITH_TZ_OFFSET_FORMAT, LogEvent} from "@tsed/logger"; import {levels} from "@tsed/logger"; -// @ts-ignore -import * as dateFormat from "date-format"; import * as os from "os"; import {formatter} from "./fn/formatter.js"; @@ -141,11 +139,7 @@ describe("PatternLayout", () => { }); it("%d should allow for format specification", () => { - testPattern( - tokens, - "%d{ISO8601_WITH_TZ_OFFSET}", - dateFormat.asString(dateFormat.ISO8601_WITH_TZ_OFFSET_FORMAT, logEvent.startTime, undefined) - ); + testPattern(tokens, "%d{ISO8601_WITH_TZ_OFFSET}", dateFormat(ISO8601_WITH_TZ_OFFSET_FORMAT, logEvent.startTime)); testPattern(tokens, "%d{ISO8601}", "2017-06-18T22:29:38.234"); testPattern(tokens, "%d{ABSOLUTE}", "22:29:38.234"); testPattern(tokens, "%d{DATE}", "18 06 2017 22:29:38.234"); diff --git a/packages/logger-pattern-layout/src/common/components/date.ts b/packages/logger-pattern-layout/src/common/components/date.ts index df4a4e3b..c775f146 100644 --- a/packages/logger-pattern-layout/src/common/components/date.ts +++ b/packages/logger-pattern-layout/src/common/components/date.ts @@ -1,29 +1,35 @@ -import {LogEvent} from "@tsed/logger"; -// @ts-ignore -import * as dateFormat from "date-format"; +import { + ABSOLUTETIME_FORMAT, + dateFormat as serialize, + DATETIME_FORMAT, + ISO8601_FORMAT, + ISO8601_WITH_TZ_OFFSET_FORMAT, + LogEvent +} from "@tsed/logger"; import {formatter} from "../fn/formatter.js"; import type {FormatterOptions} from "../types/FormatterOptions.js"; formatter("d", (loggingEvent: LogEvent, specifier: string, {timezoneOffset}: FormatterOptions): string => { - let format = dateFormat.ISO8601_FORMAT; + let format = ISO8601_FORMAT; + if (specifier) { format = specifier; // Pick up special cases if (format === "ISO8601") { - format = dateFormat.ISO8601_FORMAT; + format = ISO8601_FORMAT; } else if (format === "ISO8601_WITH_TZ_OFFSET") { - format = dateFormat.ISO8601_WITH_TZ_OFFSET_FORMAT; + format = ISO8601_WITH_TZ_OFFSET_FORMAT; } else if (format === "ABSOLUTE") { - format = dateFormat.ABSOLUTETIME_FORMAT; + format = ABSOLUTETIME_FORMAT; } else if (format === "DATE") { - format = dateFormat.DATETIME_FORMAT; + format = DATETIME_FORMAT; } } // Format the date - return dateFormat.asString(format, loggingEvent.startTime, timezoneOffset); + return serialize(format, loggingEvent.startTime, timezoneOffset); }); formatter("r", (loggingEvent: LogEvent, _, {timezoneOffset}): string => { - return dateFormat.asString("hh:mm:ss", loggingEvent.startTime, timezoneOffset); + return serialize("hh:mm:ss", loggingEvent.startTime, timezoneOffset); }); diff --git a/packages/logger-pattern-layout/src/common/index.ts b/packages/logger-pattern-layout/src/common/index.ts index ebb51783..8a29caeb 100644 --- a/packages/logger-pattern-layout/src/common/index.ts +++ b/packages/logger-pattern-layout/src/common/index.ts @@ -1,7 +1,6 @@ /** * @file Automatically generated by @tsed/barrels. */ -export * from "./PatternLayout.js"; export * from "./components/categoryName.js"; export * from "./components/colors.js"; export * from "./components/date.js"; @@ -14,6 +13,7 @@ export * from "./components/pid.js"; export * from "./components/userDefined.js"; export * from "./decorators/Formatter.js"; export * from "./fn/formatter.js"; +export * from "./PatternLayout.js"; export * from "./registries/formatterRegistry.js"; export * from "./types/FormatterHandler.js"; export * from "./types/FormatterOptions.js"; diff --git a/packages/logger/package.json b/packages/logger/package.json index 81bdb337..7f71822d 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -55,7 +55,6 @@ "homepage": "https://github.com/tsedio/logger", "dependencies": { "colors": "1.4.0", - "date-format": "^4.0.14", "semver": "^7.7.2", "tslib": "2.8.1" }, diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 2c2148de..ccd94b5c 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -18,11 +18,12 @@ export * from "./layouts/decorators/layout.js"; export * from "./layouts/fn/layout.js"; export * from "./layouts/interfaces/BasicLayoutConfiguration.js"; export * from "./layouts/registries/LayoutsRegistry.js"; -export * from "./layouts/utils/StringUtils.js"; export * from "./layouts/utils/colorizeUtils.js"; export * from "./layouts/utils/format.js"; export * from "./layouts/utils/logEventToObject.js"; +export * from "./layouts/utils/StringUtils.js"; export * from "./layouts/utils/timestampLevelAndCategory.js"; export * from "./logger/class/Logger.js"; export * from "./logger/class/LoggerAppenders.js"; export * from "./logger/utils/tableUtils.js"; +export * from "./utils/dateFormat.js"; diff --git a/packages/logger/src/layouts/utils/timestampLevelAndCategory.ts b/packages/logger/src/layouts/utils/timestampLevelAndCategory.ts index 84856aa7..e8608c80 100644 --- a/packages/logger/src/layouts/utils/timestampLevelAndCategory.ts +++ b/packages/logger/src/layouts/utils/timestampLevelAndCategory.ts @@ -1,7 +1,7 @@ // @ts-ignore -import * as dateFormat from "date-format"; import {LogEvent} from "../../core/LogEvent.js"; +import {dateFormat} from "../../utils/dateFormat.js"; import {colorize} from "./colorizeUtils.js"; import {StringUtils} from "./StringUtils.js"; @@ -9,7 +9,7 @@ export function timestampLevelAndCategory(loggingEvent: LogEvent, colour: any, t return colorize( StringUtils.format( "[%s] [%s] [%s] - ", - dateFormat.asString(loggingEvent.startTime, timezoneOffset), + dateFormat(loggingEvent.startTime, timezoneOffset), loggingEvent.formattedLevel, loggingEvent.categoryName ), diff --git a/packages/logger/src/utils/dateFormat.ts b/packages/logger/src/utils/dateFormat.ts new file mode 100644 index 00000000..57a1a13f --- /dev/null +++ b/packages/logger/src/utils/dateFormat.ts @@ -0,0 +1,243 @@ +type DatePart = "FullYear" | "Month" | "Date" | "Hours" | "Minutes" | "Seconds" | "Milliseconds"; + +interface PatternMatcher { + pattern: RegExp; + regexp: string; + index: number; + fn: (date: Date, value: string) => void; +} + +function padWithZeros(vNumber: string | number, width: number): string { + let numAsString = vNumber.toString(); + while (numAsString.length < width) { + numAsString = "0" + numAsString; + } + return numAsString; +} + +function addZero(vNumber: string | number): string { + return padWithZeros(vNumber, 2); +} + +/** + * Formats the TimeOffset + * Thanks to http://www.svendtofte.com/code/date_format/ + * @private + */ +function offset(timezoneOffset: number): string { + const os = Math.abs(timezoneOffset); + let h = String(Math.floor(os / 60)); + let m = String(os % 60); + h = ("0" + h).slice(-2); + m = ("0" + m).slice(-2); + return timezoneOffset === 0 ? "Z" : (timezoneOffset < 0 ? "+" : "-") + h + ":" + m; +} + +export function dateFormat(date?: Date): string; +export function dateFormat(date: Date, timezoneOffset?: number): string; +export function dateFormat(format: string, date?: Date): string; +export function dateFormat(format: string, date: Date, timezoneOffset?: number): string; +export function dateFormat(formatOrDate?: string | Date, dateOrTimezoneOffset?: Date | number, _timezoneOffset?: number): string { + const format = typeof formatOrDate === "string" ? formatOrDate : ISO8601_FORMAT; + let date = typeof formatOrDate === "string" ? dateOrTimezoneOffset : formatOrDate; + + if (!(date instanceof Date)) { + date = now(); + } + + // Issue # 14 - Per ISO8601 standard, the time string should be local time + // with timezone info. + // See https://en.wikipedia.org/wiki/ISO_8601 section "Time offsets from UTC" + + const vDay = addZero(date.getDate()); + const vMonth = addZero(date.getMonth() + 1); + const vYearLong = addZero(date.getFullYear()); + const vYearShort = addZero(vYearLong.substring(2, 4)); + const vYear = format.indexOf("yyyy") > -1 ? vYearLong : vYearShort; + const vHour = addZero(date.getHours()); + const vMinute = addZero(date.getMinutes()); + const vSecond = addZero(date.getSeconds()); + const vMillisecond = padWithZeros(date.getMilliseconds(), 3); + const vTimeZone = offset(date.getTimezoneOffset()); + const formatted = format + .replace(/dd/g, vDay) + .replace(/MM/g, vMonth) + .replace(/y{1,4}/g, vYear) + .replace(/hh/g, vHour) + .replace(/mm/g, vMinute) + .replace(/ss/g, vSecond) + .replace(/SSS/g, vMillisecond) + .replace(/O/g, vTimeZone); + return formatted; +} + +function setDatePart(date: Date, part: DatePart, value: number, local: boolean): void { + const setter = `set${local ? "" : "UTC"}${part}` as const; + (date as unknown as Record number>)[setter](value); +} + +function extractDateParts(pattern: string, str: string, missingValuesDate?: Date): Date { + // Javascript Date object doesn't support custom timezone. Sets all felds as + // GMT based to begin with. If the timezone offset is provided, then adjust + // it using provided timezone, otherwise, adjust it with the system timezone. + const local = pattern.indexOf("O") < 0; + let monthOverflow = false; + const matchers: PatternMatcher[] = [ + { + pattern: /y{1,4}/, + regexp: "\\d{1,4}", + index: -1, + fn: function (date, value) { + setDatePart(date, "FullYear", Number(value), local); + } + }, + { + pattern: /MM/, + regexp: "\\d{1,2}", + index: -1, + fn: function (date, value) { + const month = Number(value) - 1; + setDatePart(date, "Month", month, local); + if (date.getMonth() !== month) { + // in the event of 31 May --> 31 Feb --> 3 Mar + // this is correct behavior if no Date is involved + monthOverflow = true; + } + } + }, + { + pattern: /dd/, + regexp: "\\d{1,2}", + index: -1, + fn: function (date, value) { + // in the event of 31 May --> 31 Feb --> 3 Mar + // reset Mar back to Feb, before setting the Date + if (monthOverflow) { + setDatePart(date, "Month", date.getMonth() - 1, local); + } + setDatePart(date, "Date", Number(value), local); + } + }, + { + pattern: /hh/, + regexp: "\\d{1,2}", + index: -1, + fn: function (date, value) { + setDatePart(date, "Hours", Number(value), local); + } + }, + { + pattern: /mm/, + regexp: "\\d\\d", + index: -1, + fn: function (date, value) { + setDatePart(date, "Minutes", Number(value), local); + } + }, + { + pattern: /ss/, + regexp: "\\d\\d", + index: -1, + fn: function (date, value) { + setDatePart(date, "Seconds", Number(value), local); + } + }, + { + pattern: /SSS/, + regexp: "\\d\\d\\d", + index: -1, + fn: function (date, value) { + setDatePart(date, "Milliseconds", Number(value), local); + } + }, + { + pattern: /O/, + regexp: "[+-]\\d{1,2}:?\\d{2}?|Z", + index: -1, + fn: function (date, value) { + if (value === "Z") { + value = "0"; + } else { + value = value.replace(":", ""); + } + const offsetValue = Number(value); + const absoluteOffset = Math.abs(offsetValue); + const timezoneOffset = (offsetValue > 0 ? -1 : 1) * ((absoluteOffset % 100) + Math.floor(absoluteOffset / 100) * 60); + // Per ISO8601 standard: UTC = local time - offset + // + // For example, 2000-01-01T01:00:00-0700 + // local time: 2000-01-01T01:00:00 + // ==> UTC : 2000-01-01T08:00:00 ( 01 - (-7) = 8 ) + // + // To make it even more confusing, the date.getTimezoneOffset() is + // opposite sign of offset string in the ISO8601 standard. So if offset + // is '-0700' the getTimezoneOffset() would be (+)420. The line above + // calculates timezoneOffset to matche Javascript's behavior. + // + // The date/time of the input is actually the local time, so the date + // object that was constructed is actually local time even thought the + // UTC setters are used. This means the date object's internal UTC + // representation was wrong. It needs to be fixed by substracting the + // offset (or adding the offset minutes as they are opposite sign). + // + // Note: the time zone has to be processed after all other fields are + // set. The result would be incorrect if the offset was calculated + // first then overriden by the other filed setters. + date.setUTCMinutes(date.getUTCMinutes() + timezoneOffset); + } + } + ]; + + const parsedPattern = matchers.reduce( + function (p, m) { + if (m.pattern.test(p.regexp)) { + m.index = p.regexp.match(m.pattern)?.index ?? -1; + p.regexp = p.regexp.replace(m.pattern, "(" + m.regexp + ")"); + } else { + m.index = -1; + } + return p; + }, + {regexp: pattern} + ); + + const dateFns = matchers.filter(function (m) { + return m.index > -1; + }); + dateFns.sort(function (a, b) { + return a.index - b.index; + }); + + const matcher = new RegExp(parsedPattern.regexp); + const matches = matcher.exec(str); + if (matches) { + const date = missingValuesDate || now(); + dateFns.forEach(function (f, i) { + f.fn(date, matches[i + 1]); + }); + + return date; + } + + throw new Error("String '" + str + "' could not be parsed as '" + pattern + "'"); +} + +export function parse(pattern: string, str: string, missingValuesDate?: Date): Date { + if (!pattern) { + throw new Error("pattern must be supplied"); + } + + return extractDateParts(pattern, str, missingValuesDate); +} + +/** + * Used for testing - replace this function with a fixed date. + */ +export function now() { + return new Date(); +} + +export const ISO8601_FORMAT = "yyyy-MM-ddThh:mm:ss.SSS"; +export const ISO8601_WITH_TZ_OFFSET_FORMAT = "yyyy-MM-ddThh:mm:ss.SSSO"; +export const DATETIME_FORMAT = "dd MM yyyy hh:mm:ss.SSS"; +export const ABSOLUTETIME_FORMAT = "hh:mm:ss.SSS"; diff --git a/vitest.config.mts b/vitest.config.mts index 3cbd2198..ffab7797 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -2,8 +2,16 @@ import {defineConfig} from "vite"; export default defineConfig({ test: { + coverage: { + thresholds: { + statements: 26, + branches: 62, + functions: 46, + lines: 26 + } + }, projects: [ 'packages/**/vitest.config.{mts,ts}', ] } -}) +}) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7b710e9b..66552cba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2896,7 +2896,6 @@ __metadata: "@tsed/typescript": "workspace:*" "@tsed/vitest": "workspace:*" colors: "npm:1.4.0" - date-format: "npm:^4.0.14" semver: "npm:^7.7.2" tslib: "npm:2.8.1" typescript: "npm:5.9.2"