From 31684c2dc36c35687ea5c9558d20034db8589097 Mon Sep 17 00:00:00 2001 From: Jeremy Mouton Date: Sat, 7 Nov 2026 14:43:53 +0100 Subject: [PATCH] refactor: remove unused styles and types modules --- .github/workflows/ci.yml | 3 + CHANGELOG.md | 27 +- README.md | 3 +- knip.json | 11 + package.json | 4 +- packages/core/package.json | 4 +- packages/core/src/blocks/blockToNode.ts | 4 +- packages/core/src/blocks/findBlock.ts | 40 ++ packages/core/src/blocks/index.ts | 5 +- packages/core/src/blocks/nodeToBlock.ts | 2 +- packages/core/src/editor/Editor.ts | 33 +- packages/core/src/index.ts | 16 +- packages/core/src/plugins/blockIdPlugin.ts | 17 +- packages/core/src/plugins/bubbleMenuPlugin.ts | 53 +- packages/core/src/plugins/createPlugins.ts | 2 +- packages/core/src/plugins/dragDropPlugin.ts | 102 +-- packages/core/src/plugins/index.ts | 2 +- packages/core/src/plugins/inputRules.ts | 2 +- packages/core/src/plugins/mediaMenuPlugin.ts | 2 +- .../src/plugins/multiBlockSelectionPlugin.ts | 2 +- packages/core/src/plugins/slashMenuPlugin.ts | 188 ++--- packages/core/src/pm/ProseMirrorAPI.ts | 3 +- packages/core/src/schema/createSchema.ts | 2 +- packages/core/src/schema/index.ts | 2 +- packages/core/src/schema/nodes/doc.ts | 2 +- packages/core/src/schema/nodes/index.ts | 1 - packages/core/src/schema/nodes/paragraph.ts | 5 - packages/core/src/schema/sanitizeStyle.ts | 12 +- packages/core/src/styles/editor.css | 50 -- packages/core/src/styles/index.ts | 23 - packages/core/src/styles/injectStyles.ts | 4 +- packages/core/src/types/index.ts | 23 - packages/react/package.json | 3 +- .../react/src/blocks/createReactBlockSpec.tsx | 22 +- packages/react/src/components/BubbleMenu.tsx | 4 +- packages/react/src/components/MediaMenu.tsx | 30 +- .../react/src/components/OpenBlockView.tsx | 2 +- packages/react/src/components/SlashMenu.tsx | 7 +- .../react/src/components/TableHandles.tsx | 94 ++- packages/react/src/components/TableMenu.tsx | 2 +- packages/react/src/components/index.ts | 5 +- packages/react/src/hooks/useOpenBlock.ts | 27 +- pnpm-lock.yaml | 642 ++++++++++++++++-- scripts/check-unused-css.mjs | 73 ++ 44 files changed, 1070 insertions(+), 490 deletions(-) create mode 100644 knip.json create mode 100644 packages/core/src/blocks/findBlock.ts delete mode 100644 packages/core/src/styles/index.ts delete mode 100644 packages/core/src/types/index.ts create mode 100644 scripts/check-unused-css.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f16c522..ab610a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,9 @@ jobs: cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm lint + # Unused files / exports / dependencies (knip) and unused CSS classes + - name: Dead code + run: pnpm deadcode # Build first: the react package and the example type-check against the built core - run: pnpm build - run: pnpm typecheck diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ad539..4aef40e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 GitHub Release; pre-release tags publish under the `next` dist-tag. - Public sanitization helpers: `sanitizeLinkHref`, `sanitizeImageSrc`, `sanitizeEmbedUrl`, `sanitizeCssColor`, `sanitizeTextAlign`, `toNonNegativeNumber`, and `generateId`. -- CI workflow (lint, build, type-check, production dependency audit) and Dependabot configuration. +- `findBlockById(doc, id)` helper (early exit, never walks inline content). +- Multi-block selection API is now public: `createMultiBlockSelectionPlugin`, `MULTI_BLOCK_SELECTION_KEY`, + `selectBlock`, `clearBlockSelection`, `getSelectedBlockPositions`, `isMultiBlockSelectionActive` (+ types). +- `textColorMark`, `backgroundColorMark` and the `HardBreakContent` type are exported from the package root. +- CI workflow (lint, dead code, build, type-check, production dependency audit) and Dependabot configuration. +- `pnpm deadcode`: [knip](https://knip.dev) (unused files, exports, dependencies) + unused CSS class check. + +### Performance +- **React**: `useOpenBlock` only subscribes to `change` / `selectionChange` while `onUpdate` / + `onSelectionChange` are provided. Previously every keystroke converted the whole document to JSON even when + nobody listened (~0.5 ms per keystroke on a 2,000-block document). +- Drag & drop: the block position cache is rebuilt lazily on hover instead of on every transaction; the side + menu is repositioned once per animation frame after edits (no forced synchronous layout per keystroke); + document-level `mousemove` / `mouseup` listeners are only attached while dragging; block DOM lookup uses + `view.nodeDOM()` instead of an attribute selector. +- Block id assignment and block lookups no longer walk inline content (x1.5 / x5 faster on large documents). +- `TableHandles`: table position resolved via `posAtDOM` (no full-document walk), mouse moves outside tables + are ignored when no handle is shown, no state updates on edits while hidden. +- `MediaMenu`: a single passive capture listener for scroll events (was two, hiding the menu twice). +- npm tarball of `@labbs/openblock-core`: 1.2 MB → 520 kB (standalone source maps are only shipped in the + GitHub Release assets). + +### Removed +- Unused code: `styles/index.ts` and `types/index.ts` barrels, the `prosemirror-transform` dependency (never + imported), dead CSS rules (`.ob-text-align-buttons`, `.selectedCell`, table resize handles, `.ob-table-wrapper`, + `.ob-color-picker-indicator`), internal-only exports. ### Changed - Removed the `uuid` runtime dependency (block IDs use the Web Crypto API). diff --git a/README.md b/README.md index 923b005..88d856f 100644 --- a/README.md +++ b/README.md @@ -400,8 +400,9 @@ pnpm install # Build all packages (ESM + CJS + standalone bundles) pnpm build -# Lint and type-check +# Lint, dead code (unused files / exports / deps / CSS classes) and type-check pnpm lint +pnpm deadcode pnpm typecheck # Run example in dev mode diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..210d5d2 --- /dev/null +++ b/knip.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "workspaces": { + "packages/*": { + "project": ["src/**/*.{ts,tsx}"] + }, + "examples/*": { + "project": ["src/**/*.{ts,tsx}"] + } + } +} diff --git a/package.json b/package.json index 221bed9..de017a4 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,14 @@ "lint": "eslint packages/*/src", "test": "pnpm -r test", "clean": "pnpm -r clean", - "typecheck": "pnpm -r typecheck" + "typecheck": "pnpm -r typecheck", + "deadcode": "knip && node scripts/check-unused-css.mjs" }, "devDependencies": { "@eslint/js": "^10.0.1", "eslint": "^10.14.0", "globals": "^17.14.0", + "knip": "^6.44.1", "typescript": "~6.0.3", "typescript-eslint": "^8.72.1" }, diff --git a/packages/core/package.json b/packages/core/package.json index 704bdc7..41490c0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,7 +28,8 @@ "./package.json": "./package.json" }, "files": [ - "dist" + "dist", + "!dist/standalone/*.map" ], "keywords": [ "editor", @@ -59,7 +60,6 @@ "prosemirror-model": "^1.25.12", "prosemirror-schema-list": "^1.6.0", "prosemirror-state": "^1.4.4", - "prosemirror-transform": "^1.12.5", "prosemirror-view": "^1.42.9" }, "devDependencies": { diff --git a/packages/core/src/blocks/blockToNode.ts b/packages/core/src/blocks/blockToNode.ts index 95aa6bf..d9aa256 100644 --- a/packages/core/src/blocks/blockToNode.ts +++ b/packages/core/src/blocks/blockToNode.ts @@ -103,7 +103,7 @@ export function blockToNode(schema: Schema, block: Block): PMNode { * @param content - Array of inline content items * @returns Array of ProseMirror nodes */ -export function inlineContentToNodes(schema: Schema, content: InlineContent[]): PMNode[] { +function inlineContentToNodes(schema: Schema, content: InlineContent[]): PMNode[] { const nodes: PMNode[] = []; for (const item of content) { @@ -143,7 +143,7 @@ export function inlineContentToNodes(schema: Schema, content: InlineContent[]): * @param styles - The text styles object * @returns Array of ProseMirror marks */ -export function stylesToMarks(schema: Schema, styles: TextStyles): readonly Mark[] { +function stylesToMarks(schema: Schema, styles: TextStyles): readonly Mark[] { const marks: Mark[] = []; if (styles.bold && schema.marks.bold) { diff --git a/packages/core/src/blocks/findBlock.ts b/packages/core/src/blocks/findBlock.ts new file mode 100644 index 0000000..09d1083 --- /dev/null +++ b/packages/core/src/blocks/findBlock.ts @@ -0,0 +1,40 @@ +/** + * Block lookup helpers. + * + * @module + */ + +import type { Node as PMNode } from 'prosemirror-model'; + +/** + * Finds a block node by its id. + * + * Block ids only live on block nodes, so inline content (text, hard breaks) + * is never visited, and the search stops at the first match - unlike + * `doc.descendants`, which cannot skip the remaining siblings. + * + * @param doc - The document (or any node) to search in + * @param id - The block id to look for + * @returns The node and its position, or null if not found + */ +export function findBlockById(doc: PMNode, id: string): { node: PMNode; pos: number } | null { + return id ? searchChildren(doc, 0, id) : null; +} + +function searchChildren( + parent: PMNode, + start: number, + id: string +): { node: PMNode; pos: number } | null { + let pos = start; + for (let i = 0; i < parent.childCount; i++) { + const child = parent.child(i); + if (child.attrs.id === id) return { node: child, pos }; + if (!child.inlineContent && child.childCount > 0) { + const found = searchChildren(child, pos + 1, id); + if (found) return found; + } + pos += child.nodeSize; + } + return null; +} diff --git a/packages/core/src/blocks/index.ts b/packages/core/src/blocks/index.ts index 5bef43a..e1dea82 100644 --- a/packages/core/src/blocks/index.ts +++ b/packages/core/src/blocks/index.ts @@ -20,5 +20,6 @@ export type { } from './types'; // Conversion functions -export { blockToNode, inlineContentToNodes, stylesToMarks, blocksToDoc } from './blockToNode'; -export { nodeToBlock, marksToStyles, docToBlocks } from './nodeToBlock'; +export { blockToNode, blocksToDoc } from './blockToNode'; +export { nodeToBlock, docToBlocks } from './nodeToBlock'; +export { findBlockById } from './findBlock'; diff --git a/packages/core/src/blocks/nodeToBlock.ts b/packages/core/src/blocks/nodeToBlock.ts index 8a6ce7d..64aa963 100644 --- a/packages/core/src/blocks/nodeToBlock.ts +++ b/packages/core/src/blocks/nodeToBlock.ts @@ -164,7 +164,7 @@ function nodeContentToInline(node: PMNode): InlineContent[] { * @param marks - Array of ProseMirror marks * @returns A TextStyles object */ -export function marksToStyles(marks: readonly Mark[]): TextStyles { +function marksToStyles(marks: readonly Mark[]): TextStyles { const styles: TextStyles = {}; for (const mark of marks) { diff --git a/packages/core/src/editor/Editor.ts b/packages/core/src/editor/Editor.ts index 02bc60b..866cd5c 100644 --- a/packages/core/src/editor/Editor.ts +++ b/packages/core/src/editor/Editor.ts @@ -6,7 +6,7 @@ * * @example * ```typescript - * import { OpenBlockEditor } from '@openblock/core'; + * import { OpenBlockEditor } from '@labbs/openblock-core'; * * const editor = new OpenBlockEditor({ * element: document.getElementById('editor'), @@ -24,7 +24,7 @@ import { EditorView } from 'prosemirror-view'; import { EditorState, Transaction, Plugin, Selection } from 'prosemirror-state'; -import { Schema } from 'prosemirror-model'; +import { Schema, Node as PMNode } from 'prosemirror-model'; import { history as createHistoryPlugin } from 'prosemirror-history'; import { generateId } from '../utils/generateId'; @@ -43,6 +43,7 @@ import { blockToNode, nodeToBlock, docToBlocks, + findBlockById, } from '../blocks'; /** @@ -237,23 +238,9 @@ export class OpenBlockEditor { return found ? nodeToBlock(found.node) : undefined; } - /** - * Locate a block node by id. Stops traversing as soon as it is found - * (returning false from `descendants` only skips children, so we also - * short-circuit siblings with a flag). - */ - private _findBlockById( - id: string - ): { node: import('prosemirror-model').Node; pos: number } | null { - let result: { node: import('prosemirror-model').Node; pos: number } | null = null; - this.pm.doc.descendants((node, pos) => { - if (result) return false; - if (node.attrs.id === id) { - result = { node, pos }; - return false; - } - }); - return result; + /** Locate a block node by id (see {@link findBlockById}). */ + private _findBlockById(id: string): { node: PMNode; pos: number } | null { + return findBlockById(this.pm.doc, id); } /** @@ -385,6 +372,8 @@ export class OpenBlockEditor { // and deleting it first would invalidate this ancestor's range. return false; } + // Ids only live on block nodes: skip inline content + return !node.inlineContent; }); // Delete in reverse order to preserve positions during iteration @@ -846,7 +835,7 @@ export class OpenBlockEditor { /** * Convert a block to a code block. */ - private _convertToCodeBlock(pos: number, node: import('prosemirror-model').Node): void { + private _convertToCodeBlock(pos: number, node: PMNode): void { const codeBlockType = this._schema.nodes.codeBlock; if (!codeBlockType) return; @@ -987,7 +976,7 @@ export class OpenBlockEditor { private _setColumnWidthsEvenly( tr: Transaction, columnListPos: number, - columnList: import('prosemirror-model').Node, + columnList: PMNode, options: { skipPos?: number; columnCount?: number } = {} ): void { const count = options.columnCount ?? columnList.childCount; @@ -1023,7 +1012,7 @@ export class OpenBlockEditor { // Going down to one (or zero) column: unwrap the columnList and keep content if (columnList.childCount <= 2) { - const remainingContent: import('prosemirror-model').Node[] = []; + const remainingContent: PMNode[] = []; columnList.forEach((col, offset) => { const colPos = columnListPos + 1 + offset; // Keep the other columns' content — or this column's own content diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 098bb50..b0bae18 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -103,6 +103,8 @@ export { strikethroughMark, codeMark, linkMark, + textColorMark, + backgroundColorMark, } from './schema'; // Sanitization helpers - validate user-provided URLs and CSS values before @@ -123,7 +125,7 @@ export { generateId } from './utils/generateId'; // BLOCKS // =========================================================================== -export { blockToNode, nodeToBlock, blocksToDoc, docToBlocks } from './blocks'; +export { blockToNode, nodeToBlock, blocksToDoc, docToBlocks, findBlockById } from './blocks'; export type { Block, PartialBlock, @@ -132,6 +134,7 @@ export type { TextStyles, StyledText, LinkContent, + HardBreakContent, InlineContent, } from './blocks'; @@ -211,6 +214,17 @@ export { } from './plugins'; export type { BubbleMenuConfig, BubbleMenuState, BlockTypeInfo, TextAlign } from './plugins'; +// Multi-block selection +export { + createMultiBlockSelectionPlugin, + MULTI_BLOCK_SELECTION_KEY, + selectBlock, + clearBlockSelection, + getSelectedBlockPositions, + isMultiBlockSelectionActive, +} from './plugins'; +export type { MultiBlockSelectionConfig, MultiBlockSelectionState } from './plugins'; + // Table plugin export { createTablePlugin } from './plugins'; export type { TablePluginConfig } from './plugins'; diff --git a/packages/core/src/plugins/blockIdPlugin.ts b/packages/core/src/plugins/blockIdPlugin.ts index ef967f1..576715b 100644 --- a/packages/core/src/plugins/blockIdPlugin.ts +++ b/packages/core/src/plugins/blockIdPlugin.ts @@ -22,7 +22,7 @@ export const BLOCK_ID_PLUGIN_KEY = new PluginKey('blockIds'); * * @example * ```typescript - * import { createBlockIdPlugin } from '@openblock/core'; + * import { createBlockIdPlugin } from '@labbs/openblock-core'; * * const plugins = [ * createBlockIdPlugin(), @@ -48,14 +48,11 @@ export function createBlockIdPlugin(): Plugin { const seenIds = new Set(); newState.doc.descendants((node, pos) => { - // Only handle block nodes (plus column nodes, which need IDs too) - if (!node.isBlock && node.type.name !== 'column') { - return; - } - - // Only assign ID if the node type has an id attribute defined + // Ids only live on block nodes (columns included): inline nodes are + // never reached since textblock content is skipped below. + // Only assign an ID if the node type defines an id attribute. if (node.attrs.id === undefined) { - return; + return !node.inlineContent; } const id = node.attrs.id as string | null; @@ -69,6 +66,10 @@ export function createBlockIdPlugin(): Plugin { } else { seenIds.add(id); } + + // Ids only live on block nodes: skip inline content (text nodes are + // the bulk of the document, and this runs on every edit) + return !node.inlineContent; }); // If no updates needed, return null diff --git a/packages/core/src/plugins/bubbleMenuPlugin.ts b/packages/core/src/plugins/bubbleMenuPlugin.ts index d21584a..20a22b1 100644 --- a/packages/core/src/plugins/bubbleMenuPlugin.ts +++ b/packages/core/src/plugins/bubbleMenuPlugin.ts @@ -9,6 +9,7 @@ import { Plugin, PluginKey, EditorState, NodeSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import type { Mark } from 'prosemirror-model'; import { BlurHideHandle, createBlurHideHandle, @@ -153,34 +154,34 @@ function getActiveMarks(state: EditorState): BubbleMenuState['activeMarks'] { backgroundColor: null as string | null, }; - if (empty) { - // Check stored marks or marks at cursor - const storedMarks = state.storedMarks || $from.marks(); - for (const mark of storedMarks) { - if (mark.type.name === 'bold') marks.bold = true; - if (mark.type.name === 'italic') marks.italic = true; - if (mark.type.name === 'underline') marks.underline = true; - if (mark.type.name === 'strikethrough') marks.strikethrough = true; - if (mark.type.name === 'code') marks.code = true; - if (mark.type.name === 'link') marks.link = mark.attrs.href; - if (mark.type.name === 'textColor') marks.textColor = mark.attrs.color; - if (mark.type.name === 'backgroundColor') marks.backgroundColor = mark.attrs.color; + const collect = (markList: readonly Mark[]) => { + for (const mark of markList) { + switch (mark.type.name) { + case 'bold': + case 'italic': + case 'underline': + case 'strikethrough': + case 'code': + marks[mark.type.name] = true; + break; + case 'link': + marks.link = mark.attrs.href; + break; + case 'textColor': + case 'backgroundColor': + marks[mark.type.name] = mark.attrs.color; + break; + } } + }; + + if (empty) { + // Stored marks or marks at cursor + collect(state.storedMarks || $from.marks()); } else { - // Check marks in selection range + // Marks in the selection range state.doc.nodesBetween(from, to, (node) => { - if (node.isText && node.marks) { - for (const mark of node.marks) { - if (mark.type.name === 'bold') marks.bold = true; - if (mark.type.name === 'italic') marks.italic = true; - if (mark.type.name === 'underline') marks.underline = true; - if (mark.type.name === 'strikethrough') marks.strikethrough = true; - if (mark.type.name === 'code') marks.code = true; - if (mark.type.name === 'link') marks.link = mark.attrs.href; - if (mark.type.name === 'textColor') marks.textColor = mark.attrs.color; - if (mark.type.name === 'backgroundColor') marks.backgroundColor = mark.attrs.color; - } - } + if (node.isText) collect(node.marks); }); } @@ -251,7 +252,7 @@ function shouldShowMenu( * * @example * ```typescript - * import { createBubbleMenuPlugin, BUBBLE_MENU_PLUGIN_KEY } from '@openblock/core'; + * import { createBubbleMenuPlugin, BUBBLE_MENU_PLUGIN_KEY } from '@labbs/openblock-core'; * * const plugin = createBubbleMenuPlugin(); * diff --git a/packages/core/src/plugins/createPlugins.ts b/packages/core/src/plugins/createPlugins.ts index 173fd73..75d0559 100644 --- a/packages/core/src/plugins/createPlugins.ts +++ b/packages/core/src/plugins/createPlugins.ts @@ -133,7 +133,7 @@ export interface CreatePluginsOptions { * * @example * ```typescript - * import { createPlugins } from '@openblock/core'; + * import { createPlugins } from '@labbs/openblock-core'; * * const plugins = createPlugins({ * schema: mySchema, diff --git a/packages/core/src/plugins/dragDropPlugin.ts b/packages/core/src/plugins/dragDropPlugin.ts index 35edb08..a98405e 100644 --- a/packages/core/src/plugins/dragDropPlugin.ts +++ b/packages/core/src/plugins/dragDropPlugin.ts @@ -88,7 +88,7 @@ export interface DragDropConfig { * * @example * ```typescript - * import { createDragDropPlugin } from '@openblock/core'; + * import { createDragDropPlugin } from '@labbs/openblock-core'; * * const plugin = createDragDropPlugin({ * showHandles: true, @@ -200,6 +200,9 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { // Add class to handle and body for cursor handle.classList.add(`${handleClass}--dragging`); document.body.style.cursor = 'grabbing'; + + // Track the pointer on the whole document, for the drag only + toggleDragListeners(true); }); menu.appendChild(handle); @@ -227,10 +230,8 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { } // Get the actual DOM element for this block to position correctly inside columns - const blockId = node.attrs.id; - const blockEl = blockId - ? (view.dom.querySelector(`[data-block-id="${blockId}"]`) as HTMLElement | null) - : null; + const dom = view.nodeDOM(blockPos); + const blockEl = dom instanceof HTMLElement ? dom : null; const editorRect = view.dom.getBoundingClientRect(); let top: number; @@ -289,7 +290,9 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { let cachedDoc: PMNode | null = null; /** - * Update the block position cache when document changes. + * Rebuild the block position cache if the document changed since the last + * build. Called lazily on hover (not on every transaction): typing never + * pays for it. */ function updateBlockPosCache(view: EditorView): void { if (view.state.doc === cachedDoc) return; @@ -301,7 +304,8 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { if (node.attrs.id) { blockPosCache.set(node.attrs.id, pos); } - return true; + // Ids only live on block nodes: skip inline content + return !node.inlineContent; }); } @@ -322,18 +326,10 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { const blockId = blockEl.getAttribute('data-block-id'); if (!blockId) return null; - // Use cached position if available - const cachedPos = blockPosCache.get(blockId); - if (cachedPos !== undefined) { - return { pos: cachedPos, id: blockId }; - } - - // Fallback: update cache and try again + // No-op unless the document changed since the last lookup updateBlockPosCache(view); const pos = blockPosCache.get(blockId); - if (pos === undefined) return null; - - return { pos, id: blockId }; + return pos === undefined ? null : { pos, id: blockId }; } // Event handlers stored for cleanup @@ -341,6 +337,22 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { let handleDragMove: ((event: MouseEvent) => void) | null = null; let handleDragEnd: (() => void) | null = null; + /** + * Attach / detach the document-level drag listeners. They are only + * attached while a block is being dragged, so regular mouse moves anywhere + * on the page don't run editor code. + */ + function toggleDragListeners(enabled: boolean): void { + if (!handleDragMove || !handleDragEnd) return; + if (enabled) { + document.addEventListener('mousemove', handleDragMove); + document.addEventListener('mouseup', handleDragEnd); + } else { + document.removeEventListener('mousemove', handleDragMove); + document.removeEventListener('mouseup', handleDragEnd); + } + } + // Timer for delayed hiding of side menu let hideMenuTimer: ReturnType | null = null; @@ -520,6 +532,8 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { // Mouse up - drop the block handleDragEnd = () => { + toggleDragListeners(false); + // Reset cursor document.body.style.cursor = ''; @@ -551,10 +565,6 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { const dragHandle = sideMenuEl?.querySelector(`.${handleClass}`); dragHandle?.classList.remove(`${handleClass}--dragging`); }; - - // Attach mouse event listeners to document for drag - document.addEventListener('mousemove', handleDragMove); - document.addEventListener('mouseup', handleDragEnd); } return new Plugin({ @@ -575,28 +585,43 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { }); } + // Pending repositioning after a document change (one per frame max) + let repositionRafId: number | null = null; + const cancelReposition = () => { + if (repositionRafId !== null) { + cancelAnimationFrame(repositionRafId); + repositionRafId = null; + } + }; + return { update(view, prevState) { - const docChanged = view.state.doc !== prevState.doc; - - // Update block position cache when document changes - updateBlockPosCache(view); - // Ensure side menu exists (may not have been created if editor wasn't mounted) ensureSideMenu(view); - const state = DRAG_DROP_PLUGIN_KEY.getState(view.state); - const prevPluginState = DRAG_DROP_PLUGIN_KEY.getState(prevState); + const hoveredBlockPos = DRAG_DROP_PLUGIN_KEY.getState(view.state)?.hoveredBlockPos ?? null; + const prevHoveredBlockPos = + DRAG_DROP_PLUGIN_KEY.getState(prevState)?.hoveredBlockPos ?? null; - // Repositioning does DOM measurements (querySelector + - // getBoundingClientRect): skip when nothing relevant changed - const hoveredBlockPos = state?.hoveredBlockPos ?? null; - const prevHoveredBlockPos = prevPluginState?.hoveredBlockPos ?? null; - if (hoveredBlockPos === prevHoveredBlockPos && !docChanged) { + // Hovered block changed: reposition right away + if (hoveredBlockPos !== prevHoveredBlockPos) { + cancelReposition(); + updateSideMenuPosition(view, hoveredBlockPos); return; } - updateSideMenuPosition(view, hoveredBlockPos); + // Edits can shift the hovered block. Repositioning measures the DOM + // (getBoundingClientRect): do it once per frame instead of forcing + // a synchronous layout on every keystroke. + if (hoveredBlockPos !== null && view.state.doc !== prevState.doc && repositionRafId === null) { + repositionRafId = requestAnimationFrame(() => { + repositionRafId = null; + updateSideMenuPosition( + view, + DRAG_DROP_PLUGIN_KEY.getState(view.state)?.hoveredBlockPos ?? null + ); + }); + } }, destroy() { @@ -604,15 +629,12 @@ export function createDragDropPlugin(config: DragDropConfig = {}): Plugin { cancelAnimationFrame(ensureMenuRafId); ensureMenuRafId = null; } + cancelReposition(); if (handleMouseMove) { view.dom.removeEventListener('mousemove', handleMouseMove); } - if (handleDragMove) { - document.removeEventListener('mousemove', handleDragMove); - } - if (handleDragEnd) { - document.removeEventListener('mouseup', handleDragEnd); - } + toggleDragListeners(false); + if (draggedBlockPos !== null) document.body.style.cursor = ''; if (hideMenuTimer) { clearTimeout(hideMenuTimer); hideMenuTimer = null; diff --git a/packages/core/src/plugins/index.ts b/packages/core/src/plugins/index.ts index 81f35f8..7449a29 100644 --- a/packages/core/src/plugins/index.ts +++ b/packages/core/src/plugins/index.ts @@ -62,7 +62,7 @@ export { MULTI_BLOCK_SELECTION_KEY, selectBlock, clearBlockSelection, - getSelectedBlocks, + getSelectedBlockPositions, isMultiBlockSelectionActive, } from './multiBlockSelectionPlugin'; export type { MultiBlockSelectionConfig, MultiBlockSelectionState } from './multiBlockSelectionPlugin'; diff --git a/packages/core/src/plugins/inputRules.ts b/packages/core/src/plugins/inputRules.ts index 4e54478..2b27a7d 100644 --- a/packages/core/src/plugins/inputRules.ts +++ b/packages/core/src/plugins/inputRules.ts @@ -383,7 +383,7 @@ export interface InputRulesConfig { * * @example * ```typescript - * import { createInputRulesPlugin } from '@openblock/core'; + * import { createInputRulesPlugin } from '@labbs/openblock-core'; * * const plugin = createInputRulesPlugin(schema, { * headings: true, diff --git a/packages/core/src/plugins/mediaMenuPlugin.ts b/packages/core/src/plugins/mediaMenuPlugin.ts index 6dde79a..148d441 100644 --- a/packages/core/src/plugins/mediaMenuPlugin.ts +++ b/packages/core/src/plugins/mediaMenuPlugin.ts @@ -112,7 +112,7 @@ function getMediaSelection(state: EditorState): { mediaType: MediaType; pos: num * * @example * ```typescript - * import { createMediaMenuPlugin, MEDIA_MENU_PLUGIN_KEY } from '@openblock/core'; + * import { createMediaMenuPlugin, MEDIA_MENU_PLUGIN_KEY } from '@labbs/openblock-core'; * * const plugin = createMediaMenuPlugin(); * diff --git a/packages/core/src/plugins/multiBlockSelectionPlugin.ts b/packages/core/src/plugins/multiBlockSelectionPlugin.ts index ca6cdd4..aa6e354 100644 --- a/packages/core/src/plugins/multiBlockSelectionPlugin.ts +++ b/packages/core/src/plugins/multiBlockSelectionPlugin.ts @@ -391,7 +391,7 @@ export function clearBlockSelection(view: EditorView): void { /** * Get currently selected block positions. */ -export function getSelectedBlocks(state: EditorState): number[] { +export function getSelectedBlockPositions(state: EditorState): number[] { const pluginState = MULTI_BLOCK_SELECTION_KEY.getState(state); return pluginState?.selectedBlocks || []; } diff --git a/packages/core/src/plugins/slashMenuPlugin.ts b/packages/core/src/plugins/slashMenuPlugin.ts index fff093d..7fff72b 100644 --- a/packages/core/src/plugins/slashMenuPlugin.ts +++ b/packages/core/src/plugins/slashMenuPlugin.ts @@ -10,6 +10,7 @@ import { Plugin, PluginKey, EditorState, Selection, TextSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { setBlockType } from 'prosemirror-commands'; +import type { Node as PMNode } from 'prosemirror-model'; /** * Plugin key for accessing slash menu state. @@ -82,7 +83,7 @@ export interface SlashMenuConfig { * * @example * ```typescript - * import { createSlashMenuPlugin, SLASH_MENU_PLUGIN_KEY } from '@openblock/core'; + * import { createSlashMenuPlugin, SLASH_MENU_PLUGIN_KEY } from '@labbs/openblock-core'; * * const plugin = createSlashMenuPlugin({ onlyAtStart: true }); * @@ -312,7 +313,7 @@ export function executeSlashCommand( * @param view - The editor view * @param node - The new node to insert */ -function replaceCurrentBlock(view: EditorView, node: ReturnType): void { +function replaceCurrentBlock(view: EditorView, node: PMNode): void { const { $from } = view.state.selection; // Target the textblock that actually contains the cursor, not the @@ -346,45 +347,40 @@ function replaceCurrentBlock(view: EditorView, node: ReturnType { + view.dispatch(view.state.tr.replaceSelectionWith(node)); + }; + // Headings if (schema.nodes.heading) { - items.push( - { - id: 'heading1', - title: 'Heading 1', - description: 'Large section heading', - icon: 'heading1', - keywords: ['h1', 'title', 'large'], - group: 'Basic blocks', - action: (view, _state) => { - setBlockType(schema.nodes.heading, { level: 1 })(view.state, view.dispatch); - }, - }, - { - id: 'heading2', - title: 'Heading 2', - description: 'Medium section heading', - icon: 'heading2', - keywords: ['h2', 'subtitle'], - group: 'Basic blocks', - action: (view, _state) => { - setBlockType(schema.nodes.heading, { level: 2 })(view.state, view.dispatch); - }, - }, - { - id: 'heading3', - title: 'Heading 3', - description: 'Small section heading', - icon: 'heading3', - keywords: ['h3'], + const headings = [ + { description: 'Large section heading', keywords: ['h1', 'title', 'large'] }, + { description: 'Medium section heading', keywords: ['h2', 'subtitle'] }, + { description: 'Small section heading', keywords: ['h3'] }, + ]; + headings.forEach(({ description, keywords }, i) => { + const level = i + 1; + items.push({ + id: `heading${level}`, + title: `Heading ${level}`, + description, + icon: `heading${level}`, + keywords, group: 'Basic blocks', - action: (view, _state) => { - setBlockType(schema.nodes.heading, { level: 3 })(view.state, view.dispatch); + action: (view) => { + setBlockType(schema.nodes.heading, { level })(view.state, view.dispatch); }, - } - ); + }); + }); } + /** Replace the current block with a list holding one empty item */ + const createListAction = (listType: 'bulletList' | 'orderedList') => (view: EditorView) => { + const item = schema.nodes.listItem.create(null, schema.nodes.paragraph.create()); + replaceCurrentBlock(view, schema.nodes[listType].create(null, item)); + }; + // Lists if (schema.nodes.bulletList && schema.nodes.listItem) { items.push({ @@ -394,14 +390,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'list', keywords: ['ul', 'unordered', 'bullets'], group: 'Lists', - action: (view, _state) => { - const item = schema.nodes.listItem.create( - null, - schema.nodes.paragraph.create() - ); - const list = schema.nodes.bulletList.create(null, item); - replaceCurrentBlock(view, list); - }, + action: createListAction('bulletList'), }); } @@ -413,14 +402,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'listOrdered', keywords: ['ol', 'ordered', 'numbers'], group: 'Lists', - action: (view, _state) => { - const item = schema.nodes.listItem.create( - null, - schema.nodes.paragraph.create() - ); - const list = schema.nodes.orderedList.create(null, item); - replaceCurrentBlock(view, list); - }, + action: createListAction('orderedList'), }); } @@ -442,9 +424,8 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe // Callout - callouts accept inline content directly if (schema.nodes.callout) { - const createCalloutAction = (calloutType: string) => (view: EditorView, _state: SlashMenuState) => { - const node = schema.nodes.callout.create({ calloutType }); - replaceCurrentBlock(view, node); + const createCalloutAction = (calloutType: string) => (view: EditorView) => { + replaceCurrentBlock(view, schema.nodes.callout.create({ calloutType })); }; items.push( @@ -529,6 +510,14 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe // Columns if (schema.nodes.columnList && schema.nodes.column) { + /** Insert a column layout, one empty paragraph per column */ + const createColumnsAction = (widths: number[]) => (view: EditorView) => { + const columns = widths.map((width) => + schema.nodes.column.create({ width }, schema.nodes.paragraph.create()) + ); + insertNode(view, schema.nodes.columnList.create(null, columns)); + }; + items.push( { id: 'columns2', @@ -537,18 +526,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'columns', keywords: ['col', 'layout', 'split', 'two'], group: 'Layout', - action: (view, _state) => { - const col1 = schema.nodes.column.create( - { width: 50 }, - schema.nodes.paragraph.create() - ); - const col2 = schema.nodes.column.create( - { width: 50 }, - schema.nodes.paragraph.create() - ); - const columnList = schema.nodes.columnList.create(null, [col1, col2]); - view.dispatch(view.state.tr.replaceSelectionWith(columnList)); - }, + action: createColumnsAction([50, 50]), }, { id: 'columns3', @@ -557,22 +535,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'columns', keywords: ['col', 'layout', 'split', 'three'], group: 'Layout', - action: (view, _state) => { - const col1 = schema.nodes.column.create( - { width: 33.33 }, - schema.nodes.paragraph.create() - ); - const col2 = schema.nodes.column.create( - { width: 33.33 }, - schema.nodes.paragraph.create() - ); - const col3 = schema.nodes.column.create( - { width: 33.34 }, - schema.nodes.paragraph.create() - ); - const columnList = schema.nodes.columnList.create(null, [col1, col2, col3]); - view.dispatch(view.state.tr.replaceSelectionWith(columnList)); - }, + action: createColumnsAction([33.33, 33.33, 33.34]), }, { id: 'columnsSidebar', @@ -581,45 +544,25 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'columns', keywords: ['col', 'layout', 'sidebar', 'aside'], group: 'Layout', - action: (view, _state) => { - const sidebar = schema.nodes.column.create( - { width: 30 }, - schema.nodes.paragraph.create() - ); - const main = schema.nodes.column.create( - { width: 70 }, - schema.nodes.paragraph.create() - ); - const columnList = schema.nodes.columnList.create(null, [sidebar, main]); - view.dispatch(view.state.tr.replaceSelectionWith(columnList)); - }, + action: createColumnsAction([30, 70]), } ); } // Tables if (schema.nodes.table && schema.nodes.tableRow && schema.nodes.tableCell) { - const createTableAction = - (rowCount: number, colCount: number) => - (view: EditorView, _state: SlashMenuState) => { - const createRow = () => { - const cells = []; - for (let i = 0; i < colCount; i++) { - cells.push( - schema.nodes.tableCell.create(null, schema.nodes.paragraph.create()) - ); - } - return schema.nodes.tableRow.create(null, cells); - }; - - const rows = []; - for (let i = 0; i < rowCount; i++) { - rows.push(createRow()); - } - - const table = schema.nodes.table.create(null, rows); - view.dispatch(view.state.tr.replaceSelectionWith(table)); - }; + /** Insert an empty rowCount x colCount table */ + const createTableAction = (rowCount: number, colCount: number) => (view: EditorView) => { + const rows = Array.from({ length: rowCount }, () => + schema.nodes.tableRow.create( + null, + Array.from({ length: colCount }, () => + schema.nodes.tableCell.create(null, schema.nodes.paragraph.create()) + ) + ) + ); + insertNode(view, schema.nodes.table.create(null, rows)); + }; items.push( { @@ -661,10 +604,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'image', keywords: ['img', 'picture', 'photo', 'media'], group: 'Media', - action: (view) => { - const node = schema.nodes.image.create({ src: '', alt: '' }); - view.dispatch(view.state.tr.replaceSelectionWith(node)); - }, + action: (view) => insertNode(view, schema.nodes.image.create({ src: '', alt: '' })), }); } @@ -695,10 +635,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'embed', keywords: ['video', 'youtube', 'vimeo', 'iframe', 'external'], group: 'Media', - action: (view) => { - const node = schema.nodes.embed.create({ url: '', provider: 'generic' }); - view.dispatch(view.state.tr.replaceSelectionWith(node)); - }, + action: (view) => insertNode(view, schema.nodes.embed.create({ url: '', provider: 'generic' })), }, { id: 'youtube', @@ -707,10 +644,7 @@ export function getDefaultSlashMenuItems(schema: EditorState['schema']): SlashMe icon: 'youtube', keywords: ['video', 'embed', 'media'], group: 'Media', - action: (view) => { - const node = schema.nodes.embed.create({ url: '', provider: 'youtube' }); - view.dispatch(view.state.tr.replaceSelectionWith(node)); - }, + action: (view) => insertNode(view, schema.nodes.embed.create({ url: '', provider: 'youtube' })), } ); } diff --git a/packages/core/src/pm/ProseMirrorAPI.ts b/packages/core/src/pm/ProseMirrorAPI.ts index 4bc42df..9b6c19f 100644 --- a/packages/core/src/pm/ProseMirrorAPI.ts +++ b/packages/core/src/pm/ProseMirrorAPI.ts @@ -26,7 +26,7 @@ import { DecorationSet, DecorationAttrs, } from 'prosemirror-view'; -import type { DecorationSource, NodeView, NodeViewConstructor } from 'prosemirror-view'; +import type { NodeView, NodeViewConstructor } from 'prosemirror-view'; import { EditorState, Transaction, @@ -676,7 +676,6 @@ export { PluginKey, Decoration, DecorationSet, - DecorationSource, PMNode as Node, Schema, Mark, diff --git a/packages/core/src/schema/createSchema.ts b/packages/core/src/schema/createSchema.ts index d2e5daa..929251c 100644 --- a/packages/core/src/schema/createSchema.ts +++ b/packages/core/src/schema/createSchema.ts @@ -124,7 +124,7 @@ function warnOnDefaultOverrides( * * @example * ```typescript - * import { createSchema } from '@openblock/core'; + * import { createSchema } from '@labbs/openblock-core'; * * const schema = createSchema(); * // schema.nodes.paragraph, schema.marks.bold, etc. diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index 948e575..523a612 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -33,7 +33,7 @@ export { embedNode, parseEmbedUrl, } from './nodes'; -export type { TextAlignment, CalloutType, ImageAlignment, EmbedProvider } from './nodes'; +export type { CalloutType, ImageAlignment, EmbedProvider } from './nodes'; // Re-export individual mark specs for extension authors export { diff --git a/packages/core/src/schema/nodes/doc.ts b/packages/core/src/schema/nodes/doc.ts index 77ac7e8..431da3b 100644 --- a/packages/core/src/schema/nodes/doc.ts +++ b/packages/core/src/schema/nodes/doc.ts @@ -15,7 +15,7 @@ import type { NodeSpec } from 'prosemirror-model'; * * @example * ```typescript - * import { docNode } from '@openblock/core'; + * import { docNode } from '@labbs/openblock-core'; * // Used internally by createSchema() * ``` */ diff --git a/packages/core/src/schema/nodes/index.ts b/packages/core/src/schema/nodes/index.ts index 5ea72fd..b3b7f03 100644 --- a/packages/core/src/schema/nodes/index.ts +++ b/packages/core/src/schema/nodes/index.ts @@ -7,7 +7,6 @@ // Core nodes export { docNode } from './doc'; export { paragraphNode } from './paragraph'; -export type { TextAlignment } from './paragraph'; export { headingNode } from './heading'; export { textNode } from './text'; export { hardBreakNode } from './hardBreak'; diff --git a/packages/core/src/schema/nodes/paragraph.ts b/packages/core/src/schema/nodes/paragraph.ts index 52c90b9..2d72dde 100644 --- a/packages/core/src/schema/nodes/paragraph.ts +++ b/packages/core/src/schema/nodes/paragraph.ts @@ -11,11 +11,6 @@ import type { NodeSpec, DOMOutputSpec, Node as PMNode } from 'prosemirror-model' import { blockIdAttr, getBlockIdAttrs, blockIdToDOM } from '../blockIdAttrs'; import { sanitizeTextAlign } from '../sanitizeStyle'; -/** - * Valid text alignment values. - */ -export type TextAlignment = 'left' | 'center' | 'right'; - /** * Paragraph node spec. * diff --git a/packages/core/src/schema/sanitizeStyle.ts b/packages/core/src/schema/sanitizeStyle.ts index 30b6317..217e9fe 100644 --- a/packages/core/src/schema/sanitizeStyle.ts +++ b/packages/core/src/schema/sanitizeStyle.ts @@ -51,18 +51,16 @@ export function sanitizeCssColor(value: unknown): string | null { return null; } -/** - * Text alignment values supported by text blocks. - */ -export type TextAlign = 'left' | 'center' | 'right' | 'justify'; +/** Text alignment values accepted on text blocks ('justify' can come from pasted HTML). */ +type TextAlignValue = 'left' | 'center' | 'right' | 'justify'; -const TEXT_ALIGNS: ReadonlySet = new Set(['left', 'center', 'right', 'justify']); +const TEXT_ALIGNS: ReadonlySet = new Set(['left', 'center', 'right', 'justify']); /** * Normalizes a text alignment value; anything unknown becomes 'left'. */ -export function sanitizeTextAlign(value: unknown): TextAlign { - return typeof value === 'string' && TEXT_ALIGNS.has(value) ? (value as TextAlign) : 'left'; +export function sanitizeTextAlign(value: unknown): TextAlignValue { + return typeof value === 'string' && TEXT_ALIGNS.has(value) ? (value as TextAlignValue) : 'left'; } /** diff --git a/packages/core/src/styles/editor.css b/packages/core/src/styles/editor.css index 06f14c1..fd106ab 100644 --- a/packages/core/src/styles/editor.css +++ b/packages/core/src/styles/editor.css @@ -625,16 +625,6 @@ flex-shrink: 0; } -/* =========================================================================== - * Text Alignment Buttons (in Bubble Menu) - * =========================================================================== */ - -.ob-text-align-buttons { - display: flex; - align-items: center; - gap: 1px; -} - /* =========================================================================== * Block Type Selector (in Bubble Menu) * =========================================================================== */ @@ -854,36 +844,6 @@ margin-bottom: 0; } -/* Selected cell */ -.openblock-editor .ob-table-cell.selectedCell, -.openblock-editor .ob-table-header.selectedCell { - background: hsl(220 90% 56% / 0.1); -} - -/* Cell resize handle (for future column resize) */ -.openblock-editor .ob-table-cell-resize-handle, -.openblock-editor .ob-table-header-resize-handle { - position: absolute; - right: -2px; - top: 0; - bottom: 0; - width: 4px; - cursor: col-resize; - background: transparent; - z-index: 10; -} - -.openblock-editor .ob-table-cell-resize-handle:hover, -.openblock-editor .ob-table-header-resize-handle:hover { - background: hsl(var(--ob-primary) / 0.3); -} - -/* Table wrapper for overflow handling */ -.openblock-editor .ob-table-wrapper { - overflow-x: auto; - margin: 0.5em 0; -} - /* =========================================================================== * Link Popover (compact inline design) * =========================================================================== */ @@ -1031,16 +991,6 @@ position: relative; } -.ob-color-picker-indicator { - position: absolute; - bottom: 3px; - left: 50%; - transform: translateX(-50%); - width: 14px; - height: 3px; - border-radius: 2px; -} - .ob-color-picker-dropdown { /* Position set via inline styles (fixed positioning) */ background: hsl(var(--ob-background)); diff --git a/packages/core/src/styles/index.ts b/packages/core/src/styles/index.ts deleted file mode 100644 index b3d4e4d..0000000 --- a/packages/core/src/styles/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Styles module. - * - * `editor.css` is the single source of truth for the editor styles. - * - * By default the editor injects these styles automatically into the document - * head (see {@link injectStyles}, called by `Editor` unless the - * `injectStyles: false` config option is set). - * - * If you prefer to manage CSS yourself (e.g. to let your bundler dedupe, - * order, or post-process it), disable auto-injection and import the - * stylesheet directly: - * - * ```typescript - * import '@labbs/openblock-core/styles'; - * - * const editor = new Editor({ injectStyles: false, ... }); - * ``` - * - * @module - */ - -export { injectStyles, removeStyles, areStylesInjected } from './injectStyles'; diff --git a/packages/core/src/styles/injectStyles.ts b/packages/core/src/styles/injectStyles.ts index e77160f..24885df 100644 --- a/packages/core/src/styles/injectStyles.ts +++ b/packages/core/src/styles/injectStyles.ts @@ -24,7 +24,7 @@ let stylesInjected = false; * * @example * ```typescript - * import { injectStyles } from '@openblock/core'; + * import { injectStyles } from '@labbs/openblock-core'; * * // Automatically inject styles (called by editor by default) * injectStyles(); @@ -58,7 +58,7 @@ export function injectStyles(): boolean { * * @example * ```typescript - * import { removeStyles } from '@openblock/core'; + * import { removeStyles } from '@labbs/openblock-core'; * * // Clean up when done * removeStyles(); diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts deleted file mode 100644 index 457a8ba..0000000 --- a/packages/core/src/types/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * OpenBlock Core Types. - * - * Re-exports types from various modules for convenience. - * All types are PUBLIC - this is a core principle of OpenBlock. - * - * @module - */ - -// Block types - re-export from blocks module -export type { - TextStyles, - StyledText, - LinkContent, - InlineContent, - Block, - PartialBlock, - BlockIdentifier, - BlockPlacement, -} from '../blocks/types'; - -// Editor event types - re-export from editor module -export type { EditorEvents, EventHandler } from '../editor/EditorConfig'; diff --git a/packages/react/package.json b/packages/react/package.json index 53b27ea..a772143 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -25,7 +25,8 @@ "./package.json": "./package.json" }, "files": [ - "dist" + "dist", + "!dist/standalone/*.map" ], "keywords": [ "editor", diff --git a/packages/react/src/blocks/createReactBlockSpec.tsx b/packages/react/src/blocks/createReactBlockSpec.tsx index 06eb70a..0c6a747 100644 --- a/packages/react/src/blocks/createReactBlockSpec.tsx +++ b/packages/react/src/blocks/createReactBlockSpec.tsx @@ -35,6 +35,7 @@ import { type NodeViewConstructor, Node as PMNode, EditorView, + findBlockById, } from '@labbs/openblock-core'; /** @@ -339,22 +340,11 @@ export function useUpdateBlock( (updates: Partial<{ [K in keyof T]: T[K]['default'] }>) => { if (!editor || editor.isDestroyed) return; - // Find the block position - let pos: number | null = null; - editor.pm.doc.descendants((node, nodePos) => { - if (node.attrs.id === blockId) { - pos = nodePos; - return false; - } - }); - - if (pos !== null) { - const node = editor.pm.doc.nodeAt(pos); - if (node) { - const tr = editor.pm.createTransaction(); - tr.setNodeMarkup(pos, undefined, { ...node.attrs, ...updates }); - editor.pm.dispatch(tr); - } + const found = findBlockById(editor.pm.doc, blockId); + if (found) { + const tr = editor.pm.createTransaction(); + tr.setNodeMarkup(found.pos, undefined, { ...found.node.attrs, ...updates }); + editor.pm.dispatch(tr); } }, [editor, blockId] diff --git a/packages/react/src/components/BubbleMenu.tsx b/packages/react/src/components/BubbleMenu.tsx index cda3a04..7b76754 100644 --- a/packages/react/src/components/BubbleMenu.tsx +++ b/packages/react/src/components/BubbleMenu.tsx @@ -8,7 +8,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView, BubbleMenu } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView, BubbleMenu } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock(); @@ -28,7 +28,7 @@ * * @example Adding a translate button * ```tsx - * import { BubbleMenu, BubbleMenuItem } from '@openblock/react'; + * import { BubbleMenu, BubbleMenuItem } from '@labbs/openblock-react'; * * // Define a custom "Translate" button * const translateItem: BubbleMenuItem = { diff --git a/packages/react/src/components/MediaMenu.tsx b/packages/react/src/components/MediaMenu.tsx index aea7a50..70df0e2 100644 --- a/packages/react/src/components/MediaMenu.tsx +++ b/packages/react/src/components/MediaMenu.tsx @@ -6,7 +6,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView, MediaMenu } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView, MediaMenu } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock(); @@ -29,6 +29,7 @@ import { deleteMediaNode, ImageAttrs, EmbedAttrs, + findBlockById, } from '@labbs/openblock-core'; import { usePluginState } from '../hooks/usePluginState'; import { useClickOutside } from '../hooks/useClickOutside'; @@ -185,17 +186,10 @@ export function MediaMenu({ editor, className }: MediaMenuProps): React.ReactEle ); }; - // Listen to scroll on the editor container and window - const editorElement = editor.pm.view.dom; - const scrollContainer = editorElement.closest('.openblock-container') || editorElement.parentElement; - - window.addEventListener('scroll', handleScroll, true); - scrollContainer?.addEventListener('scroll', handleScroll); - - return () => { - window.removeEventListener('scroll', handleScroll, true); - scrollContainer?.removeEventListener('scroll', handleScroll); - }; + // Capture phase: also catches scrolls of any inner scroll container + // (scroll events don't bubble), so one listener covers them all. + window.addEventListener('scroll', handleScroll, { capture: true, passive: true }); + return () => window.removeEventListener('scroll', handleScroll, { capture: true }); }, [editor, menuState?.visible]); // Close popovers when clicking outside the menu, the popovers, @@ -247,16 +241,8 @@ export function MediaMenu({ editor, className }: MediaMenuProps): React.ReactEle // Fallback: find the node again by its id if (expectedId !== null) { - let found: number | null = null; - doc.descendants((node, pos) => { - if (found !== null) return false; - if (node.type.name === expectedType && node.attrs.id === expectedId) { - found = pos; - return false; - } - return true; - }); - return found; + const found = findBlockById(doc, expectedId); + return found && found.node.type.name === expectedType ? found.pos : null; } return null; diff --git a/packages/react/src/components/OpenBlockView.tsx b/packages/react/src/components/OpenBlockView.tsx index 397ef58..06daf07 100644 --- a/packages/react/src/components/OpenBlockView.tsx +++ b/packages/react/src/components/OpenBlockView.tsx @@ -3,7 +3,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock({ diff --git a/packages/react/src/components/SlashMenu.tsx b/packages/react/src/components/SlashMenu.tsx index 53504f4..50130f4 100644 --- a/packages/react/src/components/SlashMenu.tsx +++ b/packages/react/src/components/SlashMenu.tsx @@ -8,7 +8,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView, SlashMenu } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView, SlashMenu } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock(); @@ -27,7 +27,7 @@ * * @example Adding an AI writing assistant * ```tsx - * import { SlashMenu, SlashMenuItem } from '@openblock/react'; + * import { SlashMenu, SlashMenuItem } from '@labbs/openblock-react'; * * const aiWriteItem: SlashMenuItem = { * id: 'ai-write', @@ -110,9 +110,6 @@ import { usePluginState } from '../hooks/usePluginState'; import { useFlipPosition } from '../hooks/useFlipPosition'; import { QuoteIcon } from './icons'; -// Re-export SlashMenuItem for convenience -export type { SlashMenuItem } from '@labbs/openblock-core'; - /** * Props for SlashMenu component. */ diff --git a/packages/react/src/components/TableHandles.tsx b/packages/react/src/components/TableHandles.tsx index 93bf176..c7571f0 100644 --- a/packages/react/src/components/TableHandles.tsx +++ b/packages/react/src/components/TableHandles.tsx @@ -8,7 +8,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView, TableHandles } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView, TableHandles } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock(); @@ -25,6 +25,7 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; import { OpenBlockEditor, + EditorView, Transaction, addRowAtIndex, addColumnAtIndex, @@ -74,6 +75,33 @@ interface HoverState { index: number; } +/** + * Resolve the document position of a rendered `` element. + */ +function findTablePos(view: EditorView, tableElement: HTMLElement): number | null { + let pos: number; + try { + pos = view.posAtDOM(tableElement, 0); + } catch { + return null; + } + const { doc } = view.state; + let tablePos: number | null = doc.nodeAt(pos)?.type.name === 'table' ? pos : null; + if (tablePos === null) { + const $pos = doc.resolve(pos); + for (let depth = $pos.depth; depth > 0; depth--) { + if ($pos.node(depth).type.name === 'table') { + tablePos = $pos.before(depth); + break; + } + } + } + if (tablePos === null) return null; + // Make sure the position really maps to this element (e.g. nested tables) + const dom = view.nodeDOM(tablePos); + return dom === tableElement || (dom instanceof Node && tableElement.contains(dom)) ? tablePos : null; +} + /** * Find table info from DOM and editor state. * @@ -84,22 +112,11 @@ function getTableState( editor: OpenBlockEditor, tableElement: HTMLElement ): TableState | null { - // Find table position in document + // Find the table position from its DOM element (O(depth), instead of + // walking the whole document) const view = editor.pm.view; - let tablePos = -1; - - view.state.doc.descendants((node, pos) => { - if (node.type.name === 'table' && tablePos === -1) { - const domNode = view.nodeDOM(pos); - if (domNode === tableElement || tableElement.contains(domNode as Node)) { - tablePos = pos; - return false; - } - } - return true; - }); - - if (tablePos === -1) return null; + const tablePos = findTablePos(view, tableElement); + if (tablePos === null) return null; const table = view.state.doc.nodeAt(tablePos); if (!table || table.type.name !== 'table') return null; @@ -295,6 +312,9 @@ export function TableHandles({ // Mirror of tableState readable from stable event handlers. const tableStateRef = useRef(null); + // Whether a handle menu is open (read by the transaction listener) + const menuOpenRef = useRef(false); + menuOpenRef.current = showRowMenu !== null || showColMenu !== null; tableStateRef.current = tableState; const hideAll = useCallback(() => { @@ -349,21 +369,21 @@ export function TableHandles({ const current = tableStateRef.current; if (!tableElement) { - // Not over a table, but check if we have a current table state - // and mouse is near the table (in the handle zone) - if (current) { - const tableRect = current.tableRect; - // Keep visible only if mouse is within the extend button zones (right and bottom) - const isInExtendZone = - (e.clientX >= tableRect.right && e.clientX <= tableRect.right + 30 && - e.clientY >= tableRect.top && e.clientY <= tableRect.bottom) || - (e.clientY >= tableRect.bottom && e.clientY <= tableRect.bottom + 30 && - e.clientX >= tableRect.left && e.clientX <= tableRect.right); - - if (isInExtendZone) { - clearHideTimeout(); - return; - } + // Nothing is shown: nothing to hide (avoids re-arming a timer on + // every mouse move anywhere on the page) + if (!current) return; + + // Keep visible only if mouse is within the extend button zones (right and bottom) + const tableRect = current.tableRect; + const isInExtendZone = + (e.clientX >= tableRect.right && e.clientX <= tableRect.right + 30 && + e.clientY >= tableRect.top && e.clientY <= tableRect.bottom) || + (e.clientY >= tableRect.bottom && e.clientY <= tableRect.bottom + 30 && + e.clientX >= tableRect.left && e.clientX <= tableRect.right); + + if (isInExtendZone) { + clearHideTimeout(); + return; } scheduleHide(); return; @@ -456,7 +476,11 @@ export function TableHandles({ if (!editor || editor.isDestroyed) return; const unsubscribe = editor.on('transaction', ({ transaction }) => { - if ((transaction as Transaction).docChanged) { + // Skip the state updates while typing when no handle is shown + if ( + (transaction as Transaction).docChanged && + (tableStateRef.current !== null || menuOpenRef.current) + ) { hideAll(); } }); @@ -472,11 +496,11 @@ export function TableHandles({ hideAll(); }; - window.addEventListener('scroll', handleScrollOrResize, true); - window.addEventListener('resize', handleScrollOrResize); + window.addEventListener('scroll', handleScrollOrResize, { capture: true, passive: true }); + window.addEventListener('resize', handleScrollOrResize, { passive: true }); return () => { - window.removeEventListener('scroll', handleScrollOrResize, true); + window.removeEventListener('scroll', handleScrollOrResize, { capture: true }); window.removeEventListener('resize', handleScrollOrResize); }; }, [editor, tableState, hideAll]); diff --git a/packages/react/src/components/TableMenu.tsx b/packages/react/src/components/TableMenu.tsx index f75f514..5bda4fa 100644 --- a/packages/react/src/components/TableMenu.tsx +++ b/packages/react/src/components/TableMenu.tsx @@ -6,7 +6,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView, TableMenu } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView, TableMenu } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock(); diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 8ffaf19..ae38036 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -2,14 +2,11 @@ export { OpenBlockView } from './OpenBlockView'; export type { OpenBlockViewProps, OpenBlockViewRef } from './OpenBlockView'; export { SlashMenu } from './SlashMenu'; -export type { SlashMenuProps, SlashMenuItem } from './SlashMenu'; +export type { SlashMenuProps } from './SlashMenu'; export { BubbleMenu, BUBBLE_MENU_ITEMS, DEFAULT_BUBBLE_MENU_ORDER } from './BubbleMenu'; export type { BubbleMenuProps, BubbleMenuItem } from './BubbleMenu'; -export { LinkPopover } from './LinkPopover'; -export type { LinkPopoverProps } from './LinkPopover'; - export { ColorPicker, DEFAULT_TEXT_COLORS, DEFAULT_BACKGROUND_COLORS } from './ColorPicker'; export type { ColorPickerProps, ColorOption } from './ColorPicker'; diff --git a/packages/react/src/hooks/useOpenBlock.ts b/packages/react/src/hooks/useOpenBlock.ts index f9dcdf8..04cbcd0 100644 --- a/packages/react/src/hooks/useOpenBlock.ts +++ b/packages/react/src/hooks/useOpenBlock.ts @@ -5,7 +5,7 @@ * * @example * ```tsx - * import { useOpenBlock, OpenBlockView } from '@openblock/react'; + * import { useOpenBlock, OpenBlockView } from '@labbs/openblock-react'; * * function MyEditor() { * const editor = useOpenBlock({ @@ -106,9 +106,10 @@ export function useOpenBlock(options: UseOpenBlockOptions = {}): OpenBlockEditor prosemirror: editorOptions.prosemirror, // Stable wrappers that delegate to the latest callbacks provided to the // hook, so consumers can pass inline functions without them being - // frozen at mount time. - onUpdate: (blocks) => optionsRef.current.onUpdate?.(blocks), - onSelectionChange: (blocks) => optionsRef.current.onSelectionChange?.(blocks), + // frozen at mount time. onUpdate / onSelectionChange are subscribed + // separately below, only while provided. + onUpdate: undefined, + onSelectionChange: undefined, onFocus: () => optionsRef.current.onFocus?.(), onBlur: () => optionsRef.current.onBlur?.(), }); @@ -134,6 +135,24 @@ export function useOpenBlock(options: UseOpenBlockOptions = {}): OpenBlockEditor }; }, []); + // The editor only converts the document to blocks (a full-document walk + // and allocation) when someone listens to it: subscribe only while the + // callbacks are actually provided, instead of on every keystroke. + const hasOnUpdate = options.onUpdate !== undefined; + const hasOnSelectionChange = options.onSelectionChange !== undefined; + + useEffect(() => { + if (!editor || !hasOnUpdate) return; + return editor.on('change', ({ blocks }) => optionsRef.current.onUpdate?.(blocks)); + }, [editor, hasOnUpdate]); + + useEffect(() => { + if (!editor || !hasOnSelectionChange) return; + return editor.on('selectionChange', ({ blocks }) => + optionsRef.current.onSelectionChange?.(blocks) + ); + }, [editor, hasOnSelectionChange]); + return editor; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1cf87cf..50fee97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,19 +10,22 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.14.0) + version: 10.0.1(eslint@10.14.0(jiti@2.7.0)) eslint: specifier: ^10.14.0 - version: 10.14.0 + version: 10.14.0(jiti@2.7.0) globals: specifier: ^17.14.0 version: 17.14.0 + knip: + specifier: ^6.44.1 + version: 6.44.1 typescript: specifier: ~6.0.3 version: 6.0.3 typescript-eslint: specifier: ^8.72.1 - version: 8.72.1(eslint@10.14.0)(typescript@6.0.3) + version: 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) examples/basic: dependencies: @@ -47,13 +50,13 @@ importers: version: 19.3.0(@types/react@19.3.0) '@vitejs/plugin-react': specifier: ^6.2.1 - version: 6.2.1(vite@8.3.6) + version: 6.2.1(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1)) typescript: specifier: ~6.0.3 version: 6.0.3 vite: specifier: ^8.3.6 - version: 8.3.6 + version: 8.3.6(jiti@2.7.0)(yaml@2.9.1) packages/core: dependencies: @@ -84,9 +87,6 @@ importers: prosemirror-state: specifier: ^1.4.4 version: 1.4.4 - prosemirror-transform: - specifier: ^1.12.5 - version: 1.12.5 prosemirror-view: specifier: ^1.42.9 version: 1.42.9 @@ -105,10 +105,10 @@ importers: version: 6.0.3 vite: specifier: ^8.3.6 - version: 8.3.6 + version: 8.3.6(jiti@2.7.0)(yaml@2.9.1) vite-plugin-dts: specifier: ^5.1.2 - version: 5.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6) + version: 5.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1)) packages/react: dependencies: @@ -136,10 +136,10 @@ importers: version: 6.0.3 vite: specifier: ^8.3.6 - version: 8.3.6 + version: 8.3.6(jiti@2.7.0)(yaml@2.9.1) vite-plugin-dts: specifier: ^5.1.2 - version: 5.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6) + version: 5.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1)) packages: @@ -149,6 +149,15 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@eslint-community/eslint-utils@4.10.1': resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -248,9 +257,244 @@ packages: '@microsoft/tsdoc@0.17.1': resolution: {integrity: sha512-VHyYyyoH63jFoO3shpw9Zfijk5v/9p/VrClW35z0/q6ooK7sSmxpzOkS4iZPxs3vO4XtBW4H38HRAKkAMIK7DA==} + '@napi-rs/wasm-runtime@1.2.7': + resolution: {integrity: sha512-zJccwplOAbgfZju95IUHazLnx9RAPtPN2YGJXI6UbrgeaR9QfPjGrQL6ERkSAtnVCA4Q+83ApGyaX5zdpCaKkQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3 + '@emnapi/runtime': ^1.7.1 || 2.0.0-beta.2 || 2.0.0-beta.3 + + '@oxc-parser/binding-android-arm-eabi@0.155.0': + resolution: {integrity: sha512-A6xv7hDtnTm+xlZ7utn6rRQpRMJHRhVr3gjRGqlx0RUX84mNk0pAsy6YN/wWhuOTkaZhRrrzaLxf0NfbH77j2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.155.0': + resolution: {integrity: sha512-T1wWwH9/JI5g7Z4kEQwDOwhToxdDhRyOTklH+uuNpJ16m3MZFmGl+l2rKc0NHQdbVRG2LTOBsNjpW3h1BWytPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.155.0': + resolution: {integrity: sha512-ZoRB3qJxJ8DWA41jHja+YeB0lLfjBy4T+Kr15vnN1k65X3zbJt3rPZftEwN4Ju4tmA4K/T9dWWD7Z8ewQLnrVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.155.0': + resolution: {integrity: sha512-vxtL86BtM+IgrpejA86nbTk5j0HKTIMeuaJ1fQciw6p9RfavPtIPjI7gN34owap0yG9txXBaAWpGdxokNwdqIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.155.0': + resolution: {integrity: sha512-tN6Ru5e0UVnWaIuiH8jEwnvxuxg7FvhWwSd3oe5oDhOD4ObsSr9/N6WfHLjilUWlPPj65ptwrSDH0Y7lzwTzmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.155.0': + resolution: {integrity: sha512-h4ozA5n6L6lwolsz9iwunEhKWnIVbmFRots5QxEix0H6nJ5ICpuDa7PuShuhfpIDFe3+Qvyscioyiv+9g+4Syg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.155.0': + resolution: {integrity: sha512-058/+LardpfuUlsKIvBGk/CYoakEVcMgqYx8GgyUZeOxFWTojYLlCc1Gb60/y21v3esDhHidgiY/Zbq9LpBhaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.155.0': + resolution: {integrity: sha512-nrOt2Y3xIAzu+lSPzH0864ieO+iCCXgoo9qFlGANE8Xh0cQ+RP8Wstfp+/wXN2YCyb98cAGzX0VruKYw7it/jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-arm64-musl@0.155.0': + resolution: {integrity: sha512-KYAayyN5hQbJPtGKtRxlyc8XYmyntOplyXSQzTDrmbz7l/GCrzQbSswvyJFBTH1LGtsZrbXOAfN52i/Kz6kLKg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-ppc64-gnu@0.155.0': + resolution: {integrity: sha512-CSuSY5hfjtsBnHXydOj8jiNfS1qHbdPZuPz1YL1cSgK7VmNYP+QlZIbG0BBYYBgX/vYEgZwbG9mBdW4YairZ5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-gnu@0.155.0': + resolution: {integrity: sha512-7KCfOQm+Nz2N8trAXbQkal4TQ5756WVEmhm35y816YHvyFnEfVsICsiV5k0pEszNf/fRm4cm2i6i18SJ6/iCWg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-riscv64-musl@0.155.0': + resolution: {integrity: sha512-JSlhgoK+/g1TGJI5NuADx2+6syhxnQNLCWLXyO5QLFyetomcr/vIxSdiNlmyAv963YXUzCuwAwRG6npZ5xyJ1A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-linux-s390x-gnu@0.155.0': + resolution: {integrity: sha512-WFIX+dPokWp64mi434Md3U3lJaTwNxAphvGjpltoyQjGUsdXavOIRrnp9dOS34Hd75HOAwDxAI+YwWOwqmJbtg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-gnu@0.155.0': + resolution: {integrity: sha512-g6xSDC/JpbnVOz5uafMGyK3txBVRFMQTx8opv/yHXGxqV3ZMVl3bf7W8XrSnF68dmy3gM4dgReNryuE53+WkeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-parser/binding-linux-x64-musl@0.155.0': + resolution: {integrity: sha512-6/4xlxKlZJWrQ0uwDFPc0H9+xLu7068CKhX3vIDW3SbvP/lVgnVuT5v6dyugeWskWpuxqF/WJ4WKwhxjU3pG2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-parser/binding-openharmony-arm64@0.155.0': + resolution: {integrity: sha512-tltklL0J0XjDxIRTBXWOBhvc2mitcmES45pIxhwDWfxRyqz7Vkeu0NekTZEIxGE4f/lXgq3vZcjjffteQOVorg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-win32-arm64-msvc@0.155.0': + resolution: {integrity: sha512-alAb9TzjatUEsOeoncAziUnfdTDxfyswhalFZ9sC6plUgATIUx2NIZ0+bMn6aJ28AduS1OpG/OAisr/YISn4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.155.0': + resolution: {integrity: sha512-yc5cbSfcGmRv/KpxDFuyk8adEBaWd/9HilWvBbQ4bFbus1V2PUh2mGB2Cfq9rqZTqLeiMT8x0skV+LKp9hOMgQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.155.0': + resolution: {integrity: sha512-8heq6jGq+rfpTDRDjoQiBHw0maQZf3IXkS99pdHQZPoTL5kyF5qyPS0RidUlSdnWDIqxXb5nux7GBD9cT+imUw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.155.0': + resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} + '@oxc-project/types@0.156.0': resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] + '@rolldown/binding-android-arm-eabi@1.2.16': resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -393,6 +637,9 @@ packages: resolution: {integrity: sha512-acP68tGx/DJEIQgTqj/xStBRaghN7axV+uAjNasDjVXgHgHaLQ1f16/bbXtw6mNLjURhTORD4fdfbOZM7kyWcw==} engines: {node: '>=20.9.0'} + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -663,6 +910,9 @@ packages: fast-uri@3.1.9: resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==} + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -685,6 +935,11 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + formatly@0.11.4: + resolution: {integrity: sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA==} + engines: {node: '>=18.14.0'} + hasBin: true + fs-extra@11.3.6: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} @@ -697,6 +952,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -763,6 +1021,10 @@ packages: isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} @@ -781,6 +1043,11 @@ packages: keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + knip@6.44.1: + resolution: {integrity: sha512-Hbd8nvkIsNgFfXgC7Jsb5LHv7jj6VwA3tmfHd53XPX2z9Vc9RnAZqV0BIiP4OrtYqKwCoLseFHQZerb1bNlbSw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -907,6 +1174,13 @@ packages: orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + oxc-parser@0.155.0: + resolution: {integrity: sha512-C/ReSf1JUFukK/zvk6jhcNibSssvas5+ktIzK7fbKVd3CedWe59C3QwHEOrEZj/4Rt9+xg0Jeis/Jv1F/KM1mw==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -915,6 +1189,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-manager-detector@1.9.0: + resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -1010,6 +1287,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.14: resolution: {integrity: sha512-1ZjYvR9sbkiwC5+9QHetqW5HmA2pHRMaoupD01t2HlYnV/vydC2zPwLixhsMieSWVZt0JJRg5LVIk0Ow1D/TrA==} engines: {node: '>= 0.4'} @@ -1044,6 +1324,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + smol-toml@1.9.1: + resolution: {integrity: sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==} + engines: {node: '>= 18'} + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} @@ -1059,6 +1343,10 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} @@ -1077,6 +1365,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + tslib@2.9.1: + resolution: {integrity: sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1096,6 +1387,10 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + unbash@6.0.3: + resolution: {integrity: sha512-hRzSpMeM8uYMQkrnIVaVy3f44oLlokglVeEyP5hIQj88z60bKPO9VVbEpGfxChGBh9lBkzuWKOTOzPdfD6KNEA==} + engines: {node: '>=14'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -1213,6 +1508,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -1241,6 +1540,11 @@ packages: peerDependencies: yjs: ^13.0.0 + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} + engines: {node: '>= 14.6'} + hasBin: true + yjs@13.6.33: resolution: {integrity: sha512-q/UYvr1gOk4XII+Cu1CW2dINjYqJJFifTzogEaIARznrvX7uOlHsdTEXcnOBMuIsrCGEEKdNxi74kyMLZlKVfw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -1249,6 +1553,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + snapshots: '@cacheable/memory@2.2.0': @@ -1263,9 +1570,25 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 - '@eslint-community/eslint-utils@4.10.1(eslint@10.14.0)': + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.9.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.9.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': dependencies: - eslint: 10.14.0 + tslib: 2.9.1 + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.14.0(jiti@2.7.0))': + dependencies: + eslint: 10.14.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1286,9 +1609,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.14.0)': + '@eslint/js@10.0.1(eslint@10.14.0(jiti@2.7.0))': optionalDependencies: - eslint: 10.14.0 + eslint: 10.14.0(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -1375,8 +1698,135 @@ snapshots: '@microsoft/tsdoc@0.17.1': {} + '@napi-rs/wasm-runtime@1.2.7(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.155.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.155.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.155.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.155.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.155.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.155.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.155.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.155.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.155.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.155.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.155.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.155.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.155.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.155.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.155.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.155.0': + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.155.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.155.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.155.0': + optional: true + + '@oxc-project/types@0.155.0': {} + '@oxc-project/types@0.156.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.2.7(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + '@rolldown/binding-android-arm-eabi@1.2.16': optional: true @@ -1463,6 +1913,11 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.9.1 + optional: true + '@types/argparse@1.0.38': {} '@types/esrecurse@4.3.1': {} @@ -1479,15 +1934,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.72.1(@typescript-eslint/parser@8.72.1(eslint@10.14.0)(typescript@6.0.3))(eslint@10.14.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.72.1(@typescript-eslint/parser@8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.72.1 - '@typescript-eslint/type-utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.72.1 - eslint: 10.14.0 + eslint: 10.14.0(jiti@2.7.0) ignore: 7.0.14 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -1495,14 +1950,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.72.1(eslint@10.14.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.72.1 '@typescript-eslint/types': 8.72.1 '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.72.1 debug: 4.4.3 - eslint: 10.14.0 + eslint: 10.14.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1525,13 +1980,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.72.1(eslint@10.14.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.72.1 '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.14.0 + eslint: 10.14.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -1554,13 +2009,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.72.1(eslint@10.14.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.14.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.14.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.72.1 '@typescript-eslint/types': 8.72.1 '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) - eslint: 10.14.0 + eslint: 10.14.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1570,10 +2025,10 @@ snapshots: '@typescript-eslint/types': 8.72.1 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.2.1(vite@8.3.6)': + '@vitejs/plugin-react@6.2.1(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.6 + vite: 8.3.6(jiti@2.7.0)(yaml@2.9.1) '@volar/language-core@2.4.31': dependencies: @@ -1691,9 +2146,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.14.0: + eslint@10.14.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.14.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.14.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -1723,6 +2178,8 @@ snapshots: minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -1756,6 +2213,10 @@ snapshots: fast-uri@3.1.9: {} + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -1777,6 +2238,11 @@ snapshots: flatted@3.4.4: {} + formatly@0.11.4: + dependencies: + fd-package-json: 2.0.0 + package-manager-detector: 1.9.0 + fs-extra@11.3.6: dependencies: graceful-fs: 4.2.11 @@ -1788,6 +2254,10 @@ snapshots: function-bind@1.1.2: {} + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -1834,6 +2304,8 @@ snapshots: isomorphic.js@0.2.5: {} + jiti@2.7.0: {} + jju@1.4.0: {} json-schema-traverse@0.4.1: {} @@ -1852,6 +2324,21 @@ snapshots: dependencies: '@keyv/serialize': 1.1.1 + knip@6.44.1: + dependencies: + formatly: 0.11.4 + get-tsconfig: 4.14.3 + jiti: 2.7.0 + oxc-parser: 0.155.0 + oxc-resolver: 11.24.2 + picomatch: 4.0.7 + smol-toml: 1.9.1 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 6.0.3 + yaml: 2.9.1 + zod: 4.6.7 + kolorist@1.8.0: {} levn@0.4.1: @@ -1958,6 +2445,52 @@ snapshots: orderedmap@2.1.1: {} + oxc-parser@0.155.0: + dependencies: + '@oxc-project/types': 0.155.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.155.0 + '@oxc-parser/binding-android-arm64': 0.155.0 + '@oxc-parser/binding-darwin-arm64': 0.155.0 + '@oxc-parser/binding-darwin-x64': 0.155.0 + '@oxc-parser/binding-freebsd-x64': 0.155.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.155.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.155.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.155.0 + '@oxc-parser/binding-linux-arm64-musl': 0.155.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.155.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.155.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.155.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.155.0 + '@oxc-parser/binding-linux-x64-gnu': 0.155.0 + '@oxc-parser/binding-linux-x64-musl': 0.155.0 + '@oxc-parser/binding-openharmony-arm64': 0.155.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.155.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.155.0 + '@oxc-parser/binding-win32-x64-msvc': 0.155.0 + + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -1966,6 +2499,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-manager-detector@1.9.0: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -2079,6 +2614,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.14: dependencies: es-errors: 1.3.0 @@ -2121,6 +2658,8 @@ snapshots: shebang-regex@3.0.0: {} + smol-toml@1.9.1: {} + source-map-js@1.2.2: {} source-map@0.6.1: {} @@ -2129,6 +2668,8 @@ snapshots: string-argv@0.3.2: {} + strip-json-comments@5.0.3: {} + supports-color@8.1.1: dependencies: has-flag: 4.0.0 @@ -2144,17 +2685,20 @@ snapshots: dependencies: typescript: 6.0.3 + tslib@2.9.1: + optional: true + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.72.1(eslint@10.14.0)(typescript@6.0.3): + typescript-eslint@8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/parser@8.72.1(eslint@10.14.0)(typescript@6.0.3))(eslint@10.14.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.72.1(eslint@10.14.0)(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/parser@8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.72.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.72.1(eslint@10.14.0)(typescript@6.0.3) - eslint: 10.14.0 + '@typescript-eslint/utils': 8.72.1(eslint@10.14.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.14.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -2163,9 +2707,11 @@ snapshots: ufo@1.6.4: {} + unbash@6.0.3: {} + universalify@2.0.1: {} - unplugin-dts@1.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6): + unplugin-dts@1.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1)): dependencies: '@rollup/pluginutils': 5.5.1 '@volar/typescript': 2.4.31(typescript@6.0.3) @@ -2180,7 +2726,7 @@ snapshots: optionalDependencies: '@microsoft/api-extractor': 7.60.1 rolldown: 1.2.16 - vite: 8.3.6 + vite: 8.3.6(jiti@2.7.0)(yaml@2.9.1) transitivePeerDependencies: - supports-color @@ -2195,12 +2741,12 @@ snapshots: dependencies: punycode: 2.3.1 - vite-plugin-dts@5.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6): + vite-plugin-dts@5.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1)): dependencies: - unplugin-dts: 1.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6) + unplugin-dts: 1.1.2(@microsoft/api-extractor@7.60.1)(rolldown@1.2.16)(typescript@6.0.3)(vite@8.3.6(jiti@2.7.0)(yaml@2.9.1)) optionalDependencies: '@microsoft/api-extractor': 7.60.1 - vite: 8.3.6 + vite: 8.3.6(jiti@2.7.0)(yaml@2.9.1) transitivePeerDependencies: - '@rspack/core' - '@vue/language-core' @@ -2210,7 +2756,7 @@ snapshots: - typescript - webpack - vite@8.3.6: + vite@8.3.6(jiti@2.7.0)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -2219,6 +2765,8 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.9.1 vscode-jsonrpc@10.0.0: {} @@ -2235,6 +2783,8 @@ snapshots: w3c-keyname@2.2.8: {} + walk-up-path@4.0.0: {} + webpack-virtual-modules@0.6.2: {} which@2.0.2: @@ -2257,8 +2807,12 @@ snapshots: lib0: 0.2.119 yjs: 13.6.33 + yaml@2.9.1: {} + yjs@13.6.33: dependencies: lib0: 0.2.119 yocto-queue@0.1.0: {} + + zod@4.6.7: {} diff --git a/scripts/check-unused-css.mjs b/scripts/check-unused-css.mjs new file mode 100644 index 0000000..b2730e2 --- /dev/null +++ b/scripts/check-unused-css.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +/** + * Dead CSS check: fails when the editor stylesheet defines class selectors + * that no source file uses. Complements knip, which covers the TypeScript + * side (unused files, exports, dependencies). + * + * A class counts as used when: + * - its name appears as a whole word in a .ts / .tsx source file; + * - it is a BEM modifier (`block--modifier`) of a used block, and the source + * builds that modifier dynamically: `block--${...}` or `${...}--modifier`; + * - it is set by ProseMirror itself (`ProseMirror-*`). + * + * Usage: node scripts/check-unused-css.mjs + */ +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; + +const root = join(import.meta.dirname, '..'); +const stylesheets = ['packages/core/src/styles/editor.css']; +const sourceDirs = ['packages/core/src', 'packages/react/src']; + +function listSources(dir) { + return readdirSync(dir).flatMap((name) => { + const path = join(dir, name); + if (statSync(path).isDirectory()) return listSources(path); + return /\.tsx?$/.test(name) && !name.endsWith('.d.ts') ? [path] : []; + }); +} + +const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +const sources = sourceDirs + .flatMap((dir) => listSources(join(root, dir))) + .map((file) => readFileSync(file, 'utf8')) + .join('\n'); +const mentioned = (pattern) => new RegExp(pattern).test(sources); +const wordUsed = (name) => mentioned(`(? 0) { + const block = cls.slice(0, sep); + const modifier = cls.slice(sep + 2); + if (!wordUsed(block)) return false; + return mentioned(`${escape(block)}--\\$\\{`) || mentioned(`\\}--${escape(modifier)}(?![\\w-])`); + } + return false; +} + +let failed = false; +for (const sheet of stylesheets) { + const css = readFileSync(join(root, sheet), 'utf8') + .replace(/\/\*[\s\S]*?\*\//g, '') // comments + .replace(/(["'])(?:\\.|(?!\1).)*\1/g, '""'); // strings (content: "...", url("...")) + + const classes = new Set(); + for (const [, selector] of css.matchAll(/([^{};]+)\{/g)) { + if (selector.trim().startsWith('@')) continue; // @media / @keyframes params + for (const [, cls] of selector.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) classes.add(cls); + } + + const unused = [...classes].filter((cls) => !isUsed(cls)).sort(); + if (unused.length > 0) { + failed = true; + console.error(`${relative(root, join(root, sheet))}: ${unused.length} unused class(es)`); + for (const cls of unused) console.error(` .${cls}`); + } else { + console.log(`${sheet}: all ${classes.size} classes are used`); + } +} + +process.exit(failed ? 1 : 0);