mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
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:
parent
2df1be0b4f
commit
9dba5ff02b
5 changed files with 120 additions and 17 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
24
src/utils.ts
24
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<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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue