Group six more clusters out of web/src/lib

Takes the flat module count from 66 to 42, continuing what admin/ and
calendar/ started.

  lib/mailbox/  archiveDate, emptyFolder, folderMove, labelTree,
                mailboxName, mailboxRoute
  lib/sieve/    sieve, sieveApply, sieveFolders
  lib/input/    keyboard, swipe, touch, listSelection, dropUpload
  lib/notify/   notify, webpush, webpushEnable
  lib/sw/       swCache, swFacts, staleBuild
  lib/text/     html, markdown, text, emlName

FOUR THINGS THE FILENAMES GET WRONG, each checked by reading the file
rather than trusting what it is called:

  - appFolder is not a mailbox. It is the `ihasmail` folder in JMAP
    *Files*, where the client keeps signature images and synced settings.
    It stays flat.
  - format holds no formatting of text. It re-exports the date and clock
    formatters, so it belongs with dates/datetime, not with text/.
  - preview is the file viewer deciding what it can show without
    downloading, and source is where to point someone asking for this
    instance's AGPL source. Neither is about text.
  - notify is not Web Push. It is the tab title, the favicon badge and
    the new-mail sound -- in-app notification, which is why it sits with
    webpush rather than under sw/ with the service worker's own concerns.

threadScroll stays flat too: it decides where a conversation opens, which
is view state rather than a gesture, and input/ is honest only if
everything in it interprets something the reader did.

