From bec688b91acfbd6905901b7b083c178d735d3359 Mon Sep 17 00:00:00 2001 From: Dariusz Koryto Date: Fri, 16 Oct 2026 15:57:36 +0200 Subject: [PATCH] ci: add oxlint correctness check and resolve its findings (#592) * refactor: resolve oxlint correctness findings without behavior changes - Snapshot iterations that mutate their collection with Array.from() instead of a spread (live childNodes, self-removing disposers, a Set extended inside its own loop). - Drop empty spread fallbacks, redundant regex escapes, Boolean() casts inside negations, a rethrow-only try/catch and statement ternaries. - Use startsWith/endsWith for anchored literal checks in the extension. - List U+FE0F explicitly in the mobile attachment emoji class; the matched set is unchanged. - Fix the WeChat copy fixture, where \oint in a template literal lost its backslash and the block formula never exercised \oint. - Document the intentional control-character regexes (filename sanitizing and input validation), the throwing provider stream test, Astro's generated type reference and the mobile auto-sync finally block with narrow inline exceptions. * ci: run oxlint correctness rules on pull requests Pin oxlint 1.88.0 and enable only its correctness category across apps/ and packages/, excluding build output, vendored code and the iOS app. Findings are errors and the script passes --deny-warnings, so CI fails on any finding. no-unused-vars stays off until unused code is removed in a dedicated pass. --- .github/workflows/test.yml | 5 +- .oxlintrc.json | 26 +++++++++ apps/api/src/companion.test.mjs | 1 + apps/api/src/mcp-routes.test.mjs | 4 +- apps/api/src/resource-service.ts | 1 + apps/desktop/src/main/acp-adapter-manager.mjs | 3 +- apps/desktop/src/main/resource-url.mjs | 1 + apps/desktop/src/main/staged-resource.mjs | 2 + apps/extension/article-clip.test.mjs | 4 +- apps/extension/src/github-repo.ts | 2 +- apps/extension/src/platform-clip.ts | 2 +- apps/extension/src/selection-clip.ts | 2 +- apps/extension/src/social-clip.ts | 2 +- apps/extension/src/xueqiu-clip.ts | 2 +- .../src/components/LocalTiptapEditor.tsx | 4 +- .../src/hooks/useMobileAutomaticSync.ts | 1 + apps/mobile/src/lib/mobile-attachments.ts | 3 +- apps/site/src/components/magicui/meteors.tsx | 2 +- apps/site/src/env.d.ts | 1 + apps/web/src/components/DiagramEditorPane.tsx | 4 +- apps/web/src/components/PosterEditorPane.tsx | 2 +- apps/web/src/lib/custom-editor-theme.ts | 1 + apps/web/src/lib/desktop-repository.ts | 10 ++-- apps/web/src/lib/markdown-export.ts | 1 + apps/web/src/lib/markdown-resource-paste.ts | 2 +- apps/web/src/lib/note-html-export.ts | 1 + apps/web/src/lib/note-markdown-export.ts | 1 + apps/web/src/lib/plugins/plugin-host.test.mjs | 6 +-- apps/web/src/lib/plugins/plugin-host.ts | 16 +++--- apps/web/src/lib/wechat-copy.test.mjs | 2 +- bun.lock | 53 ++++++++++++++++--- package.json | 2 + packages/shared/src/details.ts | 10 ++-- .../shared/src/diagram-node-presentation.ts | 1 + packages/shared/src/diagram-operations.ts | 2 +- packages/shared/src/diagram-view.ts | 2 +- packages/shared/src/note-image-card.ts | 1 + packages/shared/src/poster-operations.ts | 2 +- packages/shared/src/poster.ts | 4 +- packages/shared/src/schemas.ts | 1 + 40 files changed, 139 insertions(+), 53 deletions(-) create mode 100644 .oxlintrc.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7542853f..38814d59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test -# Runs the non-E2E regression suite, type checks and the web build for every +# Runs the non-E2E regression suite, lint, type checks and the web build for every # pull request and every push to main, so failures surface before a release # instead of in the release workflow. on: @@ -52,6 +52,9 @@ jobs: sleep "$((15 * attempt))" done + - name: Lint + run: bun run lint + - name: Type check run: bun run typecheck diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..a2485dfd --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc"], + "categories": { + "correctness": "error" + }, + "rules": { + // Unused code is removed in a dedicated pass before this rule is enforced. + "no-unused-vars": "off" + }, + "overrides": [ + { + // Print windows deliberately load a preload that exposes no desktop bridge. + "files": ["apps/desktop/src/preload/print.cjs"], + "rules": { "unicorn/no-empty-file": "off" } + } + ], + "ignorePatterns": [ + "**/dist/**", + "**/vendor/**", + "apps/ios/**", + "apps/site/.astro/**", + "apps/extension/dist-firefox/**", + "packages/plugin-api/dist/**" + ] +} diff --git a/apps/api/src/companion.test.mjs b/apps/api/src/companion.test.mjs index 3786c071..dc7d0193 100644 --- a/apps/api/src/companion.test.mjs +++ b/apps/api/src/companion.test.mjs @@ -398,6 +398,7 @@ describe("companion HTTP contracts", () => { expect((await getCompanionTurn(db, scope, payload.id))).toMatchObject({ status: "failed", response: "partial response" }); }); test("maps provider HTTP 400 without leaking the response body", async () => { + // oxlint-disable-next-line require-yield -- the provider stream fails before its first chunk const { request } = fixture({ stream: async () => ({ totalUsage: Promise.resolve({}), fullStream: (async function* () { const error = new Error("Invalid schema sk-secret"); error.statusCode = 400; diff --git a/apps/api/src/mcp-routes.test.mjs b/apps/api/src/mcp-routes.test.mjs index b2dfeb13..4c0ac919 100644 --- a/apps/api/src/mcp-routes.test.mjs +++ b/apps/api/src/mcp-routes.test.mjs @@ -45,12 +45,12 @@ const mcpRequest = (payload, headers = {}) => ({ const modernMcpRequest = (payload, headers = {}) => { const params = { - ...(payload.params ?? {}), + ...payload.params, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { name: "edgeever-test", version: "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {}, - ...(payload.params?._meta ?? {}), + ...payload.params?._meta, }, }; const name = payload.method === "resources/read" ? params.uri : params.name; diff --git a/apps/api/src/resource-service.ts b/apps/api/src/resource-service.ts index 30cbf965..fba956e3 100644 --- a/apps/api/src/resource-service.ts +++ b/apps/api/src/resource-service.ts @@ -129,6 +129,7 @@ export const validateAttachmentUpload = (size: number) => { export const normalizeFilename = (filename: string) => filename .trim() .replace(/[\\/]/g, "-") + // oxlint-disable-next-line no-control-regex -- strips control characters from resource filenames .replace(/[\u0000-\u001f\u007f]/g, "") .slice(0, 160); diff --git a/apps/desktop/src/main/acp-adapter-manager.mjs b/apps/desktop/src/main/acp-adapter-manager.mjs index ff0197c4..6ded55d3 100644 --- a/apps/desktop/src/main/acp-adapter-manager.mjs +++ b/apps/desktop/src/main/acp-adapter-manager.mjs @@ -115,7 +115,8 @@ const runNpm = (args, cwd, { executablePath = process.execPath, spawnImpl = spaw child.once("error", (error) => { clearTimeout(timer); reject(error); }); child.once("exit", (code) => { clearTimeout(timer); - code === 0 ? resolve() : reject(new Error("adapter_install_failed")); + if (code === 0) resolve(); + else reject(new Error("adapter_install_failed")); }); }); diff --git a/apps/desktop/src/main/resource-url.mjs b/apps/desktop/src/main/resource-url.mjs index 54d9342e..169230dd 100644 --- a/apps/desktop/src/main/resource-url.mjs +++ b/apps/desktop/src/main/resource-url.mjs @@ -34,6 +34,7 @@ export const resourceIdFromRequest = (requestUrl) => { const normalizeDownloadFilename = (filename) => filename .trim() .replace(/[\\/]/g, "-") + // oxlint-disable-next-line no-control-regex -- strips control characters from resource filenames .replace(/[\u0000-\u001f\u007f]/g, "") .slice(0, 160); diff --git a/apps/desktop/src/main/staged-resource.mjs b/apps/desktop/src/main/staged-resource.mjs index 49a7b3da..885b845d 100644 --- a/apps/desktop/src/main/staged-resource.mjs +++ b/apps/desktop/src/main/staged-resource.mjs @@ -13,7 +13,9 @@ export const normalizeStagedResourceMetadataInput = (input) => { const type = typeof input.type === "string" && input.type.trim() ? input.type.trim() : "application/octet-stream"; const size = Number(input.size); if (!memoId || memoId.length > MAX_STAGED_RESOURCE_MEMO_ID_LENGTH) throw new Error("Invalid staged resource memo id"); + // oxlint-disable-next-line no-control-regex -- rejects control characters in staged resource names if (!name || name.length > MAX_STAGED_RESOURCE_NAME_LENGTH || /[\u0000-\u001f\u007f]/.test(name)) throw new Error("Invalid staged resource name"); + // oxlint-disable-next-line no-control-regex -- rejects control characters in staged resource types if (type.length > MAX_STAGED_RESOURCE_TYPE_LENGTH || /[\u0000-\u001f\u007f]/.test(type)) throw new Error("Invalid staged resource type"); if (!Number.isSafeInteger(size) || size <= 0 || size > MAX_STAGED_RESOURCE_BYTES) { throw new Error("Staged resource must be between 1 byte and 1 GiB"); diff --git a/apps/extension/article-clip.test.mjs b/apps/extension/article-clip.test.mjs index d4d110bd..4f8b7da9 100644 --- a/apps/extension/article-clip.test.mjs +++ b/apps/extension/article-clip.test.mjs @@ -235,7 +235,7 @@ test("Douban short comments bind to the chosen own permalink and do not capture test("Jike declared content cannot lose its entire text or links, and media cannot be replaced by duplicates",()=>{ - for(const mode of ["remove","hidden"]){const doc=fixture("jike-grid"),text=doc.querySelector('.text');mode==="remove"?text.remove():text.setAttribute('hidden','');expect(read("jike-grid",doc)).toEqual({ok:false,reason:"incomplete"});} + for(const mode of ["remove","hidden"]){const doc=fixture("jike-grid"),text=doc.querySelector('.text');if(mode==="remove")text.remove();else text.setAttribute('hidden','');expect(read("jike-grid",doc)).toEqual({ok:false,reason:"incomplete"});} const links=fixture("jike-long");for(const link of Array.from(links.querySelectorAll('.text a[href]')))link.remove();expect(read("jike-long",links)).toEqual({ok:false,reason:"incomplete"}); const wrong=fixture("jike-long");wrong.querySelector('.text a[href]').setAttribute('href','https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d');expect(read("jike-long",wrong)).toEqual({ok:false,reason:"incomplete"}); const images=fixture("jike-grid"),first=images.querySelector('.image-item img').getAttribute('src');for(const img of Array.from(images.querySelectorAll('.image-item img')))img.setAttribute('src',first);expect(read("jike-grid",images)).toEqual({ok:false,reason:"incomplete"}); @@ -244,7 +244,7 @@ test("Jike declared content cannot lose its entire text or links, and media cann test("hidden visual date and rating fields are omitted while optional short-comment time does not gate text",()=>{ const doc=fixture("douban-book");doc.querySelector('.main-meta').textContent='';const time=doc.createElement('span');time.setAttribute('content','2024-07-31');time.setAttribute('hidden','');time.textContent='2024-07-31 17:37:35';doc.querySelector('.main-meta').appendChild(time); const rating=doc.querySelector('.main-title-rating');rating.setAttribute('title','HIDDEN RATING');rating.setAttribute('hidden','');const result=read('douban-book',doc);expect(result.clip.publishedAt).toBeUndefined();expect(result.clip.markdown).not.toContain('HIDDEN RATING'); - for(const action of ['remove','href']){const list=fixture('douban-comments'),card=list.querySelector('.comment-item'),time=card.querySelector('.comment-time');action==='remove'?time.remove():time.removeAttribute('href');const target=doubanCommentTarget(card,'https://book.douban.com/subject/36864764/comments/');const saved=readDouban(list,target,'https://book.douban.com/subject/36864764/comments/');expect(saved.ok).toBe(true);expect(saved.clip.markdown).toContain('/subject/36864764/comments/#c-4237804504');} + for(const action of ['remove','href']){const list=fixture('douban-comments'),card=list.querySelector('.comment-item'),time=card.querySelector('.comment-time');if(action==='remove')time.remove();else time.removeAttribute('href');const target=doubanCommentTarget(card,'https://book.douban.com/subject/36864764/comments/');const saved=readDouban(list,target,'https://book.douban.com/subject/36864764/comments/');expect(saved.ok).toBe(true);expect(saved.clip.markdown).toContain('/subject/36864764/comments/#c-4237804504');} }); const jikeFeedUrl='https://m.okjike.com/topics/63579abb6724cc583b9bba9a'; diff --git a/apps/extension/src/github-repo.ts b/apps/extension/src/github-repo.ts index 8ab1c8b3..9a6756da 100644 --- a/apps/extension/src/github-repo.ts +++ b/apps/extension/src/github-repo.ts @@ -185,7 +185,7 @@ const cleanLicense = (value: string) => collapse(value).replace(/\s+license$/i, const cleanLanguage = (value: string) => { const text = collapse(value).replace(/\s+\d+(?:\.\d+)?%$/, "").trim(); - if (!text || text.length > 40 || /%$/.test(text) || /^languages?$/i.test(text)) return ""; + if (!text || text.length > 40 || text.endsWith("%") || /^languages?$/i.test(text)) return ""; return text; }; diff --git a/apps/extension/src/platform-clip.ts b/apps/extension/src/platform-clip.ts index 48a0b01c..7e7487b1 100644 --- a/apps/extension/src/platform-clip.ts +++ b/apps/extension/src/platform-clip.ts @@ -65,7 +65,7 @@ export const platformTarget = (pageUrl: string): PlatformTarget | null => { return id ? { platform: "xiaoyuzhou", id, url: `https://www.xiaoyuzhoufm.com/episode/${id}` } : null; } if (url.hostname === "www.zhipin.com" || url.hostname === "zhipin.com") { - const id = /^\/job_detail\/([a-zA-Z0-9_~\-]+)\.html$/.exec(url.pathname)?.[1]; + const id = /^\/job_detail\/([a-zA-Z0-9_~-]+)\.html$/.exec(url.pathname)?.[1]; return id ? { platform: "boss", id, url: `https://www.zhipin.com/job_detail/${id}.html` } : null; } if (url.hostname === "xueqiu.com" || url.hostname === "www.xueqiu.com") { diff --git a/apps/extension/src/selection-clip.ts b/apps/extension/src/selection-clip.ts index 3d705279..8233c8cb 100644 --- a/apps/extension/src/selection-clip.ts +++ b/apps/extension/src/selection-clip.ts @@ -1,7 +1,7 @@ /** Plain-text fallback when the clicked selection no longer has matching DOM. */ export const selectionTextMarkdown = (value: string) => value // Entities keep Markdown, HTML and automatic links from reinterpreting literal text. - .replace(/[!-/:-@\[-`{-~]/g, (character) => `&#${character.charCodeAt(0)};`) + .replace(/[!-/:-@[-`{-~]/g, (character) => `&#${character.charCodeAt(0)};`) .replace(/^[ \t]+/gm, (indent) => indent.replace(/ /g, " ").replace(/\t/g, " ")); const escapeMarkdownLabel = (value: string) => diff --git a/apps/extension/src/social-clip.ts b/apps/extension/src/social-clip.ts index 87e20745..4e8a0eb2 100644 --- a/apps/extension/src/social-clip.ts +++ b/apps/extension/src/social-clip.ts @@ -455,7 +455,7 @@ export const readLinkedin = (doc: Document, target: PlatformTarget): PlatformRea link.closest(LINKEDIN_CARD_SELECTOR) === card && platformText(link)); const person = actorLinks.find(link => { const url = safePlatformUrl(link.getAttribute('href') || '', target.url); - return url && /(^|\.)linkedin\.com$/.test(new URL(url).hostname) && /^\/in\//.test(new URL(url).pathname); + return url && /(^|\.)linkedin\.com$/.test(new URL(url).hostname) && new URL(url).pathname.startsWith("/in/"); }); const author = text(record(schema?.author)?.name) || platformText(own('.update-components-actor__title, [data-tracking-control-name="public_post_feed-actor-name"]')) || platformText(person?.querySelector('p') || person) || actorLinks.map(platformText).find(Boolean); diff --git a/apps/extension/src/xueqiu-clip.ts b/apps/extension/src/xueqiu-clip.ts index 7e02e582..3964c3c3 100644 --- a/apps/extension/src/xueqiu-clip.ts +++ b/apps/extension/src/xueqiu-clip.ts @@ -71,7 +71,7 @@ export const readXueqiu = (doc: Document, target: PlatformTarget, sourceUrl: str const title = (titleNode ? platformText(platformReadableCopy(titleNode)) : "") || platformText(clean).slice(0, 120); if (!markdown || !title) return { ok: false, reason: "unreadable" }; const cleanTime = timeLink ? platformReadableCopy(timeLink) : null; - const published = cleanTime && /^发布于/.test(platformText(cleanTime)) ? publishedTime(cleanTime.querySelector("time")) : undefined; + const published = cleanTime && platformText(cleanTime).startsWith("发布于") ? publishedTime(cleanTime.querySelector("time")) : undefined; return { ok: true, clip: { ...target, title, author: author ? platformText(platformReadableCopy(author)) || undefined : undefined, publishedAt: published, markdown, tags: ["web-clip", "xueqiu", "投资", ...xueqiuStocks(clean, target.url)], completeness: "complete" } }; }; diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx index ee8070b2..7443abe4 100644 --- a/apps/mobile/src/components/LocalTiptapEditor.tsx +++ b/apps/mobile/src/components/LocalTiptapEditor.tsx @@ -1718,9 +1718,9 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) { disabled={(action.id === "undo" && ((historyState ?? 0) & 1) === 0) || (action.id === "redo" && ((historyState ?? 0) & 2) === 0) || (action.id === "increaseListIndent" - && !Boolean(editor?.can().chain().focus().sinkListItem(activeListItemType).run())) + && !editor?.can().chain().focus().sinkListItem(activeListItemType).run()) || (action.id === "decreaseListIndent" - && !Boolean(editor?.can().chain().focus().liftListItem(activeListItemType).run()))} + && !editor?.can().chain().focus().liftListItem(activeListItemType).run())} icon={toolbarIcons[action.id]} label={getMobileEditorToolbarActionLabel(action.id, props.locale)} onRun={toolbarHandlers[action.id]} diff --git a/apps/mobile/src/hooks/useMobileAutomaticSync.ts b/apps/mobile/src/hooks/useMobileAutomaticSync.ts index 87d81830..8d5920db 100644 --- a/apps/mobile/src/hooks/useMobileAutomaticSync.ts +++ b/apps/mobile/src/hooks/useMobileAutomaticSync.ts @@ -112,6 +112,7 @@ export const useMobileAutomaticSync = ({ requestedRef.current = false; requestedForceRef.current = false; void runRef.current(nextForce); + // oxlint-disable-next-line no-unsafe-finally -- the catch above handles every error, so nothing is overridden; the finally block is restructured separately return; } diff --git a/apps/mobile/src/lib/mobile-attachments.ts b/apps/mobile/src/lib/mobile-attachments.ts index 971fd07d..946815c6 100644 --- a/apps/mobile/src/lib/mobile-attachments.ts +++ b/apps/mobile/src/lib/mobile-attachments.ts @@ -67,7 +67,7 @@ const normalizeAttachmentFilename = (label: string, resourceId: string) => { const withoutPrefix = label.replace(/^\s*(?:附件[::]|Attachment:)\s*/i, "").trim(); const colonParts = withoutPrefix.split(/[::]\s*/).filter(Boolean); const candidate = (colonParts.at(-1) || withoutPrefix) - .replace(/^[\s📎📄📦📊🗃️🗂️]+/u, "") + .replace(/^[\uFE0F\s📎📄📦📊🗃🗂]+/u, "") .trim(); return candidate || resourceId; }; @@ -225,6 +225,7 @@ export const getMobileAttachmentUpdatePayload = (contentJson: TiptapDoc) => ({ export const getMobileResourceUpdatePayload = getMobileAttachmentUpdatePayload; const safeCacheFilename = (filename: string) => + // oxlint-disable-next-line no-control-regex -- strips control characters from cached filenames filename.trim().replace(/[\\/]/g, "-").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 160) || "attachment"; const EXT_BY_MIME: Record = { diff --git a/apps/site/src/components/magicui/meteors.tsx b/apps/site/src/components/magicui/meteors.tsx index aab57a43..3bf7ba59 100644 --- a/apps/site/src/components/magicui/meteors.tsx +++ b/apps/site/src/components/magicui/meteors.tsx @@ -12,7 +12,7 @@ export const Meteors = ({ number = 20, className }: MeteorsProps) => { ); useEffect(() => { - const styles = [...new Array(number)].map(() => ({ + const styles = Array.from({ length: number }, () => ({ top: 0, left: Math.floor(Math.random() * (400 - -400) + -400) + "px", animationDelay: Math.random() * (0.8 - 0.2) + 0.2 + "s", diff --git a/apps/site/src/env.d.ts b/apps/site/src/env.d.ts index acef35f1..84666ef5 100644 --- a/apps/site/src/env.d.ts +++ b/apps/site/src/env.d.ts @@ -1,2 +1,3 @@ +// oxlint-disable-next-line typescript/triple-slash-reference -- Astro loads its generated types this way /// /// diff --git a/apps/web/src/components/DiagramEditorPane.tsx b/apps/web/src/components/DiagramEditorPane.tsx index 4c98012a..9deecb9c 100644 --- a/apps/web/src/components/DiagramEditorPane.tsx +++ b/apps/web/src/components/DiagramEditorPane.tsx @@ -875,7 +875,7 @@ const bindDiagramScrollerFit = (graph: Graph) => { gridWidth as number | undefined, gridHeight, padding as number | undefined, - applyDiagramScrollerFitOptions({ ...(options ?? {}) }, bounds), + applyDiagramScrollerFitOptions({ ...options }, bounds), ); }) as typeof graph.fitToContent & { edgeeverBound?: boolean }; bound.edgeeverBound = true; @@ -1240,7 +1240,7 @@ const nodeMetadata = ( text: diagramNodePresentation(node, kind).text, lineHeight: mindStyle?.visual.label.lineHeight ?? flowchartStyle?.label.lineHeight ?? architectureStyle?.label.lineHeight ?? 18, }, - ...(architectureStyle?.attrs ?? {}), + ...architectureStyle?.attrs, }, ...(hasPorts ? { ports: { groups: { diff --git a/apps/web/src/components/PosterEditorPane.tsx b/apps/web/src/components/PosterEditorPane.tsx index cda5473e..83c97a84 100644 --- a/apps/web/src/components/PosterEditorPane.tsx +++ b/apps/web/src/components/PosterEditorPane.tsx @@ -1138,7 +1138,7 @@ export default function PosterEditorPane({ const sourceSession = requiresLocalEditSession(source) ? createLocalEditSession(source) : (await api.createMemoEditSession(source.id)).editSession; - const markdown = `${source.contentMarkdown}\n\n![${title.replace(/[\[\]\\\n]/g, " ")}](${resource.url})`; + const markdown = `${source.contentMarkdown}\n\n![${title.replace(/[[\]\\\n]/g, " ")}](${resource.url})`; const result = await repository.updateMemo(source, { expectedRevision: source.revision, expectedContentHash: source.contentHash, diff --git a/apps/web/src/lib/custom-editor-theme.ts b/apps/web/src/lib/custom-editor-theme.ts index 378550f5..15203fa7 100644 --- a/apps/web/src/lib/custom-editor-theme.ts +++ b/apps/web/src/lib/custom-editor-theme.ts @@ -173,6 +173,7 @@ export const parseCustomEditorThemeFile = (source: string): PortableCustomEditor export const customEditorThemeFileName = (name: string) => { const safeName = name .trim() + // oxlint-disable-next-line no-control-regex -- replaces control characters in theme filenames .replace(/[\u0000-\u001f<>:"/\\|?*]+/g, "-") .replace(/\s+/g, " ") .replace(/[. ]+$/g, "") diff --git a/apps/web/src/lib/desktop-repository.ts b/apps/web/src/lib/desktop-repository.ts index 57e53e8a..e03d5de4 100644 --- a/apps/web/src/lib/desktop-repository.ts +++ b/apps/web/src/lib/desktop-repository.ts @@ -161,13 +161,9 @@ export const createDesktopRepository = (): EdgeEverRepository => ({ return local; } if (typeof navigator !== "undefined" && !navigator.onLine) return local; - try { - const remote = await api.listTemplates(); - await Promise.all(remote.templates.map((template) => request("template.cache", { template }))); - return remote; - } catch (error) { - throw error; - } + const remote = await api.listTemplates(); + await Promise.all(remote.templates.map((template) => request("template.cache", { template }))); + return remote; }, createTemplate: (input) => request("template.create", input).then((result) => { window.dispatchEvent(new CustomEvent("edgeever:sync-queue-changed")); diff --git a/apps/web/src/lib/markdown-export.ts b/apps/web/src/lib/markdown-export.ts index 369ea207..a961b1fa 100644 --- a/apps/web/src/lib/markdown-export.ts +++ b/apps/web/src/lib/markdown-export.ts @@ -34,6 +34,7 @@ export class MarkdownExportMemoryLimitError extends Error { export const sanitizeExportPathSegment = (value: string, fallback: string) => { const sanitized = value + // oxlint-disable-next-line no-control-regex -- replaces control characters in export paths .replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-") .replace(/\s+/g, " ") .replace(/[. ]+$/g, "") diff --git a/apps/web/src/lib/markdown-resource-paste.ts b/apps/web/src/lib/markdown-resource-paste.ts index df85f723..f798fcc7 100644 --- a/apps/web/src/lib/markdown-resource-paste.ts +++ b/apps/web/src/lib/markdown-resource-paste.ts @@ -1,6 +1,6 @@ const escapeLabel = (value: string) => value .replaceAll("\\", "\\\\") - .replace(/[\[\]\r\n]/g, (character) => character === "\r" || character === "\n" ? " " : `\\${character}`); + .replace(/[[\]\r\n]/g, (character) => character === "\r" || character === "\n" ? " " : `\\${character}`); export const resourceToMarkdown = (resource: { kind: "image" | "attachment"; url: string; filename: string | null }, fallbackName: string) => { const label = escapeLabel(resource.filename || fallbackName || "file"); diff --git a/apps/web/src/lib/note-html-export.ts b/apps/web/src/lib/note-html-export.ts index 3acaa7f7..040fc8cf 100644 --- a/apps/web/src/lib/note-html-export.ts +++ b/apps/web/src/lib/note-html-export.ts @@ -66,6 +66,7 @@ export const getHtmlImageEmbedNoticeKind = ( export const buildHtmlFilename = (title: string, fallback: string) => { const sanitized = title + // oxlint-disable-next-line no-control-regex -- replaces control characters in export filenames .replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-") .replace(/\s+/g, " ") .replace(/[. ]+$/g, "") diff --git a/apps/web/src/lib/note-markdown-export.ts b/apps/web/src/lib/note-markdown-export.ts index c2ba40b4..71990a4c 100644 --- a/apps/web/src/lib/note-markdown-export.ts +++ b/apps/web/src/lib/note-markdown-export.ts @@ -14,6 +14,7 @@ const WINDOWS_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i; export const buildMarkdownFilename = (title: string, fallback: string) => { const sanitized = title + // oxlint-disable-next-line no-control-regex -- replaces control characters in export filenames .replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-") .replace(/\s+/g, " ") .replace(/[. ]+$/g, "") diff --git a/apps/web/src/lib/plugins/plugin-host.test.mjs b/apps/web/src/lib/plugins/plugin-host.test.mjs index c44398fd..fdafda2b 100644 --- a/apps/web/src/lib/plugins/plugin-host.test.mjs +++ b/apps/web/src/lib/plugins/plugin-host.test.mjs @@ -189,7 +189,7 @@ describe("EdgeEverPluginHost", () => { set: async (pluginId, key, value) => secrets.set(`${pluginId}:${key}`, value), remove: async (pluginId, key) => secrets.delete(`${pluginId}:${key}`), clearNamespace: async (pluginId) => { - for (const key of [...secrets.keys()]) if (key.startsWith(`${pluginId}:`)) secrets.delete(key); + for (const key of Array.from(secrets.keys())) if (key.startsWith(`${pluginId}:`)) secrets.delete(key); }, }; const packageStorage = { @@ -289,7 +289,7 @@ describe("EdgeEverPluginHost", () => { put: async (value) => packages.set(`${value.pluginId}:${value.version}`, value), remove: async (pluginId, version) => { if (version) packages.delete(`${pluginId}:${version}`); - else for (const key of [...packages.keys()]) if (key.startsWith(`${pluginId}:`)) packages.delete(key); + else for (const key of Array.from(packages.keys())) if (key.startsWith(`${pluginId}:`)) packages.delete(key); }, }; const secretStorage = { @@ -331,7 +331,7 @@ describe("EdgeEverPluginHost", () => { put: async (value) => packages.set(`${value.pluginId}:${value.version}`, value), remove: async (id, version) => { if (version) packages.delete(`${id}:${version}`); - else for (const key of [...packages.keys()]) if (key.startsWith(`${id}:`)) packages.delete(key); + else for (const key of Array.from(packages.keys())) if (key.startsWith(`${id}:`)) packages.delete(key); }, }; const host = new EdgeEverPluginHost({ repository, scope: "test", packageStorage }); diff --git a/apps/web/src/lib/plugins/plugin-host.ts b/apps/web/src/lib/plugins/plugin-host.ts index 1da4efbd..0ccc5e5d 100644 --- a/apps/web/src/lib/plugins/plugin-host.ts +++ b/apps/web/src/lib/plugins/plugin-host.ts @@ -1047,24 +1047,24 @@ export class EdgeEverPluginHost { try { dispose(); } catch { /* A broken disposer must not strand the plugin. */ } } try { await active.plugin.deactivate?.(); } catch { /* Cleanup must continue after a broken deactivate hook. */ } - for (const key of [...this.commands.keys()]) { + for (const key of Array.from(this.commands.keys())) { if (key.startsWith(`${pluginId}:`)) this.commands.delete(key); } - for (const key of [...this.panels.keys()]) { + for (const key of Array.from(this.panels.keys())) { if (key.startsWith(`${pluginId}:`)) this.panels.delete(key); } - for (const key of [...this.embeds.keys()]) { + for (const key of Array.from(this.embeds.keys())) { if (key.startsWith(`${pluginId}:`)) this.embeds.delete(key); } - for (const [key, disposers] of [...this.mountedPanels]) { + for (const [key, disposers] of Array.from(this.mountedPanels)) { if (!key.startsWith(`${pluginId}:`)) continue; - for (const dispose of [...disposers]) { + for (const dispose of Array.from(disposers)) { try { dispose(); } catch { /* Panel cleanup must not strand the plugin. */ } } } - for (const [key, embedDisposers] of [...this.mountedEmbeds]) { + for (const [key, embedDisposers] of Array.from(this.mountedEmbeds)) { if (!key.startsWith(`${pluginId}:`)) continue; - for (const dispose of [...embedDisposers]) { + for (const dispose of Array.from(embedDisposers)) { try { dispose(); } catch { /* Embed cleanup must not strand the plugin. */ } } } @@ -1618,7 +1618,7 @@ export class EdgeEverPluginHost { this.refreshSnapshot(); const dispose = () => { this.embeds.delete(key); - for (const mountedDispose of [...(this.mountedEmbeds.get(key) ?? [])]) mountedDispose(); + for (const mountedDispose of Array.from(this.mountedEmbeds.get(key) ?? [])) mountedDispose(); this.refreshSnapshot(); }; disposers.push(dispose); diff --git a/apps/web/src/lib/wechat-copy.test.mjs b/apps/web/src/lib/wechat-copy.test.mjs index 01fb20e1..1bdc8e30 100644 --- a/apps/web/src/lib/wechat-copy.test.mjs +++ b/apps/web/src/lib/wechat-copy.test.mjs @@ -58,7 +58,7 @@ const MARKDOWN = ` - **行内公式**:例如质能方程 $E = mc^2$。 - **多行独立公式块**: $$ -\oint_{\\partial \\Omega} \\mathbf{E} \\cdot d\\mathbf{S} = \\frac{1}{\\varepsilon_0} \\iiint_{\\Omega} \\rho \\, dV +\\oint_{\\partial \\Omega} \\mathbf{E} \\cdot d\\mathbf{S} = \\frac{1}{\\varepsilon_0} \\iiint_{\\Omega} \\rho \\, dV $$ - [x] 已完成的待办 diff --git a/bun.lock b/bun.lock index 49a3d89b..86260805 100644 --- a/bun.lock +++ b/bun.lock @@ -84,6 +84,7 @@ "fake-indexeddb": "^6.2.5", "google-auth-library": "^11.0.2", "linkedom": "0.18.13", + "oxlint": "1.88.0", "tailwindcss": "^4.3.2", "tw-animate-css": "^1.4.0", "typescript": "6.0.3", @@ -101,7 +102,7 @@ }, "apps/desktop": { "name": "@edgeever/desktop", - "version": "1.112.0", + "version": "1.120.0", "dependencies": { "@agentclientprotocol/sdk": "^1.5.1", "@edgeever/public-network": "workspace:*", @@ -120,17 +121,17 @@ "name": "@edgeever/extension", "version": "0.1.17", "dependencies": { + "@joplin/turndown-plugin-gfm": "1.0.68", "@mixmark-io/domino": "2.2.0", "@mozilla/readability": "^0.6.0", "marked": "^18.0.5", + "mdast-util-from-markdown": "2.0.3", + "mdast-util-gfm": "3.1.0", + "micromark-extension-gfm": "3.0.0", "react": "~19.2.7", "react-dom": "~19.2.7", "sonner": "2.0.8", "turndown": "^7.2.4", - "@joplin/turndown-plugin-gfm": "1.0.68", - "mdast-util-from-markdown": "2.0.3", - "mdast-util-gfm": "3.1.0", - "micromark-extension-gfm": "3.0.0", }, "devDependencies": { "web-ext": "^10.7.0", @@ -331,9 +332,9 @@ }, "patchedDependencies": { "expo-sharing@57.0.22": "patches/expo-sharing@57.0.22.patch", + "@joplin/turndown-plugin-gfm@1.0.68": "patches/@joplin%2Fturndown-plugin-gfm@1.0.68.patch", "mermaid@11.16.1": "patches/mermaid@11.16.1.patch", "npm@11.21.0": "patches/npm@11.21.0.patch", - "@joplin/turndown-plugin-gfm@1.0.68": "patches/@joplin%2Fturndown-plugin-gfm@1.0.68.patch", }, "overrides": { "dompurify": "3.4.16", @@ -1174,6 +1175,44 @@ "@oxc-project/types": ["@oxc-project/types@0.153.0", "", {}, "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.88.0", "", { "os": "android", "cpu": "arm" }, "sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.88.0", "", { "os": "android", "cpu": "arm64" }, "sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.88.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.88.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.88.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.88.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.88.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.88.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.88.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.88.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.88.0", "", { "os": "linux", "cpu": "none" }, "sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.88.0", "", { "os": "linux", "cpu": "none" }, "sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.88.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.88.0", "", { "os": "linux", "cpu": "x64" }, "sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.88.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.88.0", "", { "os": "none", "cpu": "arm64" }, "sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.88.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.88.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.88.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g=="], + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="], "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], @@ -3452,6 +3491,8 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "oxlint": ["oxlint@1.88.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.88.0", "@oxlint/binding-android-arm64": "1.88.0", "@oxlint/binding-darwin-arm64": "1.88.0", "@oxlint/binding-darwin-x64": "1.88.0", "@oxlint/binding-freebsd-x64": "1.88.0", "@oxlint/binding-linux-arm-gnueabihf": "1.88.0", "@oxlint/binding-linux-arm-musleabihf": "1.88.0", "@oxlint/binding-linux-arm64-gnu": "1.88.0", "@oxlint/binding-linux-arm64-musl": "1.88.0", "@oxlint/binding-linux-ppc64-gnu": "1.88.0", "@oxlint/binding-linux-riscv64-gnu": "1.88.0", "@oxlint/binding-linux-riscv64-musl": "1.88.0", "@oxlint/binding-linux-s390x-gnu": "1.88.0", "@oxlint/binding-linux-x64-gnu": "1.88.0", "@oxlint/binding-linux-x64-musl": "1.88.0", "@oxlint/binding-openharmony-arm64": "1.88.0", "@oxlint/binding-win32-arm64-msvc": "1.88.0", "@oxlint/binding-win32-ia32-msvc": "1.88.0", "@oxlint/binding-win32-x64-msvc": "1.88.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], diff --git a/package.json b/package.json index b6c2d771..2820b9ed 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "build:site": "bun --cwd apps/site build", "preview:web": "vite preview --config apps/web/vite.config.ts --host 127.0.0.1 --port 4173", "preview:site": "bun --cwd apps/site preview", + "lint": "oxlint --deny-warnings apps packages", "typecheck": "tsc -p tsconfig.json --noEmit", "typecheck:mobile": "bun run --cwd apps/mobile typecheck", "typecheck:ios-editor": "bun run --cwd apps/ios/EditorSource typecheck", @@ -199,6 +200,7 @@ "fake-indexeddb": "^6.2.5", "google-auth-library": "^11.0.2", "linkedom": "0.18.13", + "oxlint": "1.88.0", "tailwindcss": "^4.3.2", "tw-animate-css": "^1.4.0", "typescript": "6.0.3", diff --git a/packages/shared/src/details.ts b/packages/shared/src/details.ts index 969f6b08..79f453f7 100644 --- a/packages/shared/src/details.ts +++ b/packages/shared/src/details.ts @@ -91,6 +91,7 @@ const escapeMarkdownLabel = (value: string) => /** http(s) URLs that are not already stored EdgeEver resources. */ const isRemoteHttpUrl = (value: string) => { + // oxlint-disable-next-line no-control-regex -- rejects URLs containing control characters or whitespace if (!value || /[\u0000-\u0020"'<>`]/.test(value)) return false; if (getResourceIdFromUrl(value)) return false; try { @@ -102,6 +103,7 @@ const isRemoteHttpUrl = (value: string) => { }; const isDetailsImageSrc = (value: string) => { + // oxlint-disable-next-line no-control-regex -- rejects image sources containing control characters or whitespace if (!value || /[\u0000-\u0020"'<>`]/.test(value)) return false; if (value.startsWith("edgeever-resource://") || value.startsWith("edgeever-staged://")) return true; if (value.startsWith("/") && !value.startsWith("//")) return true; @@ -135,7 +137,7 @@ const remoteVideoFromSrc = (src: string): RemoteVideo | null => { } catch { segment = ""; } - if (!segment || segment.length > 120 || /[\\/\[\]()<>]/.test(segment)) segment = ""; + if (!segment || segment.length > 120 || /[\\/[\]()<>]/.test(segment)) segment = ""; const filename = segment || "video"; return { src: url, @@ -320,7 +322,7 @@ export const wrapDetailsContentHtml = (html: string): string => { const document = new DOMParser().parseFromString(html, "text/html"); const blocks = [...document.querySelectorAll("details")].reverse(); for (const details of blocks) { - for (const video of [...details.querySelectorAll("video")]) { + for (const video of Array.from(details.querySelectorAll("video"))) { if (video.closest("[data-type='edgeever-file-attachment'], [data-edgeever-video-player]")) continue; const parsed = remoteVideoFromSrc(video.getAttribute("src") || ""); if (!parsed) { @@ -332,7 +334,7 @@ export const wrapDetailsContentHtml = (html: string): string => { if (details.querySelector(':scope > [data-type="detailsContent"]')) continue; const wrapper = document.createElement("div"); wrapper.setAttribute("data-type", "detailsContent"); - for (const child of [...details.childNodes]) { + for (const child of Array.from(details.childNodes)) { if (child.nodeName === "SUMMARY") continue; wrapper.appendChild(child); } @@ -367,7 +369,7 @@ export const flattenDetailsForLinearHtml = (root: HTMLElement) => { heading.appendChild(strong); replacement.appendChild(heading); } - for (const child of [...details.childNodes]) { + for (const child of Array.from(details.childNodes)) { if (child === summary) continue; if (child instanceof HTMLElement && child.getAttribute("data-type") === "detailsContent") { while (child.firstChild) { diff --git a/packages/shared/src/diagram-node-presentation.ts b/packages/shared/src/diagram-node-presentation.ts index 2b257b72..58304815 100644 --- a/packages/shared/src/diagram-node-presentation.ts +++ b/packages/shared/src/diagram-node-presentation.ts @@ -1,6 +1,7 @@ import type { DiagramNodeShape } from "./diagram"; export const visualTextUnits = (label: string) => Array.from(label).reduce( + // oxlint-disable-next-line no-control-regex -- tests for characters outside Latin-1 (total, character) => total + (/[^\u0000-\u00ff]/.test(character) ? 1 : 0.55), 0, ); diff --git a/packages/shared/src/diagram-operations.ts b/packages/shared/src/diagram-operations.ts index 5e91abb8..148f4472 100644 --- a/packages/shared/src/diagram-operations.ts +++ b/packages/shared/src/diagram-operations.ts @@ -394,7 +394,7 @@ export const applyDiagramOperations = async ( } if (current.kind === "architecture" && affectedBoundaryIds.size > 0) { const nodeById = new Map(next.nodes.map((node) => [node.id, node])); - for (const boundaryId of [...affectedBoundaryIds]) { + for (const boundaryId of Array.from(affectedBoundaryIds)) { let parentId = nodeById.get(boundaryId)?.parentId; while (parentId) { affectedBoundaryIds.add(parentId); diff --git a/packages/shared/src/diagram-view.ts b/packages/shared/src/diagram-view.ts index 18556b9e..53120af2 100644 --- a/packages/shared/src/diagram-view.ts +++ b/packages/shared/src/diagram-view.ts @@ -145,7 +145,7 @@ export const diagramDocumentToX6Cells = ( } : {}), }, ...(mindVisual ? { underline: mindVisual.underline } : {}), - ...(architectureVisual?.attrs ?? {}), + ...architectureVisual?.attrs, }, }; }); diff --git a/packages/shared/src/note-image-card.ts b/packages/shared/src/note-image-card.ts index 3d826257..df26a6af 100644 --- a/packages/shared/src/note-image-card.ts +++ b/packages/shared/src/note-image-card.ts @@ -510,6 +510,7 @@ export const resolveTheme = (background?: NoteImageBackground, theme?: NoteImage export const buildImageExportBasename = (title: string, fallback: string) => { const sanitized = title + // oxlint-disable-next-line no-control-regex -- replaces control characters in export filenames .replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-") .replace(/\s+/g, " ") .replace(/[. ]+$/g, "") diff --git a/packages/shared/src/poster-operations.ts b/packages/shared/src/poster-operations.ts index 4a6802cb..8aa425af 100644 --- a/packages/shared/src/poster-operations.ts +++ b/packages/shared/src/poster-operations.ts @@ -69,7 +69,7 @@ export const posterPatchSchema = z.object({ radius: z.number().min(0).max(4096).optional(), resourceId: z.string().min(1).max(160).regex(/^[\w-]+$/).optional(), crop: z.object({ x: z.number().min(0).max(1), y: z.number().min(0).max(1), width: z.number().min(0.01).max(1), height: z.number().min(0.01).max(1) }).strict().refine(crop => crop.x + crop.width <= 1 && crop.y + crop.height <= 1).nullable().optional(), - path: z.string().min(1).max(100000).regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+\-]+$/).optional(), + path: z.string().min(1).max(100000).regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+-]+$/).optional(), text: z.string().max(8000).optional(), fill: z.string().regex(/^#[0-9a-f]{6}$/i).optional(), fontSize: z.number().finite().min(4).max(8192).optional(), diff --git a/packages/shared/src/poster.ts b/packages/shared/src/poster.ts index 0ba164b3..9c746141 100644 --- a/packages/shared/src/poster.ts +++ b/packages/shared/src/poster.ts @@ -129,7 +129,7 @@ const path = z.object({ .string() .min(1) .max(100_000) - .regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+\-]+$/), + .regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+-]+$/), }); export const PosterElementSchema = z.union([text, shape, image, path]); export const PosterDocumentSchema = z @@ -294,7 +294,7 @@ export const posterFallbackMarkdown = (document: PosterDocument) => : "", ...document.elements .filter((element): element is PosterText => element.type === "text") - .map((element) => element.text.replace(/([\\`*_{}\[\]<>#!])/g, "\\$1")), + .map((element) => element.text.replace(/([\\`*_{}[\]<>#!])/g, "\\$1")), document.sourceMemoId ? `[Source](#memo=${document.sourceMemoId})` : "", ...document.elements .filter((element) => element.type === "image") diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index 7bd1c62d..aa7d5e5f 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -348,6 +348,7 @@ export const AiDefaultModelUpdateSchema = z.object({ }); export const AiAttachmentSchema = z.object({ + // oxlint-disable-next-line no-control-regex -- rejects control characters in attachment filenames filename: z.string().trim().min(1).max(255).regex(/^[^\u0000-\u001F\u007F]+$/), mediaType: z.enum(AI_ATTACHMENT_MEDIA_TYPES), base64Data: z.string().min(1).max(Math.ceil(MAX_AI_ATTACHMENT_BYTES / 3) * 4),