openreader/eslint.config.mjs
Richard R a56aaa2be9 Refactor PDF parsing and rendering logic; migrate to compute-worker protocol
- Removed deprecated functions related to document parsing and blob storage in blobstore.ts.
- Introduced new PDF rendering logic in pdf-preview-renderer.ts and pdf-preview-pdfjs-runtime.ts.
- Updated previews-render.ts to utilize the new PDF rendering functions.
- Refactored user-whisper-align-job.ts to use the compute-worker client for alignment requests.
- Enhanced artifact.ts and operation.ts to validate parsed PDF artifacts and resolve current PDF parses.
- Updated snapshot.ts to align with new worker operation types.
- Adjusted runtime-config.ts to check for compute-worker availability.
- Modified types in parsed-pdf.ts and tts.ts to reflect changes in the compute-worker protocol.
- Added unit tests for PDF artifact validation and compute-worker client contract.
- Removed obsolete pdf-op-key.vitest.spec.ts test file.
2026-06-12 13:43:33 -06:00

224 lines
9.3 KiB
JavaScript

import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const LOGGER_LEVEL_METHOD = "trace|debug|info|warn|error|fatal";
const STATIC_LOGGER_CALL_SELECTOR =
`CallExpression[callee.type='MemberExpression'][callee.computed=false][callee.property.name=/^(${LOGGER_LEVEL_METHOD})$/]`;
const DYNAMIC_LOGGER_CALL_SELECTOR =
"CallExpression[callee.type='MemberExpression'][callee.computed=true]";
const LOGGER_RECEIVER_SELECTOR =
":matches([callee.object.name=/^(logger|serverLogger)$/],[callee.object.property.name='logger'])";
const SERVER_LOGGER_CALL_SELECTOR = `:matches(${STATIC_LOGGER_CALL_SELECTOR}${LOGGER_RECEIVER_SELECTOR},${DYNAMIC_LOGGER_CALL_SELECTOR}${LOGGER_RECEIVER_SELECTOR})`;
const NEXT_RESPONSE_ERROR_JSON_SELECTOR =
"CallExpression[callee.type='MemberExpression'][callee.object.name='NextResponse'][callee.property.name='json'][arguments.0.type='ObjectExpression']:has(Property[key.name='error'])";
const APP_DESIGN_SYSTEM_FILES = [
"src/app/(app)/**/*.{ts,tsx}",
"src/components/**/*.{ts,tsx}",
];
const UI_ARCHITECTURE_FILES = [
"src/app/(app)/**/*.{ts,tsx}",
"src/components/auth/**/*.{ts,tsx}",
"src/components/doclist/**/*.{ts,tsx}",
"src/components/documents/DocumentUploader.tsx",
"src/components/documents/DocumentSelectionModal.tsx",
"src/components/documents/DexieMigrationModal.tsx",
"src/components/documents/ZoomControl.tsx",
"src/components/player/**/*.{ts,tsx}",
"src/components/reader/**/*.{ts,tsx}",
];
const COMPUTE_WORKER_IMPORT_PATTERNS = [
{
group: [
"@openreader/compute-worker",
"@openreader/compute-worker/*",
],
message:
"The app must communicate with compute-worker through the app-owned HTTP client and generated protocol types.",
},
];
const UI_ARCHITECTURE_IMPORT_PATHS = [
{
name: "@/components/formPrimitives",
message:
"Use '@/components/ui' primitives; formPrimitives has been removed.",
},
{
name: "@/components/ui/buttonPrimitives",
message:
"Use '@/components/ui' button exports; buttonPrimitives has been removed.",
},
{
name: "@headlessui/react",
importNames: ["Button", "Input"],
message:
"Use app UI Button/Input primitives; Headless UI should be reserved for structural primitives such as Menu, Listbox, Popover, Dialog, and Transition.",
},
];
const RESTRICTED_APP_CLASS_PATTERNS = [
{
selector: "Literal[value=/(bg|text|border|ring)-(gray|slate|zinc|neutral|stone|red|orange|yellow|amber|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\\d/]",
message:
"Use semantic design-system tokens instead of literal Tailwind palette color classes in app surfaces.",
},
{
selector: "TemplateElement[value.raw=/(bg|text|border|ring)-(gray|slate|zinc|neutral|stone|red|orange|yellow|amber|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\\d/]",
message:
"Use semantic design-system tokens instead of literal Tailwind palette color classes in app surfaces.",
},
{
selector: "Literal[value=/transition-all/]",
message:
"Use scoped transition utilities; transition-all is banned by the app design system.",
},
{
selector: "TemplateElement[value.raw=/transition-all/]",
message:
"Use scoped transition utilities; transition-all is banned by the app design system.",
},
{
selector: "Literal[value=/(bg|text|border|from|via|to)-(accent|background|foreground|base|offbase|muted|secondary-accent|surface|surface-solid|surface-sunken|line|line-soft|line-strong|soft|faint|accent-wash|accent-line|accent-strong|danger|danger-wash)\\/\\d/]",
message:
"Do not use Tailwind alpha modifiers on theme-token colors; use dedicated wash/semantic tokens.",
},
{
selector: "TemplateElement[value.raw=/(bg|text|border|from|via|to)-(accent|background|foreground|base|offbase|muted|secondary-accent|surface|surface-solid|surface-sunken|line|line-soft|line-strong|soft|faint|accent-wash|accent-line|accent-strong|danger|danger-wash)\\/\\d/]",
message:
"Do not use Tailwind alpha modifiers on theme-token colors; use dedicated wash/semantic tokens.",
},
{
selector: "Literal[value=/rounded-\\[[^\\]]*px\\]/]",
message:
"Use the app radius scale instead of arbitrary rounded-[…px] utilities.",
},
{
selector: "TemplateElement[value.raw=/rounded-\\[[^\\]]*px\\]/]",
message:
"Use the app radius scale instead of arbitrary rounded-[…px] utilities.",
},
{
selector: "Literal[value=/duration-\\[[^\\]]*ms\\]/]",
message:
"Use the shared motion duration tokens instead of arbitrary duration-[…ms] utilities.",
},
{
selector: "TemplateElement[value.raw=/duration-\\[[^\\]]*ms\\]/]",
message:
"Use the shared motion duration tokens instead of arbitrary duration-[…ms] utilities.",
},
];
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"no-restricted-imports": [
"error",
{
patterns: COMPUTE_WORKER_IMPORT_PATTERNS,
},
],
},
},
{
files: APP_DESIGN_SYSTEM_FILES,
rules: {
"no-restricted-syntax": ["error", ...RESTRICTED_APP_CLASS_PATTERNS],
},
},
{
files: UI_ARCHITECTURE_FILES,
rules: {
"no-restricted-imports": [
"error",
{
paths: UI_ARCHITECTURE_IMPORT_PATHS,
patterns: COMPUTE_WORKER_IMPORT_PATTERNS,
},
],
},
},
{
files: ["src/app/api/**/*.ts", "src/lib/server/**/*.ts"],
rules: {
"no-console": "error",
"no-restricted-syntax": [
"error",
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.length<2]`,
message:
"Server logger calls must pass context + message: logger.<level>({ event, ...ctx }, 'message').",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type!='ObjectExpression']`,
message:
"Server logger first argument must be an object with an event field.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='Literal']`,
message:
"Server logger first argument must be an object with an event field, not a string literal.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='TemplateLiteral']`,
message:
"Server logger first argument must be an object with an event field, not a template string.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='ObjectExpression']:not(:has(Property[key.name='event']))`,
message:
"Server logger context object must include an event field.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.1.type!='Literal'][arguments.1.type!='TemplateLiteral']`,
message:
"Server logger second argument must be a message string (literal or template literal).",
},
{
selector: `${STATIC_LOGGER_CALL_SELECTOR}[callee.property.name='error']${LOGGER_RECEIVER_SELECTOR}[arguments.0.type='ObjectExpression']:not(:has(ObjectExpression > Property[key.name='error'])):not(:has(ObjectExpression > SpreadElement))`,
message:
"Error-level server logger calls must include nested `error` payload (prefer `error: errorToLog(...)`).",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='detail']`,
message:
"Do not use top-level `detail` in server logger context; keep throwable text under `error.message`.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='errorCode']`,
message:
"Do not use top-level `errorCode` in server logger context; classify failures under nested `error.code`.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='err']`,
message:
"Use `error` (typically from errorToLog(...)) instead of `err` in server logs.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='error'][value.type='Literal']`,
message:
"Server logger `error` must be a structured object (prefer `errorToLog(...)`), not a literal.",
},
{
selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='error'][value.type='TemplateLiteral']`,
message:
"Server logger `error` must be a structured object (prefer `errorToLog(...)`), not template text.",
},
{
selector: `CatchClause ReturnStatement > ${NEXT_RESPONSE_ERROR_JSON_SELECTOR}[arguments.1.type='ObjectExpression']:has(Property[key.name='status'][value.value=500])`,
message:
"Use shared error response helpers (e.g. errorResponse(...)) for terminal 500 route failures instead of direct NextResponse.json({ error }).",
},
],
},
},
];
export default eslintConfig;