From 85dcf04fcae7e7d485bc579ea7c56b5fc834ea37 Mon Sep 17 00:00:00 2001 From: Erik Brinkman Date: Sat, 6 Jun 2026 15:30:31 -0400 Subject: [PATCH] feat: replace Next.js options page with a bun-bundled SPA The options page was the only remaining use of Next.js. Its Pages-Router static export loaded under MV3's `script-src 'self'` CSP only because every script was an external self-hosted file; the App Router emits inline RSC bootstrap scripts the CSP rejects, so there was no path to modernizing onto it. Drop Next.js entirely and build the page with `bun build`, like the other entry points. `options.html` is the single source of truth: it references `src/options-main.tsx` directly, so `bun dev` serves it with HMR and `bun build ./options.html` derives the production `out/` (hashed JS/CSS/SVG/favicon + rewritten HTML). The result has no chunks and no inline scripts. - pages/index.tsx -> components/options-page.tsx, mounted by src/options-main.tsx via createRoot - next/font/google -> @fontsource (woff2 inlined as data URIs by bun) - next/head -> options.html shell; next/image / StaticImage -> with the logo imported through the bundler as a hashed asset - pack zips the self-contained out/ dir instead of a hand-listed glob - remove next, @next/eslint-plugin-next, next.config.js, pages/, styles.css, the assetPrefix "/out" hack and stale tsconfig entries - drop 28 dead eslint-disable comments (no eslint is configured) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 1 - .gitignore | 5 +- biome.json | 2 +- bun.lockb | Bin 120849 -> 95842 bytes .../index.tsx => components/options-page.tsx | 80 +++--------------- components/static-image.tsx | 19 ----- manifest.json | 2 +- next.config.js | 6 -- options.html | 23 +++++ package.json | 17 ++-- pages/_app.ts | 3 - src/alter.ts | 3 - src/cli.ts | 3 - src/globals.d.ts | 6 ++ src/image.ts | 2 - src/lib.ts | 1 - src/mhtml.ts | 1 - src/options-main.tsx | 9 ++ src/options.ts | 4 - src/parse.ts | 1 - src/utils.spec.ts | 1 - styles.css | 8 -- tsconfig.json | 18 +--- 23 files changed, 67 insertions(+), 148 deletions(-) rename pages/index.tsx => components/options-page.tsx (93%) delete mode 100644 components/static-image.tsx delete mode 100644 next.config.js create mode 100644 options.html delete mode 100644 pages/_app.ts create mode 100644 src/globals.d.ts create mode 100644 src/options-main.tsx delete mode 100644 styles.css diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73ac9db..9cc28e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,4 +16,3 @@ jobs: - run: bun lint - run: bun test - run: bun export:bundle - - run: bun export:options diff --git a/.gitignore b/.gitignore index a4b2812..0e7d370 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ coverage node_modules *.tsbuildinfo -.eslintcache -.next # Artifacts # ############# @@ -20,5 +18,4 @@ node_modules /scripts /target /pkg -/images -/favicon.ico \ No newline at end of file +/images \ No newline at end of file diff --git a/biome.json b/biome.json index 118230c..95cd9b9 100644 --- a/biome.json +++ b/biome.json @@ -8,7 +8,7 @@ "files": { "includes": [ "src/*.ts", - "pages/*.tsx", + "src/*.tsx", "components/*.tsx", "*.{json,config.js}", "*.html" diff --git a/bun.lockb b/bun.lockb index ca8201c95e139775387e613d39cb7ff654b31987..08f943eab57fcf369f0abac179f3269d1e214e89 100755 GIT binary patch delta 17180 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 delta 32439 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 diff --git a/pages/index.tsx b/components/options-page.tsx similarity index 93% rename from pages/index.tsx rename to components/options-page.tsx index 640997e..5746963 100644 --- a/pages/index.tsx +++ b/components/options-page.tsx @@ -1,4 +1,3 @@ -// NOTE import from cjs here due to the way that nextjs handles internal es6 modules import styled from "@emotion/styled"; import ContentPasteIcon from "@mui/icons-material/ContentPaste"; import Alert, { type AlertColor } from "@mui/material/Alert"; @@ -18,13 +17,6 @@ import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { marked } from "marked"; -import { - EB_Garamond, - Noto_Sans, - Noto_Sans_Mono, - Noto_Serif, -} from "next/font/google"; -import Head from "next/head"; import { type ChangeEvent, type ReactElement, @@ -54,14 +46,7 @@ import { FaXmark, } from "react-icons/fa6"; import { register } from "rmapi-js"; -import ButtonSelection from "../components/button-selection"; -import CheckboxSelection from "../components/checkbox-selection"; -import FormControlLabel from "../components/form-control-label"; -import LeftRight from "../components/left-right"; -import RadioSelection from "../components/radio-selection"; -import Right from "../components/right"; -import Section from "../components/section"; -import StaticImage from "../components/static-image"; +import repubPlain from "../public/repub-plain.svg"; import { toMhtml } from "../src/mhtml"; import { defaultOptions, @@ -74,23 +59,13 @@ import { import { render } from "../src/render"; import { uploadEpub, uploadPdf } from "../src/upload"; import { sleep } from "../src/utils"; - -const ebGaramond = EB_Garamond({ - weight: "400", - subsets: ["latin"], -}); -const notoSans = Noto_Sans({ - weight: "400", - subsets: ["latin"], -}); -const notoSerif = Noto_Serif({ - weight: "400", - subsets: ["latin"], -}); -const notoMono = Noto_Sans_Mono({ - weight: "400", - subsets: ["latin"], -}); +import ButtonSelection from "./button-selection"; +import CheckboxSelection from "./checkbox-selection"; +import FormControlLabel from "./form-control-label"; +import LeftRight from "./left-right"; +import RadioSelection from "./radio-selection"; +import Right from "./right"; +import Section from "./section"; const theme = createTheme({ palette: { @@ -109,7 +84,6 @@ const unknownOpts: Partial = Object.fromEntries( async function getToken(code: string, authHost: string): Promise { // This is for testing in dev mode - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (window.chrome?.runtime?.id) { return await register(code, { authHost }); } else { @@ -252,7 +226,6 @@ function FileUpload({
- + repub , }, @@ -1056,65 +1021,55 @@ function FontNameSelector({ }} selections={[ { - // eslint-disable-next-line spellcheck/spell-checker val: "EB Garamond", icon: ( EB Garamond ), }, { - // eslint-disable-next-line spellcheck/spell-checker val: "Noto Sans", icon: ( Noto Sans ), }, { - // eslint-disable-next-line spellcheck/spell-checker val: "Noto Serif", icon: ( Noto Serif ), }, { - // eslint-disable-next-line spellcheck/spell-checker val: "Noto Mono", icon: ( Noto Mono ), }, { - // eslint-disable-next-line spellcheck/spell-checker val: "Noto Sans UI", icon: ( Noto Sans UI @@ -1291,7 +1246,6 @@ function ApiUrlsSection({ // check permissions for each non-default URL individually useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!globalThis.chrome?.permissions) return; let stale = false; // deduplicate origins so we only call contains once per unique origin @@ -1470,11 +1424,6 @@ export default function OptionsPage(): ReactElement { return ( - - reMarkable ePub Options - {/* eslint-disable-next-line spellcheck/spell-checker */} - - - {/* eslint-disable-next-line spellcheck/spell-checker */} diff --git a/components/static-image.tsx b/components/static-image.tsx deleted file mode 100644 index 70bcd9e..0000000 --- a/components/static-image.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Image, { type ImageLoaderProps, type ImageProps } from "next/image"; -import type { ReactElement } from "react"; - -function staticLoader({ src }: ImageLoaderProps): string { - return src; -} - -interface StaticImageProps extends ImageProps { - alt: string; - loader?: never; - unoptimized?: never; -} - -export default function StaticImage({ - alt, - ...props -}: StaticImageProps): ReactElement { - return {alt}; -} diff --git a/manifest.json b/manifest.json index a3ca96d..817ada0 100644 --- a/manifest.json +++ b/manifest.json @@ -19,7 +19,7 @@ "https://eu.tectonic.remarkable.com/" ], "optional_host_permissions": ["https://*/"], - "options_page": "out/index.html", + "options_page": "out/options.html", "action": { "default_title": "convert page to epub" }, diff --git a/next.config.js b/next.config.js deleted file mode 100644 index 7f77735..0000000 --- a/next.config.js +++ /dev/null @@ -1,6 +0,0 @@ -// This allows the assets to be reached by chrome -export default { - assetPrefix: process.env.NODE_ENV === "production" ? "/out" : "/", - images: { loader: "custom" }, - output: "export", -}; diff --git a/options.html b/options.html new file mode 100644 index 0000000..d2b99b3 --- /dev/null +++ b/options.html @@ -0,0 +1,23 @@ + + + + + + reMarkable ePub Options + + + + +
+ + + diff --git a/package.json b/package.json index 6b5e8f6..9da566c 100644 --- a/package.json +++ b/package.json @@ -8,24 +8,26 @@ "type": "module", "scripts": { "cli": "bun run src/cli.ts", - "dev": "next dev", + "dev": "bun ./options.html", "fmt": "biome format --write", "lint": "tsc && biome check", - "export:images": "mkdir -p images && <<< $'16\n32\n48\n128' xargs -I@ rsvg-convert -w @ -h @ public/repub.svg -o images/repub_@.png && magick images/repub_*.png favicon.ico", - "export:bundle": "bun build src/background.ts src/offscreen.ts src/popup.ts --outdir . --sourcemap=linked", - "export:options": "next build", - "export:pack": "zip -r --filesync repub.zip background.js background.js.map favicon.ico images/*.png manifest.json offscreen.html popup.html popup.js popup.js.map offscreen.js offscreen.js.map out", - "export": "bun export:images && bun export:bundle && bun export:options && bun export:pack", + "export:images": "mkdir -p images && <<< $'16\n32\n48\n128' xargs -I@ rsvg-convert -w @ -h @ public/repub.svg -o images/repub_@.png", + "export:bundle": "bun build src/background.ts src/offscreen.ts src/popup.ts --outdir . --sourcemap=linked && bun build ./options.html --outdir out --sourcemap=linked", + "export:pack": "zip -r --filesync repub.zip background.js background.js.map images/*.png manifest.json offscreen.html popup.html popup.js popup.js.map offscreen.js offscreen.js.map out", + "export": "bun export:images && bun export:bundle && bun export:pack", "pack": "bun lint && bun test --coverage && bun export && publint" }, "devDependencies": { "@biomejs/biome": "^2.4.15", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@fontsource/eb-garamond": "^5.2.7", + "@fontsource/noto-sans": "^5.2.10", + "@fontsource/noto-sans-mono": "^5.2.10", + "@fontsource/noto-serif": "^5.2.9", "@mozilla/readability": "^0.6.0", "@mui/icons-material": "^9.0.1", "@mui/material": "^9.0.1", - "@next/eslint-plugin-next": "^16.2.6", "@types/base64-js": "^1.5.0", "@types/bun": "^1.3.14", "@types/chrome": "^0.1.42", @@ -39,7 +41,6 @@ "leven": "^4.1.0", "marked": "^18.0.3", "mhtml-stream": "^2.0.2", - "next": "^16.2.6", "publint": "^0.3.21", "react": "^19.2.6", "react-dom": "^19.2.6", diff --git a/pages/_app.ts b/pages/_app.ts deleted file mode 100644 index cc247fd..0000000 --- a/pages/_app.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "../styles.css"; -import OptionsPage from "."; -export default OptionsPage; diff --git a/src/alter.ts b/src/alter.ts index 9ec80ad..b9a8d3c 100644 --- a/src/alter.ts +++ b/src/alter.ts @@ -26,7 +26,6 @@ export function exactMatch(assetData: Map): UrlMatcher { }; } -// eslint-disable-next-line spellcheck/spell-checker /** * looks for close matches * @@ -145,7 +144,6 @@ class Walker { const rep = `${encoded}`; let url = this.svgs.get(rep); if (url === undefined) { - // eslint-disable-next-line spellcheck/spell-checker url = `inlinesvg://${this.svgs.size}.svg`; this.svgs.set(rep, url); } @@ -217,7 +215,6 @@ ${this.options.tableCss}`; const blob = await canvas.convertToBlob({ type: "image/png" }); const bytes = await blob.arrayBuffer(); - // eslint-disable-next-line spellcheck/spell-checker const url = `tableimage://${this.pngs.length}.png`; this.pngs.push([url, new Uint8Array(bytes)]); diff --git a/src/cli.ts b/src/cli.ts index 663f903..b2e8ccc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -78,7 +78,6 @@ global.Image = dom.window["Image"] as typeof Image; type: "string", }) .option("img-sim-thresh", { - // eslint-disable-next-line spellcheck/spell-checker describe: "how similar image urls need to be to use them", type: "number", default: defaultOptions.imageHrefSimilarityThreshold, @@ -94,9 +93,7 @@ global.Image = dom.window["Image"] as typeof Image; type: "boolean", default: defaultOptions.filterLinks, }) - // eslint-disable-next-line spellcheck/spell-checker .option("filter-iframes", { - // eslint-disable-next-line spellcheck/spell-checker describe: "filter iframes from generated results", type: "boolean", default: defaultOptions.filterIframes, diff --git a/src/globals.d.ts b/src/globals.d.ts new file mode 100644 index 0000000..9432dff --- /dev/null +++ b/src/globals.d.ts @@ -0,0 +1,6 @@ +declare module "*.css"; + +declare module "*.svg" { + const src: string; + export default src; +} diff --git a/src/image.ts b/src/image.ts index ccefd24..2d3a0ac 100644 --- a/src/image.ts +++ b/src/image.ts @@ -37,11 +37,9 @@ export async function brighten( // create canvas, write to it, and brighten it const canvas = new OffscreenCanvas(width, height); const ctx = canvas.getContext("2d")!; - /* eslint-disable spellcheck/spell-checker */ ctx.filter = ` brightness(${brightness * 100}%) grayscale(${+grayscale * 100}%)`; - /* eslint-enable spellcheck/spell-checker */ ctx.drawImage(bitmap, 0, 0, width, height); // export result diff --git a/src/lib.ts b/src/lib.ts index 3ac395f..7cc9337 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -3,7 +3,6 @@ import { epub, type ImageData, type ImageMime } from "./epub"; import type { EpubOptions } from "./options"; import { parse } from "./parse"; -// eslint-disable-next-line spellcheck/spell-checker const remarkableCss = ` p { margin-top: 1em; diff --git a/src/mhtml.ts b/src/mhtml.ts index 9c71d87..78f4601 100644 --- a/src/mhtml.ts +++ b/src/mhtml.ts @@ -1,4 +1,3 @@ -/* eslint-disable spellcheck/spell-checker */ import { fromByteArray } from "base64-js"; import { v4 as uuidv4 } from "uuid"; diff --git a/src/options-main.tsx b/src/options-main.tsx new file mode 100644 index 0000000..59a2a3a --- /dev/null +++ b/src/options-main.tsx @@ -0,0 +1,9 @@ +import "@fontsource/eb-garamond/latin-400.css"; +import "@fontsource/noto-sans/latin-400.css"; +import "@fontsource/noto-serif/latin-400.css"; +import "@fontsource/noto-sans-mono/latin-400.css"; +import { createRoot } from "react-dom/client"; +import OptionsPage from "../components/options-page"; + +const container = document.getElementById("root")!; +createRoot(container).render(); diff --git a/src/options.ts b/src/options.ts index f81bb09..f6a4d6f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -64,7 +64,6 @@ export interface UploadOptions { lineHeight: number; tags: string; textAlignment: "left" | "justify"; - // eslint-disable-next-line spellcheck/spell-checker viewBackgroundFilter: "off" | "fullpage" | null; authHost: string; syncHost: string; @@ -114,7 +113,6 @@ export const defaultOptions: Options = { // Upload // // ------ // coverPageNumber: -1, - // eslint-disable-next-line spellcheck/spell-checker fontName: "EB Garamond", margins: 125, textScale: 1, @@ -134,7 +132,6 @@ export const defaultOptions: Options = { }; export async function getOptions({ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition storage = globalThis.chrome?.storage?.local ?? mockStorage, }: { storage?: Storage; @@ -148,7 +145,6 @@ export type SetOptions = (options: Partial) => void; export async function setOptions( opts: Partial, { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition storage = globalThis.chrome?.storage?.local ?? mockStorage, }: { storage?: Storage } = {}, ): Promise { diff --git a/src/parse.ts b/src/parse.ts index 644c6f4..e9bbd3f 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -17,7 +17,6 @@ export interface ParsedWebpage { readonly assets: AsyncIterableIterator; } -// eslint-disable-next-line @typescript-eslint/require-await async function* asIterable( data: Uint8Array, ): AsyncIterableIterator { diff --git a/src/utils.spec.ts b/src/utils.spec.ts index b0998e5..0a7cf4d 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -20,7 +20,6 @@ async function mySleep(seconds: number): Promise { test("timeout()", async () => { expect(await timeout(mySleep(1), 10)).toBeNull(); - // eslint-disable-next-line @typescript-eslint/await-thenable,@typescript-eslint/no-confusing-void-expression await expect(timeout(mySleep(10), 1)).rejects.toThrow("timeout"); }); diff --git a/styles.css b/styles.css deleted file mode 100644 index c806edb..0000000 --- a/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -html { - overflow-y: scroll; - background-color: rgb(250, 246, 240); -} - -body { - margin: 0; -} diff --git a/tsconfig.json b/tsconfig.json index 9c6d155..b137afe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "incremental": true, "target": "esnext", "moduleResolution": "bundler", - "types": ["chrome", "bun", "node", "next"], + "types": ["chrome", "bun", "node"], "noEmit": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, @@ -17,20 +17,8 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, - "skipLibCheck": true, - "plugins": [ - { - "name": "next" - } - ] + "skipLibCheck": true }, - "include": [ - "src", - "content", - "pages", - "components", - "next-env.d.ts", - ".next/types/**/*.ts" - ], + "include": ["src", "components"], "exclude": ["node_modules"] }