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.
This commit is contained in:
Zero Liu 2026-05-13 02:01:32 -07:00 committed by GitHub
parent 2df1be0b4f
commit 9dba5ff02b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 120 additions and 17 deletions

View file

@ -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

4
package-lock.json generated
View file

@ -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"
},

View file

@ -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",

View file

@ -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);
});
});

View file

@ -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<void> {
}
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(),
};
}