No behavior change. Almost every reference was on the @/ alias; eight
relative imports in files that did not move, or that moved away from a
sibling, needed rewriting by hand.
This commit is contained in:
2026-09-15 23:17:50 -07:00
parent 5cc31037c1
commit bd6a605d61
95 changed files with 104 additions and 104 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
* the joining is Intl's rather than a hardcoded " and ".
*/
import { describe, expect, it } from "vitest";
import { describeRule as describeSieve } from "../sieve";
import { describeRule as describeSieve } from "../sieve/sieve";
import { describeRule as describeRecurrence, weekdayOptions } from "../calendar/recurrence";
import { setUiLanguageForFormatting } from "../datetime";
import { setCatalog } from "../i18n";
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { hasHtmlAlternative } from "../html";
import { hasHtmlAlternative } from "../text/html";
/*
* The rule: `htmlBody` is derived, so its presence proves nothing. Only the
+1 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { isTextEntry, keyboard } from "@/lib/keyboard";
import { isTextEntry, keyboard } from "@/lib/input/keyboard";
/*
* Shortcuts after a click on a checkbox (#260).
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { comboOf, keyboard } from "@/lib/keyboard";
import { comboOf, keyboard } from "@/lib/input/keyboard";
/*
* A "keydown" that carries no key. Chrome's password autofill dispatches one
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { keyboard } from "@/lib/keyboard";
import { keyboard } from "@/lib/input/keyboard";
/*
* Two-key sequences against the single keys they start with.
+1 -1
View File
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { collectShare, shareBody, SHARE_MAX_AGE_MS } from "@/lib/shareTarget";
import { SW_CACHE_NAME } from "@/lib/swCache";
import { SW_CACHE_NAME } from "@/lib/sw/swCache";
/**
* The handoff, from the tab's side. The worker's half cannot be exercised here
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { navSwipeThreshold, swipeNavDirection, swipeThreshold, lockAxis } from "@/lib/touch";
import { navSwipeThreshold, swipeNavDirection, swipeThreshold, lockAxis } from "@/lib/input/touch";
describe("navSwipeThreshold", () => {
it("asks for more travel than a row swipe does, at every width", () => {
+1 -1
View File
@@ -3,7 +3,7 @@ import { useCalendar, type EventDraft } from "@/store/calendar";
import { useMail } from "@/store/mail";
import { uniqueAddresses } from "../address";
import { toLocalDateOnly } from "../dates";
import { htmlToText } from "../text";
import { htmlToText } from "../text/text";
/**
* How much of a message body is copied into an event description.
+1 -1
View File
@@ -9,7 +9,7 @@
* so a node has one shape and there is nothing left to detect.
*/
import type { FileNode, Id } from "@/jmap/types";
import { descendantIds } from "./folderMove";
import { descendantIds } from "./mailbox/folderMove";
/** Properties to request for a node. */
export function fileNodeProps(): string[] {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { foldersNeeded, hasDirectory, planUpload } from "@/lib/dropUpload";
import { foldersNeeded, hasDirectory, planUpload } from "@/lib/input/dropUpload";
/**
* Dropping a folder in, reduced to the two things the DataTransfer entry API
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { rowClick, type RowClick } from "@/lib/listSelection";
import { rowClick, type RowClick } from "@/lib/input/listSelection";
const IDS = ["a", "b", "c", "d", "e"];
const click = (over: Partial<Parameters<typeof rowClick>[0]> = {}): RowClick =>
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { archiveSegments, archivePath, groupByArchivePath } from "@/lib/archiveDate";
import { archiveSegments, archivePath, groupByArchivePath } from "@/lib/mailbox/archiveDate";
/**
* The dates below are written as local-time strings on purpose. The segments
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { canEmpty, emptyLabel } from "@/lib/emptyFolder";
import { canEmpty, emptyLabel } from "@/lib/mailbox/emptyFolder";
import type { MailboxRole } from "@/jmap/types";
/**
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { labelTree, visibleLabels, descendantKeywords } from "@/lib/labelTree";
import { labelTree, visibleLabels, descendantKeywords } from "@/lib/mailbox/labelTree";
import type { Label } from "@/store/settings";
const L = (keyword: string, over: Partial<Label> = {}): Label => ({ keyword, name: keyword, color: "#000", ...over });
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { isLocalizedName, mailboxDisplayName, mailboxDisplayPath } from "@/lib/mailboxName";
import { isLocalizedName, mailboxDisplayName, mailboxDisplayPath } from "@/lib/mailbox/mailboxName";
import { setCatalog, type Catalog } from "@/lib/i18n";
import type { Mailbox } from "@/jmap/types";
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isUnknownMailbox } from "@/lib/mailboxRoute";
import { isUnknownMailbox } from "@/lib/mailbox/mailboxRoute";
import type { Mailbox } from "@/jmap/types";
/**
@@ -14,7 +14,7 @@ import {
unsubscribeThisDevice,
webPushAvailable,
type JmapPushSubscription,
} from "@/lib/webpush";
} from "@/lib/notify/webpush";
import { setDeviceTrusted } from "@/lib/storage";
import type { JmapSession } from "@/jmap/types";
@@ -1,4 +1,4 @@
import { withBase } from "./basePath";
import { withBase } from "../basePath";
let baseTitle = "ihasmail";
let faviconCanvas: HTMLCanvasElement | null = null;
@@ -6,8 +6,8 @@
* permission prompt, none of which exists under a test runner.
*/
import { CAP } from "@/jmap/client";
import { withBase } from "./basePath";
import { SW_CACHE_NAME } from "./swCache";
import { withBase } from "../basePath";
import { SW_CACHE_NAME } from "../sw/swCache";
import { isDeviceTrusted } from "@/lib/storage";
import { useSession } from "@/store/session";
import { useMail } from "@/store/mail";
@@ -25,7 +25,7 @@ import {
unsubscribeThisDevice,
verifySubscription,
webPushAvailable,
} from "@/lib/webpush";
} from "@/lib/notify/webpush";
let listening = false;
+1 -1
View File
@@ -12,7 +12,7 @@
* has to find the payload lying somewhere.
*/
import { withBase } from "./basePath";
import { SW_CACHE_NAME } from "./swCache";
import { SW_CACHE_NAME } from "./sw/swCache";
export interface SharedContent {
title: string;
@@ -6,7 +6,7 @@
* Sieve below each comment is what the server actually runs.
*/
import { formatList } from "./datetime";
import { formatList } from "../datetime";
import { t } from "@/lib/i18n";
export type HeaderOp = "contains" | "notcontains" | "is" | "notis" | "matches" | "notmatches" | "regex" | "notregex" | "exists" | "notexists";
@@ -7,7 +7,7 @@ import { client, chunk } from "@/jmap/client";
import type { Email, GetResponse, Id, QueryResponse } from "@/jmap/types";
import { LIST_PROPS, useMail } from "@/store/mail";
import type { SieveRule, SieveTest } from "./sieve";
import { domainOf } from "./address";
import { domainOf } from "../address";
function headerValues(e: Email, header: string): string[] {
const h = header.toLowerCase();
+1 -1
View File
@@ -4,7 +4,7 @@
* - marker signatures: when still too big, the full HTML lives in Files and the
* identity only stores `<!--ihasmail:sig=<blobId>-->` + a plain-text fallback.
*/
import { escapeHtml, htmlToText } from "./text";
import { escapeHtml, htmlToText } from "./text/text";
/**
* Stalwart accepts a signature of `value.len() < 2048` — and that is Rust's
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { reloadIfServerRebuilt, makeConnectionWatcher, startBuildWatch } from "@/lib/staleBuild";
import { reloadIfServerRebuilt, makeConnectionWatcher, startBuildWatch } from "@/lib/sw/staleBuild";
import { APP_VERSION } from "@/lib/version";
function healthReplies(body: unknown, ok = true) {
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { publishWorkerFacts, FACTS_KEY, type WorkerFacts } from "@/lib/swFacts";
import { SW_CACHE_NAME } from "@/lib/swCache";
import { publishWorkerFacts, FACTS_KEY, type WorkerFacts } from "@/lib/sw/swFacts";
import { SW_CACHE_NAME } from "@/lib/sw/swCache";
import { setCatalog } from "@/lib/i18n";
import { catalog as de } from "@/locales/de";
@@ -1,5 +1,5 @@
import { APP_VERSION } from "./version";
import { withBase } from "./basePath";
import { APP_VERSION } from "../version";
import { withBase } from "../basePath";
import { push, type PushState } from "@/jmap/push";
/**
@@ -16,9 +16,9 @@
* was installed, which is the same condition background notifications already
* carry a push subscription has to be renewed from a tab too.
*/
import { withBase } from "./basePath";
import { withBase } from "../basePath";
import { SW_CACHE_NAME } from "./swCache";
import { t } from "./i18n";
import { t } from "../i18n";
export const FACTS_KEY = "/ihasmail-worker-facts";
+1 -1
View File
@@ -18,7 +18,7 @@
* so a template follows the same date order and clock the rest of the app was
* told to use.
*/
import { escapeHtml } from "./text";
import { escapeHtml } from "./text/text";
import { formatDate, formatClock } from "./datetime";
import type { EmailAddress } from "@/jmap/types";
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { emlFilename, sanitizeFilename } from "@/lib/emlName";
import { emlFilename, sanitizeFilename } from "@/lib/text/emlName";
describe("emlFilename", () => {
it("keeps an ordinary subject, with spaces as underscores", () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isMarkdown, renderMarkdown } from "@/lib/markdown";
import { isMarkdown, renderMarkdown } from "@/lib/text/markdown";
describe("isMarkdown", () => {
it("takes the type when there is one", () => {