Harden oxlint: consistent-type-imports, no-duplicates, no-misused-promises, prefer-optional-chain

Add 6 new lint rules: typescript/consistent-type-imports,
typescript/no-unnecessary-type-assertion, typescript/prefer-optional-chain,
typescript/no-misused-promises, import/no-duplicates, import/no-mutable-exports.
Ban @/types shim via no-restricted-imports.

Fix all violations: replace inline import() annotations with proper
type imports, merge duplicate module imports, add void to fire-and-forget
promises, convert && chains to optional chaining.
This commit is contained in:
Danila Poyarkov 2026-03-09 09:05:04 +03:00
parent 33389a95ad
commit f96fffb83f
13 changed files with 58 additions and 39 deletions

View file

@ -8,13 +8,29 @@
"rules": {
"no-unused-vars": "warn",
"no-console": "off",
"no-restricted-imports": ["error", {
"paths": [
{ "name": "@/types", "message": "Import types directly from @open-pencil/core." }
],
"patterns": [{
"group": ["@/engine/*"],
"message": "Import from @open-pencil/core unless the module adds platform-specific logic.",
"allowImportNames": ["loadFont", "listFamilies", "preloadFonts"]
}]
}],
"typescript/no-explicit-any": "warn",
"typescript/no-non-null-assertion": "warn",
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "error",
"typescript/consistent-type-imports": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/prefer-optional-chain": "error",
"import/no-cycle": "error",
"import/no-self-import": "error",
"import/no-duplicates": "error",
"import/no-mutable-exports": "error",
"unicorn/no-instanceof-array": "error",
"unicorn/no-typeof-undefined": "error",

View file

@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import type {
ListboxFilter} from 'reka-ui';
import {
ListboxContent,
ListboxFilter,
ListboxItem,
ListboxRoot,
ListboxVirtualizer,

View file

@ -10,7 +10,7 @@ import IconComponent from '~icons/lucide/diamond'
import IconComponentSet from '~icons/lucide/component'
import IconFrame from '~icons/lucide/frame'
import IconGroup from '~icons/lucide/group'
import IconInstance from '~icons/lucide/diamond'
import IconMinus from '~icons/lucide/minus'
import IconPenTool from '~icons/lucide/pen-tool'
import IconSection from '~icons/lucide/layout-grid'
@ -38,7 +38,7 @@ const nodeIcons: Record<string, typeof IconSquare> = {
GROUP: IconGroup,
COMPONENT: IconComponent,
COMPONENT_SET: IconComponentSet,
INSTANCE: IconInstance,
INSTANCE: IconComponent,
LINE: IconMinus,
TEXT: IconType,
VECTOR: IconPenTool,

View file

@ -26,11 +26,11 @@ import IconLock from '~icons/lucide/lock'
import { menuContent, menuItem } from '@/components/ui/menu'
import { ACTION_TOAST_DURATION } from '@/constants'
import { TOOLS, useEditorStore } from '@/stores/editor'
import { useEditorStore } from '@/stores/editor'
import { toolIcons } from '@/utils/tools'
import type { Component } from 'vue'
import type { Tool } from '@/stores/editor'
import type { Tool , TOOLS} from '@/stores/editor'
const store = useEditorStore()
const breakpoints = useBreakpoints({ mobile: 768 })

View file

@ -26,7 +26,7 @@ import IconX from '~icons/lucide/x'
import ColorInput from './ColorInput.vue'
import { colorToHexRaw, parseColor } from '@open-pencil/core'
import { useEditorStore } from '@/stores/editor'
import type { Variable, Color } from '@open-pencil/core'
import type { Variable, VariableCollection, VariableValue, Color } from '@open-pencil/core'
const open = defineModel<boolean>('open', { default: false })
const store = useEditorStore()
@ -155,7 +155,7 @@ function updateColorValue(variable: Variable, modeId: string, color: Color) {
function commitValueEdit(variable: Variable, modeId: string, newValue: string) {
const oldValue = structuredClone(variable.valuesByMode[modeId])
let parsed: import('@open-pencil/core').VariableValue
let parsed: VariableValue
if (variable.type === 'COLOR') {
parsed = parseColor(newValue.startsWith('#') ? newValue : `#${newValue}`)
} else if (variable.type === 'FLOAT') {
@ -187,7 +187,7 @@ function addVariable() {
if (!col) return
const id = `var:${Date.now()}`
const valuesByMode: Record<string, import('@open-pencil/core').VariableValue> = {}
const valuesByMode: Record<string, VariableValue> = {}
for (const mode of col.modes) {
valuesByMode[mode.modeId] = { r: 0, g: 0, b: 0, a: 1 }
}
@ -218,7 +218,7 @@ function addVariable() {
function addCollection() {
const id = `col:${Date.now()}`
const collection: import('@open-pencil/core').VariableCollection = {
const collection: VariableCollection = {
id,
name: 'New collection',
modes: [{ modeId: 'default', name: 'Mode 1' }],

View file

@ -15,8 +15,7 @@ import {
DropdownMenuPortal
} from 'reka-ui'
import type { Color, Stroke } from '@open-pencil/core'
import type { SceneNode } from '@open-pencil/core'
import type { Color, SceneNode, Stroke } from '@open-pencil/core'
type StrokeSides = 'ALL' | 'TOP' | 'BOTTOM' | 'LEFT' | 'RIGHT' | 'CUSTOM'

View file

@ -223,8 +223,8 @@ function hitTestRotationHandle(
export function useCanvasInput(
canvasRef: Ref<HTMLCanvasElement | null>,
store: EditorStore,
hitTestSectionTitle: (cx: number, cy: number) => import('@open-pencil/core').SceneNode | null,
hitTestComponentLabel: (cx: number, cy: number) => import('@open-pencil/core').SceneNode | null,
hitTestSectionTitle: (cx: number, cy: number) => SceneNode | null,
hitTestComponentLabel: (cx: number, cy: number) => SceneNode | null,
onCursorMove?: (cx: number, cy: number) => void
) {
const drag = ref<DragState | null>(null)

View file

@ -1,8 +1,7 @@
import { useBreakpoints, useRafFn, useResizeObserver } from '@vueuse/core'
import { onMounted, onUnmounted, type Ref } from 'vue'
import { getCanvasKit, getGpuBackend } from '@open-pencil/core'
import { SkiaRenderer } from '@open-pencil/core'
import { getCanvasKit, getGpuBackend, SkiaRenderer } from '@open-pencil/core'
import type { EditorStore } from '@/stores/editor'
import type { CanvasKit } from 'canvaskit-wasm'
@ -118,7 +117,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
}
}
const glCtx = (canvas.getContext('webgl2') ?? null) as WebGL2RenderingContext | null
const glCtx = (canvas.getContext('webgl2') ?? null)
renderer = new SkiaRenderer(ck, surface, glCtx)
store.setCanvasKit(ck, renderer)
void renderer.loadFonts().then(() => renderNow())

View file

@ -110,9 +110,9 @@ export function useCollab(store: EditorStore) {
const [sendSync, getSync] = room.makeAction<Uint8Array>('sync-step1')
const [sendSyncReply, getSyncReply] = room.makeAction<Uint8Array>('sync-reply')
sendYjsUpdate = (data, peerId) => (peerId ? sendUpdate(data, peerId) : sendUpdate(data))
sendAwareness = (data, peerId) => (peerId ? sendAw(data, peerId) : sendAw(data))
sendSyncStep1 = (data, peerId) => (peerId ? sendSync(data, peerId) : sendSync(data))
sendYjsUpdate = (data, peerId) => void (peerId ? sendUpdate(data, peerId) : sendUpdate(data))
sendAwareness = (data, peerId) => void (peerId ? sendAw(data, peerId) : sendAw(data))
sendSyncStep1 = (data, peerId) => void (peerId ? sendSync(data, peerId) : sendSync(data))
getUpdate((data) => {
if (!ydoc) return
@ -286,7 +286,7 @@ export function useCollab(store: EditorStore) {
}
}
} else if (event.target.parent === localYnodes) {
const nodeId = findNodeIdForYMap(event.target as Y.Map<unknown>)
const nodeId = findNodeIdForYMap(event.target)
if (nodeId) {
const ynode = localYnodes.get(nodeId)
if (ynode) applyYnodeToGraph(nodeId, ynode)

View file

@ -138,7 +138,7 @@ export function useKeyboard() {
computed(() => keys['shift+keya'].value && !keys['meta'].value && !keys['control'].value),
() => {
const node = store.selectedNode.value
if (node && node.type === 'FRAME' && store.selectedNodes.value.length === 1) {
if (node?.type === 'FRAME' && store.selectedNodes.value.length === 1) {
store.setLayoutMode(node.id, node.layoutMode === 'NONE' ? 'VERTICAL' : 'NONE')
} else if (store.selectedNodes.value.length > 0) {
store.wrapInAutoLayout()

View file

@ -13,9 +13,9 @@ export async function openFileDialog() {
multiple: false
})
if (!path) return
const bytes = await readFile(path as string)
const file = new File([bytes], (path as string).split('/').pop() ?? 'file.fig')
await openFileInNewTab(file, undefined, path as string)
const bytes = await readFile(path)
const file = new File([bytes], (path).split('/').pop() ?? 'file.fig')
await openFileInNewTab(file, undefined, path)
return
}
@ -51,12 +51,12 @@ const store = useEditorStore()
const MENU_ACTIONS: Record<string, () => void> = {
new: () => createTab(),
open: () => openFileDialog(),
open: () => void openFileDialog(),
close: () => {
if (activeTab.value) closeTab(activeTab.value.id)
},
save: () => store.saveFigFile(),
'save-as': () => store.saveFigFileAs(),
save: () => void store.saveFigFile(),
'save-as': () => void store.saveFigFileAs(),
duplicate: () => store.duplicateSelected(),
delete: () => store.deleteSelected(),
group: () => store.groupSelected(),

View file

@ -25,6 +25,7 @@ import {
buildFigmaClipboardHTML,
buildOpenPencilClipboardHTML,
prefetchFigmaSchema,
readFigFile,
renderNodesToImage,
renderNodesToSVG,
SceneGraph,
@ -32,7 +33,6 @@ import {
TextEditor,
UndoManager
} from '@open-pencil/core'
import { readFigFile } from '@open-pencil/core'
import type {
Color,
@ -42,12 +42,14 @@ import type {
NodeType,
Rect,
SceneNode,
SkiaRenderer,
SnapGuide,
VectorNetwork,
VectorRegion,
VectorSegment,
VectorVertex
} from '@open-pencil/core'
import type { CanvasKit } from 'canvaskit-wasm'
export type Tool =
| 'SELECT'
@ -131,8 +133,8 @@ export function createEditorStore() {
let autosaveTimer: ReturnType<typeof setTimeout> | undefined
let lastWriteTime = 0
let unwatchFile: (() => void) | null = null
let _ck: import('canvaskit-wasm').CanvasKit | null = null
let _renderer: import('@open-pencil/core').SkiaRenderer | null = null
let _ck: CanvasKit | null = null
let _renderer: SkiaRenderer | null = null
let _textEditor: TextEditor | null = null
void prefetchFigmaSchema()
@ -211,6 +213,7 @@ export function createEditorStore() {
if (!state.autosaveEnabled) return
if (!fileHandle && !filePath) return
clearTimeout(autosaveTimer)
// oxlint-disable-next-line typescript/no-misused-promises
autosaveTimer = setTimeout(async () => {
if (state.sceneVersion === savedVersion) return
if (!state.autosaveEnabled) return
@ -273,7 +276,7 @@ export function createEditorStore() {
function switchPage(pageId: string) {
const page = graph.getNode(pageId)
if (!page || page.type !== 'CANVAS') return
if (page?.type !== 'CANVAS') return
// Save current viewport
pageViewports.set(state.currentPageId, {
@ -626,8 +629,8 @@ export function createEditorStore() {
}
function setCanvasKit(
ck: import('canvaskit-wasm').CanvasKit,
renderer: import('@open-pencil/core').SkiaRenderer
ck: CanvasKit,
renderer: SkiaRenderer
) {
_ck = ck
_renderer = renderer
@ -778,6 +781,7 @@ export function createEditorStore() {
unwatchFile = () => unwatch()
} else if (fileHandle) {
let lastModified = (await fileHandle.getFile()).lastModified
// oxlint-disable-next-line typescript/no-misused-promises
const interval = setInterval(async () => {
if (!fileHandle) {
clearInterval(interval)
@ -1396,7 +1400,7 @@ export function createEditorStore() {
function createInstanceFromComponent(componentId: string, x?: number, y?: number) {
const component = graph.getNode(componentId)
if (!component || component.type !== 'COMPONENT') return null
if (component?.type !== 'COMPONENT') return null
const parentId = component.parentId ?? state.currentPageId
const instance = graph.createInstance(componentId, parentId, {
@ -1427,7 +1431,7 @@ export function createEditorStore() {
function detachInstance() {
const node = selectedNode.value
if (!node || node.type !== 'INSTANCE') return
if (node?.type !== 'INSTANCE') return
const prevComponentId = node.componentId
@ -1475,7 +1479,7 @@ export function createEditorStore() {
function ungroupSelected() {
const node = selectedNode.value
if (!node || node.type !== 'GROUP') return
if (node?.type !== 'GROUP') return
const parentId = node.parentId ?? state.currentPageId
const parent = graph.getNode(parentId)
@ -1582,7 +1586,7 @@ export function createEditorStore() {
function moveToPage(pageId: string) {
const targetPage = graph.getNode(pageId)
if (!targetPage || targetPage.type !== 'CANVAS') return
if (targetPage?.type !== 'CANVAS') return
const ids = [...state.selectedIds]
for (const id of ids) {
graph.reparentNode(id, pageId)
@ -1647,7 +1651,7 @@ export function createEditorStore() {
function adoptNodesIntoSection(sectionId: string) {
const section = graph.getNode(sectionId)
if (!section || section.type !== 'SECTION') return
if (section?.type !== 'SECTION') return
const parentId = section.parentId ?? state.currentPageId
const siblings = graph.getChildren(parentId)

View file

@ -79,7 +79,7 @@ export async function openFileInNewTab(
): Promise<void> {
const current = activeTab.value
const isUntouched =
current && current.store.state.documentName === 'Untitled' && !current.store.undo.canUndo
current?.store.state.documentName === 'Untitled' && !current.store.undo.canUndo
if (isUntouched) {
await current.store.openFigFile(file, handle, path)