From 9dba5ff02ba3c233c77a75de70fbe6b971c61964 Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Wed, 13 May 2026 02:01:32 -0700 Subject: [PATCH] chore(lint): enable import/no-nodejs-modules and import/no-extraneous-dependencies (#2420) Removes the deferred "off" overrides from eslint.config.mjs so the obsidianmd recommended config's "error" severity applies. Scopes import/no-nodejs-modules off for test setup, mocks, and Node-context build scripts (e.g. jest.setup.js polyfills TextEncoder/TextDecoder from node:util). import/no-extraneous-dependencies is now enabled everywhere; declared js-yaml, dotenv, and @jest/globals as explicit devDependencies so transitive uses pass. Fixes the one real violation in src/utils.ts by migrating two moment call sites (formatDateTime, stringToFormattedDateTime) to Luxon, which is already in dependencies. Adds 10 unit tests covering UTC/local formatting, zero-padding, round-tripping, and invalid-input fallback. --- eslint.config.mjs | 8 +++- package-lock.json | 4 +- package.json | 3 ++ src/utils.test.ts | 98 +++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 24 +++++------- 5 files changed, 120 insertions(+), 17 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index fe07d821..78bb13b6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -109,8 +109,6 @@ export default [ "@typescript-eslint/no-deprecated": "off", // SDL / import / no-unsanitized / depend: defer — review separately - "import/no-nodejs-modules": "off", - "import/no-extraneous-dependencies": "off", "no-restricted-imports": "off", "no-restricted-globals": "off", // depend/ban-dependencies suggests native replacements for axios/lodash.debounce/etc. @@ -128,6 +126,9 @@ export default [ ...globals.node, }, }, + rules: { + "import/no-nodejs-modules": "off", + }, }, // Node-context files (build configs, scripts) @@ -147,6 +148,9 @@ export default [ ...globals.node, }, }, + rules: { + "import/no-nodejs-modules": "off", + }, }, // TypeScript-specific overrides (the @typescript-eslint plugin is registered diff --git a/package-lock.json b/package-lock.json index f6035d9f..d22b480e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "zod": "^3.25.76" }, "devDependencies": { + "@jest/globals": "^29.7.0", "@langchain/ollama": "^1.2.2", "@tailwindcss/container-queries": "^0.1.1", "@testing-library/jest-dom": "^5.16.5", @@ -110,6 +111,7 @@ "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "builtin-modules": "3.3.0", + "dotenv": "^16.4.5", "electron": "^27.3.2", "esbuild": "^0.25.0", "eslint": "^9.18.0", @@ -120,6 +122,7 @@ "husky": "^9.1.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.5.0", + "js-yaml": "^4.1.0", "lint-staged": "^15.2.9", "node-fetch": "^2.7.0", "npm-run-all": "^4.1.5", @@ -10831,7 +10834,6 @@ "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 62486bcc..a07c655e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "license": "AGPL-3.0", "devDependencies": { + "@jest/globals": "^29.7.0", "@langchain/ollama": "^1.2.2", "@tailwindcss/container-queries": "^0.1.1", "@testing-library/jest-dom": "^5.16.5", @@ -54,6 +55,7 @@ "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", "builtin-modules": "3.3.0", + "dotenv": "^16.4.5", "electron": "^27.3.2", "esbuild": "^0.25.0", "eslint": "^9.18.0", @@ -64,6 +66,7 @@ "husky": "^9.1.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.5.0", + "js-yaml": "^4.1.0", "lint-staged": "^15.2.9", "node-fetch": "^2.7.0", "npm-run-all": "^4.1.5", diff --git a/src/utils.test.ts b/src/utils.test.ts index 9b027630..69ba5146 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -3,6 +3,7 @@ import { TFile } from "obsidian"; import { extractNoteFiles, extractTemplateNoteFiles, + formatDateTime, getNotesFromPath, getNotesFromTags, getUtf8ByteLength, @@ -10,6 +11,7 @@ import { shouldUseGitHubCopilotResponsesApi, processVariableNameForNotePath, removeThinkTags, + stringToFormattedDateTime, stripFrontmatter, truncateToByteLimit, withTimeout, @@ -842,3 +844,99 @@ title: Test expect(result).toBe(" Content here."); }); }); + +describe("formatDateTime", () => { + const fixedDate = new Date("2024-03-15T14:30:45.123Z"); + + it("formats UTC deterministically when timezone='utc'", () => { + const result = formatDateTime(fixedDate, "utc"); + expect(result.fileName).toBe("20240315_143045"); + expect(result.display).toBe("2024/03/15 14:30:45"); + expect(result.epoch).toBe(fixedDate.getTime()); + }); + + it("local format is structurally valid and matches host timezone offset", () => { + const result = formatDateTime(fixedDate, "local"); + // Display must always be "YYYY/MM/DD HH:mm:ss" regardless of host TZ. + expect(result.display).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/); + expect(result.fileName).toMatch(/^\d{8}_\d{6}$/); + expect(result.epoch).toBe(fixedDate.getTime()); + + // The local display should equal what the host JS Date renders. + const pad = (n: number) => String(n).padStart(2, "0"); + const expectedDisplay = + `${fixedDate.getFullYear()}/${pad(fixedDate.getMonth() + 1)}/${pad(fixedDate.getDate())} ` + + `${pad(fixedDate.getHours())}:${pad(fixedDate.getMinutes())}:${pad(fixedDate.getSeconds())}`; + expect(result.display).toBe(expectedDisplay); + }); + + it("local and UTC differ when host is not on UTC, and agree otherwise", () => { + const local = formatDateTime(fixedDate, "local"); + const utc = formatDateTime(fixedDate, "utc"); + if (fixedDate.getTimezoneOffset() === 0) { + expect(local.display).toBe(utc.display); + } else { + expect(local.display).not.toBe(utc.display); + } + }); + + it("defaults to local timezone", () => { + const explicit = formatDateTime(fixedDate, "local"); + const defaulted = formatDateTime(fixedDate); + expect(defaulted).toEqual(explicit); + }); + + it("epoch is independent of timezone choice", () => { + expect(formatDateTime(fixedDate, "local").epoch).toBe(formatDateTime(fixedDate, "utc").epoch); + }); + + it("zero-pads single-digit month/day/hour/minute/second", () => { + const earlyDate = new Date("2024-01-02T03:04:05.000Z"); + const result = formatDateTime(earlyDate, "utc"); + expect(result.fileName).toBe("20240102_030405"); + expect(result.display).toBe("2024/01/02 03:04:05"); + }); +}); + +describe("stringToFormattedDateTime", () => { + it("parses a valid 'YYYY/MM/DD HH:mm:ss' string and round-trips display", () => { + const result = stringToFormattedDateTime("2024/03/15 10:30:45"); + expect(result.display).toBe("2024/03/15 10:30:45"); + expect(result.fileName).toBe("20240315_103045"); + // The reconstructed Date must format back to the same components in local time. + const d = new Date(result.epoch); + const pad = (n: number) => String(n).padStart(2, "0"); + expect( + `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())} ` + + `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` + ).toBe("2024/03/15 10:30:45"); + }); + + it("round-trips formatDateTime(local) output", () => { + const original = new Date("2024-07-04T18:00:00.000Z"); + const formatted = formatDateTime(original, "local"); + const reparsed = stringToFormattedDateTime(formatted.display); + expect(reparsed.display).toBe(formatted.display); + expect(reparsed.fileName).toBe(formatted.fileName); + // Display is second-precision, so the round-tripped epoch loses millis. + expect(reparsed.epoch).toBe(Math.floor(original.getTime() / 1000) * 1000); + }); + + it("falls back to current date/time on invalid input", () => { + const before = Date.now(); + const result = stringToFormattedDateTime("not-a-date"); + const after = Date.now(); + expect(result.epoch).toBeGreaterThanOrEqual(before); + expect(result.epoch).toBeLessThanOrEqual(after); + expect(result.display).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/); + expect(result.fileName).toMatch(/^\d{8}_\d{6}$/); + }); + + it("falls back on wrong-format input (e.g. dashes)", () => { + const before = Date.now(); + const result = stringToFormattedDateTime("2024-03-15 10:30:45"); + const after = Date.now(); + expect(result.epoch).toBeGreaterThanOrEqual(before); + expect(result.epoch).toBeLessThanOrEqual(after); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 0fede330..b7c4751a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,7 +19,7 @@ import { BaseChatModel } from "@langchain/core/language_models/chat_models"; import { MemoryVariables } from "@langchain/core/memory"; import { RunnableSequence } from "@langchain/core/runnables"; import { BaseChain, RetrievalQAChain } from "@langchain/classic/chains"; -import moment from "moment"; +import { DateTime } from "luxon"; import { MarkdownView, Notice, TFile, Vault, normalizePath, requestUrl } from "obsidian"; import { CustomModel } from "./aiParams"; import { getApiKeyForProvider } from "@/utils/modelUtils"; @@ -311,16 +311,12 @@ export const formatDateTime = ( now: Date, timezone: "local" | "utc" = "local" ): FormattedDateTime => { - const formattedDateTime = moment(now); - - if (timezone === "utc") { - formattedDateTime.utc(); - } + const dt = timezone === "utc" ? DateTime.fromJSDate(now).toUTC() : DateTime.fromJSDate(now); return { - fileName: formattedDateTime.format("YYYYMMDD_HHmmss"), - display: formattedDateTime.format("YYYY/MM/DD HH:mm:ss"), - epoch: formattedDateTime.valueOf(), + fileName: dt.toFormat("yyyyMMdd_HHmmss"), + display: dt.toFormat("yyyy/MM/dd HH:mm:ss"), + epoch: dt.toMillis(), }; }; @@ -359,15 +355,15 @@ export async function ensureFolderExists(folderPath: string): Promise { } export function stringToFormattedDateTime(timestamp: string): FormattedDateTime { - const date = moment(timestamp, "YYYY/MM/DD HH:mm:ss"); - if (!date.isValid()) { + const date = DateTime.fromFormat(timestamp, "yyyy/MM/dd HH:mm:ss"); + if (!date.isValid) { // If the string is not in the expected format, return current date/time return formatDateTime(new Date()); } return { - fileName: date.format("YYYYMMDD_HHmmss"), - display: date.format("YYYY/MM/DD HH:mm:ss"), - epoch: date.valueOf(), + fileName: date.toFormat("yyyyMMdd_HHmmss"), + display: date.toFormat("yyyy/MM/dd HH:mm:ss"), + epoch: date.toMillis(), }; }