diff --git a/oxlint.json b/oxlint.json index 412edd6f4..9dd0839ca 100644 --- a/oxlint.json +++ b/oxlint.json @@ -92,7 +92,13 @@ "files": ["**/kiwi/kiwi-schema/**"], "rules": { "typescript/no-explicit-any": "off", - "typescript/no-non-null-assertion": "off" + "typescript/no-non-null-assertion": "off", + "typescript/prefer-for-of": "off", + "typescript/prefer-optional-chain": "off", + "typescript/consistent-type-imports": "off", + "typescript/no-unnecessary-condition": "off", + "import/no-mutable-exports": "off", + "complexity": "off" } }, { @@ -110,6 +116,12 @@ "rules": { "open-pencil/no-raw-console-format": "error" } + }, + { + "files": ["packages/core/src/**"], + "rules": { + "complexity": "warn" + } } ], "ignorePatterns": ["node_modules", "dist", "desktop", "*.config.*"] diff --git a/package.json b/package.json index 13fb4a3b5..07cb9c096 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build": "bun run lint && vite build", "preview": "vite preview", "tauri": "tauri", - "lint": "oxlint -c oxlint.json --type-aware --type-check src/ packages/cli/src/", + "lint": "oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/acp/src/", "format": "oxfmt --write src/", "check": "bun run lint", "test": "playwright test --project=openpencil", diff --git a/packages/acp/src/agent.ts b/packages/acp/src/agent.ts index 255c29af1..733e4c26c 100644 --- a/packages/acp/src/agent.ts +++ b/packages/acp/src/agent.ts @@ -152,7 +152,7 @@ export function createAgent( commands.push({ name: bt.name, description: bt.description, - input: { type: 'unstructured' } + input: { hint: 'JSON arguments or key=value pairs' } }) } @@ -160,7 +160,7 @@ export function createAgent( commands.push({ name: def.name, description: def.description, - input: { type: 'unstructured' } + input: { hint: 'JSON arguments or key=value pairs' } }) } @@ -216,7 +216,7 @@ export function createAgent( const signal = session.pendingPrompt.signal const userText = params.prompt - .filter((b): b is acp.TextContent => b.type === 'text') + .filter((b): b is acp.TextContent & { type: 'text' } => b.type === 'text') .map((b) => b.text) .join('\n') @@ -290,26 +290,13 @@ export function createAgent( update: { sessionUpdate: 'tool_call', toolCallId, - title: `${toolName}`, + title: toolName, kind, status: 'pending', rawInput: toolArgs } }) - if (signal.aborted) { - await connection.sessionUpdate({ - sessionId: params.sessionId, - update: { - sessionUpdate: 'tool_call_update', - toolCallId, - status: 'cancelled' - } - }) - session.pendingPrompt = null - return { stopReason: 'cancelled' } - } - await connection.sessionUpdate({ sessionId: params.sessionId, update: { @@ -324,8 +311,8 @@ export function createAgent( if (builtin) { result = await builtin.execute(toolArgs, session) - } else { - result = await coreTool!.execute(makeFigma(session), toolArgs) + } else if (coreTool) { + result = await coreTool.execute(makeFigma(session), toolArgs) } await connection.sessionUpdate({ diff --git a/packages/core/src/canvaskit.ts b/packages/core/src/canvaskit.ts index 872b8a57e..228fbae21 100644 --- a/packages/core/src/canvaskit.ts +++ b/packages/core/src/canvaskit.ts @@ -62,5 +62,5 @@ export async function getCanvasKit(options?: CanvasKitOptions): Promise - b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes) as number[]) + b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes)) ) return { nodes: msg.nodeChanges ?? [], meta, blobs } @@ -87,13 +87,12 @@ export function figmaNodesBounds( const parentTypes = new Map() for (const nc of nodeChanges) { - if (!nc.guid) continue const id = `${nc.guid.sessionID}:${nc.guid.localID}` parentTypes.set(id, nc.type ?? '') } for (const nc of nodeChanges) { - if (!nc.guid || !nc.type || NON_VISUAL_TYPES.has(nc.type)) continue + if (!nc.type || NON_VISUAL_TYPES.has(nc.type)) continue const parentId = nc.parentIndex?.guid ? `${nc.parentIndex.guid.sessionID}:${nc.parentIndex.guid.localID}` : null @@ -125,7 +124,6 @@ export function importClipboardNodes( const guidMap = new Map() const parentMap = new Map() for (const nc of nodeChanges) { - if (!nc.guid) continue const id = `${nc.guid.sessionID}:${nc.guid.localID}` guidMap.set(id, nc) if (nc.parentIndex?.guid) { @@ -210,7 +208,7 @@ export function importClipboardNodes( // Remap componentId from original Figma GUIDs to our node IDs for (const [, ourId] of created) { const node = graph.getNode(ourId) - if (!node || node.type !== 'INSTANCE' || !node.componentId) continue + if (node?.type !== 'INSTANCE' || !node.componentId) continue const ourComponentId = created.get(node.componentId) if (ourComponentId) graph.updateNode(ourId, { componentId: ourComponentId }) } @@ -232,8 +230,8 @@ export function importClipboardNodes( // so the node at least renders its own fills/strokes/layout. for (const [, ourId] of created) { const node = graph.getNode(ourId) - if (!node || node.type !== 'INSTANCE') continue - if (node.childIds.length === 0 && !graph.getNode(node.componentId)) { + if (node?.type !== 'INSTANCE') continue + if (node.childIds.length === 0 && (!node.componentId || !graph.getNode(node.componentId))) { graph.updateNode(ourId, { type: 'FRAME', componentId: '' }) } } diff --git a/packages/core/src/color.ts b/packages/core/src/color.ts index dff4951cc..426850611 100644 --- a/packages/core/src/color.ts +++ b/packages/core/src/color.ts @@ -11,9 +11,9 @@ export function parseColor(input: string): Color { if (!parsed) return { ...BLACK } const rgb = toRgb(parsed) return { - r: rgb?.r ?? 0, - g: rgb?.g ?? 0, - b: rgb?.b ?? 0, + r: rgb.r, + g: rgb.g, + b: rgb.b, a: parsed.alpha ?? 1 } } @@ -24,14 +24,14 @@ export function normalizeColor(color?: Partial): Color { } export function colorToHex(color: Color): string { - return (formatHex({ mode: 'rgb', r: color.r, g: color.g, b: color.b }) ?? '#000000').toUpperCase() + return formatHex({ mode: 'rgb', r: color.r, g: color.g, b: color.b }).toUpperCase() } export function colorToHex8(color: Color, alpha?: number): string { const a = alpha ?? color.a if (a >= 1) return colorToHex(color) return ( - formatHex8({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: a }) ?? '#000000FF' + formatHex8({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: a }) ).toUpperCase() } @@ -49,7 +49,7 @@ export function colorToRgba255(color: Color) { } export function colorToCSS(color: Color): string { - return formatRgb({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: color.a }) ?? 'rgb(0, 0, 0)' + return formatRgb({ mode: 'rgb', r: color.r, g: color.g, b: color.b, alpha: color.a }) } export function colorToCSSCompact(color: Color): string { diff --git a/packages/core/src/fig-export.ts b/packages/core/src/fig-export.ts index b76cf23ad..db3eab9c0 100644 --- a/packages/core/src/fig-export.ts +++ b/packages/core/src/fig-export.ts @@ -25,14 +25,14 @@ function variableValueToKiwi( value: VariableValue, type: string ): { value: Record; dataType: string; resolvedDataType: string } { - if (typeof value === 'object' && value !== null && 'aliasId' in value) { + if (typeof value === 'object' && 'aliasId' in value) { return { value: { alias: { guid: stringToGuid(value.aliasId) } }, dataType: 'ALIAS', - resolvedDataType: type === 'COLOR' ? 'COLOR' : type === 'BOOLEAN' ? 'BOOLEAN' : type === 'STRING' ? 'STRING' : 'FLOAT' + resolvedDataType: { COLOR: 'COLOR', BOOLEAN: 'BOOLEAN', STRING: 'STRING' }[type] ?? 'FLOAT' } } - if (type === 'COLOR' && typeof value === 'object' && value !== null && 'r' in value) { + if (type === 'COLOR' && typeof value === 'object' && 'r' in value) { return { value: { colorValue: { r: value.r, g: value.g, b: value.b, a: value.a } }, dataType: 'COLOR', @@ -165,14 +165,8 @@ export async function exportFigFile( if (!variable) continue const varGuid = stringToGuid(varId) - const resolvedType = - variable.type === 'COLOR' - ? 'COLOR' - : variable.type === 'BOOLEAN' - ? 'BOOLEAN' - : variable.type === 'STRING' - ? 'STRING' - : 'FLOAT' + const typeMap: Record = { COLOR: 'COLOR', BOOLEAN: 'BOOLEAN', STRING: 'STRING' } + const resolvedType = typeMap[variable.type] ?? 'FLOAT' const entries = Object.entries(variable.valuesByMode).map(([modeId, value]) => ({ modeID: stringToGuid(modeId), diff --git a/packages/core/src/figma-api.ts b/packages/core/src/figma-api.ts index e978675c9..c458a9039 100644 --- a/packages/core/src/figma-api.ts +++ b/packages/core/src/figma-api.ts @@ -1,6 +1,5 @@ -import { SceneGraph } from './scene-graph' - import type { + SceneGraph, SceneNode, NodeType, Fill, @@ -234,11 +233,11 @@ class FigmaNodeProxy { set cornerRadius(v: number | typeof MIXED) { if (v === MIXED) return this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { - cornerRadius: v as number, - topLeftRadius: v as number, - topRightRadius: v as number, - bottomRightRadius: v as number, - bottomLeftRadius: v as number, + cornerRadius: v, + topLeftRadius: v, + topRightRadius: v, + bottomRightRadius: v, + bottomLeftRadius: v, independentCorners: false }) } @@ -705,7 +704,7 @@ class FigmaNodeProxy { set layoutAlign(v: string) { const mapped = v === 'STRETCH' ? 'STRETCH' : 'AUTO' this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], { - layoutAlignSelf: mapped as SceneNode['layoutAlignSelf'] + layoutAlignSelf: mapped }) } @@ -1101,7 +1100,7 @@ export class FigmaAPI { ungroup(node: FigmaNodeProxy): void { const raw = this.graph.getNode(node[INTERNAL_ID]) - if (!raw || raw.type !== 'GROUP') return + if (raw?.type !== 'GROUP') return const parentId = raw.parentId ?? this._currentPageId for (const childId of [...raw.childIds]) { this.graph.reparentNode(childId, parentId) @@ -1293,10 +1292,13 @@ export class FigmaAPI { notify(message: string): { cancel: () => void } { if (typeof console !== 'undefined') console.log(`[figma.notify] ${message}`) + // eslint-disable-next-line no-empty-function return { cancel() {} } } + // eslint-disable-next-line no-empty-function commitUndo(): void {} + // eslint-disable-next-line no-empty-function triggerUndo(): void {} exportImage?: ( diff --git a/packages/core/src/fonts.ts b/packages/core/src/fonts.ts index c71466ec3..ab1e06136 100644 --- a/packages/core/src/fonts.ts +++ b/packages/core/src/fonts.ts @@ -5,7 +5,8 @@ import { CJK_FALLBACK_FAMILIES_MACOS, CJK_FALLBACK_FAMILIES_WINDOWS, CJK_FALLBACK_FAMILIES_LINUX, - CJK_GOOGLE_FONT + CJK_GOOGLE_FONT, + GOOGLE_FONTS_API_KEY } from './constants' import type { SceneGraph } from './scene-graph' @@ -59,8 +60,6 @@ const BUNDLED_FONTS: Record = { 'Inter|Regular': '/Inter-Regular.ttf' } -import { GOOGLE_FONTS_API_KEY } from './constants' - const googleFontsCache = new Map>() const googleFontsFailed = new Set() @@ -136,7 +135,7 @@ export async function loadFont(family: string, style = 'Regular'): Promise() async function computeFontDigest(data: ArrayBuffer): Promise { - if (typeof crypto !== 'undefined' && crypto.subtle) { + if (typeof crypto !== 'undefined') { const hash = await crypto.subtle.digest('SHA-1', data) return new Uint8Array(hash) } @@ -172,7 +172,7 @@ function buildDerivedTextData( const key = `${family}|${style}` if (seen.has(key)) return seen.add(key) - fontMeta!.push({ + fontMeta.push({ key: { family, style, postscript: '' }, fontLineHeight: 1.2, fontDigest: digestMap.get(key), @@ -421,7 +421,8 @@ export function sceneNodeToKiwi( const variable = graph.variables.get(varId) if (!variable) continue const varGuid = stringToGuid(varId) - const resolvedType = variable.type === 'COLOR' ? 'COLOR' : variable.type === 'BOOLEAN' ? 'BOOLEAN' : variable.type === 'STRING' ? 'STRING' : 'FLOAT' + const typeMap: Record = { COLOR: 'COLOR', BOOLEAN: 'BOOLEAN', STRING: 'STRING' } + const resolvedType = typeMap[variable.type] ?? 'FLOAT' entries.push({ variableData: { value: { alias: { guid: varGuid } }, diff --git a/packages/core/src/kiwi/codec.ts b/packages/core/src/kiwi/codec.ts index a634c1c4f..9fd13af2d 100644 --- a/packages/core/src/kiwi/codec.ts +++ b/packages/core/src/kiwi/codec.ts @@ -29,7 +29,7 @@ let compiledSchema: CompiledSchema | null = null */ export async function initCodec(): Promise { if (compiledSchema) return - compiledSchema = compileSchema(figmaSchema as Schema) as CompiledSchema + compiledSchema = compileSchema(figmaSchema) as CompiledSchema } export function getCompiledSchema() { @@ -38,7 +38,7 @@ export function getCompiledSchema() { } export function getSchemaBytes(): Uint8Array { - return encodeBinarySchema(figmaSchema as Schema) + return encodeBinarySchema(figmaSchema) } /** @@ -477,7 +477,7 @@ export function encodePaintWithVariableBinding( const { colorVariableBinding: _, ...basePaint } = paint const baseBytes = compiledSchema.encodePaint(basePaint) - const baseArray = Array.from(baseBytes) as number[] + const baseArray = Array.from(baseBytes) // Remove trailing 00 if (baseArray[baseArray.length - 1] === 0) { diff --git a/packages/core/src/kiwi/fig-file.ts b/packages/core/src/kiwi/fig-file.ts index e337a7aa9..57dbc66f9 100644 --- a/packages/core/src/kiwi/fig-file.ts +++ b/packages/core/src/kiwi/fig-file.ts @@ -90,7 +90,7 @@ export async function parseFigFile(buffer: ArrayBuffer): Promise { } const blobs: Uint8Array[] = (message.blobs ?? []).map((b) => - b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes) as number[]) + b.bytes instanceof Uint8Array ? b.bytes : new Uint8Array(Object.values(b.bytes)) ) const images = new Map() diff --git a/packages/core/src/kiwi/fig-import.ts b/packages/core/src/kiwi/fig-import.ts index 5055f4891..ad09b9fb6 100644 --- a/packages/core/src/kiwi/fig-import.ts +++ b/packages/core/src/kiwi/fig-import.ts @@ -35,7 +35,6 @@ export function importNodeChanges( const childrenMap = new Map() for (const nc of nodeChanges) { - if (!nc.guid) continue if (nc.phase === 'REMOVED') continue const id = guidToString(nc.guid) changeMap.set(id, nc) @@ -87,8 +86,8 @@ export function importNodeChanges( if (nc.type !== 'VARIABLE_SET') continue const modes = (nc.variableSetModes ?? []).map((m) => { - const modeId = m.id ? guidToString(m.id) : 'default' - return { modeId, name: m.name ?? 'Mode' } + const modeId = guidToString(m.id) + return { modeId, name: m.name } }) if (modes.length === 0) modes.push({ modeId: 'default', name: 'Default' }) @@ -150,7 +149,8 @@ export function importNodeChanges( if (Object.keys(valuesByMode).length === 0) { const col = graph.variableCollections.get(collectionId) const defaultMode = col?.defaultModeId ?? 'default' - valuesByMode[defaultMode] = type === 'BOOLEAN' ? false : type === 'STRING' ? '' : type === 'COLOR' ? { r: 0, g: 0, b: 0, a: 1 } : 0 + const defaultValue = type === 'BOOLEAN' ? false : (type === 'STRING' ? '' : null) + valuesByMode[defaultMode] = defaultValue ?? (type === 'COLOR' ? { r: 0, g: 0, b: 0, a: 1 } : 0) } graph.addVariable({ diff --git a/packages/core/src/kiwi/instance-overrides.ts b/packages/core/src/kiwi/instance-overrides.ts index 88c1367f8..198c7ef6a 100644 --- a/packages/core/src/kiwi/instance-overrides.ts +++ b/packages/core/src/kiwi/instance-overrides.ts @@ -88,7 +88,7 @@ export function populateAndApplyOverrides( while (populateQueue.length > 0) { const nodeId = populateQueue.pop()! const node = graph.getNode(nodeId) - if (!node || node.type !== 'INSTANCE' || !node.componentId || node.childIds.length > 0) continue + if (node?.type !== 'INSTANCE' || !node.componentId || node.childIds.length > 0) continue const comp = graph.getNode(node.componentId) if (comp && comp.childIds.length > 0) { graph.populateInstanceChildren(nodeId, node.componentId) @@ -245,7 +245,7 @@ export function populateAndApplyOverrides( function repopulateInstance(nodeId: string, compId: string) { const node = graph.getNode(nodeId) - if (!node || node.type !== 'INSTANCE') return + if (node?.type !== 'INSTANCE') return for (const childId of [...node.childIds]) graph.deleteNode(childId) graph.updateNode(nodeId, { componentId: compId }) @@ -285,7 +285,7 @@ export function populateAndApplyOverrides( if (ownAssignments) { const valueByDef = new Map() for (const a of ownAssignments) { - if (a.defID) valueByDef.set(guidToString(a.defID), a.value) + valueByDef.set(guidToString(a.defID), a.value) } applyPropAssignments(node.id, valueByDef, propRefsMap) } @@ -303,7 +303,7 @@ export function populateAndApplyOverrides( const valueByDef = new Map() for (const a of assignments) { - if (a.defID) valueByDef.set(guidToString(a.defID), a.value) + valueByDef.set(guidToString(a.defID), a.value) } applyPropAssignments(node.id, valueByDef, propRefsMap) } @@ -329,7 +329,7 @@ export function populateAndApplyOverrides( const valueByDef = new Map() for (const a of ov.componentPropAssignments) { - if (a.defID) valueByDef.set(guidToString(a.defID), a.value) + valueByDef.set(guidToString(a.defID), a.value) } applyPropAssignments(targetId, valueByDef, propRefsMap) } @@ -354,7 +354,6 @@ export function populateAndApplyOverrides( const refs = findPropRefs(child.componentId, propRefsMap) if (refs) { for (const ref of refs) { - if (!ref.defID) continue const val = valueByDef.get(guidToString(ref.defID)) if (!val) continue @@ -387,7 +386,7 @@ export function populateAndApplyOverrides( while (o < scaled.length) { const cmd = scaled[o++] if (cmd === 0) continue - const coords = cmd === 1 || cmd === 2 ? 1 : cmd === 4 ? 3 : -1 + const coords = cmd === 1 || cmd === 2 ? 1 : (cmd === 4 ? 3 : -1) if (coords < 0) { console.warn(`scaleGeometryBlobs: unknown path command ${cmd} at offset ${o - 1}`) break @@ -414,8 +413,7 @@ export function populateAndApplyOverrides( const nodeId = guidToNodeId.get(ncId) if (!nodeId) continue - for (let i = 0; i < derived.length; i++) { - const d = derived[i] + for (const d of derived) { const guids = d.guidPath?.guids if (!guids?.length) continue @@ -549,16 +547,16 @@ export function populateAndApplyOverrides( function syncNodeProps(source: SceneNode, target: SceneNode) { const updates: Partial = {} - if (source.text !== undefined && source.text !== target.text) updates.text = source.text - if (source.visible !== undefined && source.visible !== target.visible) updates.visible = source.visible - if (source.opacity !== undefined && source.opacity !== target.opacity) updates.opacity = source.opacity - if (source.fills !== undefined && source.fills !== target.fills) updates.fills = copyFills(source.fills) - if (source.strokes !== undefined && source.strokes !== target.strokes) updates.strokes = copyStrokes(source.strokes) - if (source.effects !== undefined && source.effects !== target.effects) updates.effects = copyEffects(source.effects) - if (source.styleRuns !== undefined && source.styleRuns !== target.styleRuns) updates.styleRuns = copyStyleRuns(source.styleRuns) - if (source.layoutGrow !== undefined && source.layoutGrow !== target.layoutGrow) updates.layoutGrow = source.layoutGrow - if (source.textAutoResize !== undefined && source.textAutoResize !== target.textAutoResize) updates.textAutoResize = source.textAutoResize - if (source.locked !== undefined && source.locked !== target.locked) updates.locked = source.locked + if (source.text !== target.text) updates.text = source.text + if (source.visible !== target.visible) updates.visible = source.visible + if (source.opacity !== target.opacity) updates.opacity = source.opacity + if (source.fills !== target.fills) updates.fills = copyFills(source.fills) + if (source.strokes !== target.strokes) updates.strokes = copyStrokes(source.strokes) + if (source.effects !== target.effects) updates.effects = copyEffects(source.effects) + if (source.styleRuns !== target.styleRuns) updates.styleRuns = copyStyleRuns(source.styleRuns) + if (source.layoutGrow !== target.layoutGrow) updates.layoutGrow = source.layoutGrow + if (source.textAutoResize !== target.textAutoResize) updates.textAutoResize = source.textAutoResize + if (source.locked !== target.locked) updates.locked = source.locked if (Object.keys(updates).length > 0) graph.updateNode(target.id, updates) } diff --git a/packages/core/src/kiwi/kiwi-convert.ts b/packages/core/src/kiwi/kiwi-convert.ts index a7e57cd07..9ffa0cd5b 100644 --- a/packages/core/src/kiwi/kiwi-convert.ts +++ b/packages/core/src/kiwi/kiwi-convert.ts @@ -89,14 +89,14 @@ export function convertFills(paints?: Paint[]): Fill[] { if (!paints) return [] return paints.map((p) => { const base: Fill = { - type: (p.type ?? 'SOLID') as FillType, + type: p.type as FillType, color: convertColor(p.color), opacity: p.opacity ?? 1, visible: p.visible ?? true, blendMode: (p.blendMode ?? 'NORMAL') as BlendMode } - if (p.type?.startsWith('GRADIENT') && p.stops) { + if (p.type.startsWith('GRADIENT') && p.stops) { base.gradientStops = p.stops.map((s) => ({ color: convertColor(s.color), position: s.position @@ -115,7 +115,7 @@ export function convertFills(paints?: Paint[]): Fill[] { base.imageHash = img.hash } } - base.imageScaleMode = (p.imageScaleMode as ImageScaleMode) ?? 'FILL' + base.imageScaleMode = (p.imageScaleMode ?? 'FILL') as ImageScaleMode if (p.transform) { base.imageTransform = convertGradientTransform(p.transform) } @@ -141,9 +141,9 @@ function convertStrokes( visible: p.visible ?? true, align: (align === 'INSIDE' ? 'INSIDE' - : align === 'OUTSIDE' + : (align === 'OUTSIDE' ? 'OUTSIDE' - : 'CENTER') as Stroke['align'], + : 'CENTER')), cap: (cap ?? 'NONE') as StrokeCap, join: (join ?? 'MITER') as StrokeJoin, dashPattern: dashPattern ?? [] @@ -153,13 +153,13 @@ function convertStrokes( function convertEffects(effects?: KiwiEffect[]): Effect[] { if (!effects) return [] return effects.map((e) => ({ - type: e.type as Effect['type'], + type: e.type, color: convertColor(e.color), offset: e.offset ?? { x: 0, y: 0 }, radius: e.radius ?? 0, spread: e.spread ?? 0, visible: e.visible ?? true, - blendMode: (e.blendMode as BlendMode) ?? 'NORMAL' + blendMode: (e.blendMode ?? 'NORMAL') as BlendMode })) } @@ -335,8 +335,8 @@ function importStyleRuns(nc: NodeChange): StyleRun[] { const style: CharacterStyleOverride = {} if (override.fontName) { style.fontFamily = override.fontName.family - style.fontWeight = styleToWeight(override.fontName.style ?? '') - style.italic = override.fontName.style?.toLowerCase().includes('italic') ?? false + style.fontWeight = styleToWeight(override.fontName.style) + style.italic = override.fontName.style.toLowerCase().includes('italic') } if (override.fontSize !== undefined) style.fontSize = override.fontSize if (override.letterSpacing) { @@ -346,7 +346,7 @@ function importStyleRuns(nc: NodeChange): StyleRun[] { const lh = convertLineHeight(override.lineHeight, override.fontSize ?? nc.fontSize) if (lh != null) style.lineHeight = lh } - const deco = override.textDecoration as string | undefined + const deco = override.textDecoration if (deco) style.textDecoration = mapTextDecoration(deco) if (Object.keys(style).length > 0) styleMap.set(id, style) } @@ -385,13 +385,12 @@ function resolveVectorNetwork( } | undefined - if (!vectorData || vectorData.vectorNetworkBlob === undefined) return null + if (vectorData?.vectorNetworkBlob === undefined) return null const idx = vectorData.vectorNetworkBlob if (idx < 0 || idx >= blobs.length) return null try { const network = decodeVectorNetworkBlob(blobs[idx], vectorData.styleOverrideTable) - if (!network) return null const ns = vectorData.normalizedSize const nodeW = nc.size?.x ?? 0 @@ -430,7 +429,7 @@ export function resolveGeometryPaths( if (p.commandsBlob === undefined || p.commandsBlob < 0 || p.commandsBlob >= blobs.length) continue const blob = blobs[p.commandsBlob] - if (!blob || blob.length === 0) continue + if (blob.length === 0) continue result.push({ windingRule: (p.windingRule === 'EVENODD' ? 'EVENODD' : 'NONZERO') as WindingRule, commandsBlob: blob @@ -476,7 +475,7 @@ export function nodeChangeToProps( rotation = Math.atan2(nc.transform.m10 * sx, nc.transform.m00 * sx) * (180 / Math.PI) } - const dashPattern = (nc.dashPattern as number[]) ?? [] + const dashPattern = nc.dashPattern ?? [] return { nodeType, @@ -513,16 +512,16 @@ export function nodeChangeToProps( fontSize: nc.fontSize ?? 14, fontFamily: nc.fontName?.family ?? DEFAULT_FONT_FAMILY, fontWeight: styleToWeight(nc.fontName?.style ?? ''), - italic: nc.fontName?.style?.toLowerCase().includes('italic') ?? false, + italic: nc.fontName?.style.toLowerCase().includes('italic') ?? false, textAlignHorizontal: - (nc.textAlignHorizontal as 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED') ?? 'LEFT', - textAlignVertical: (nc.textAlignVertical as TextAlignVertical) ?? 'TOP', - textAutoResize: (nc.textAutoResize as TextAutoResize) ?? 'NONE', - textCase: (nc.textCase as TextCase) ?? 'ORIGINAL', + (nc.textAlignHorizontal ?? 'LEFT') as 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED', + textAlignVertical: (nc.textAlignVertical ?? 'TOP') as TextAlignVertical, + textAutoResize: (nc.textAutoResize ?? 'NONE') as TextAutoResize, + textCase: (nc.textCase ?? 'ORIGINAL') as TextCase, textDecoration: mapTextDecoration(nc.textDecoration as string), lineHeight: convertLineHeight(nc.lineHeight, nc.fontSize), letterSpacing: convertLetterSpacing(nc.letterSpacing, nc.fontSize), - maxLines: (nc.maxLines as number) ?? null, + maxLines: (nc.maxLines ?? null) as number | null, styleRuns: importStyleRuns(nc), horizontalConstraint: mapConstraint(nc.horizontalConstraint as string), verticalConstraint: mapConstraint(nc.verticalConstraint as string), @@ -537,9 +536,9 @@ export function nodeChangeToProps( primaryAxisAlign: mapStackJustify(nc.stackPrimaryAlignItems ?? nc.stackJustify), counterAxisAlign: mapStackCounterAlign(nc.stackCounterAlignItems ?? nc.stackCounterAlign), layoutWrap: nc.stackWrap === 'WRAP' ? 'WRAP' : 'NO_WRAP', - counterAxisSpacing: (nc.stackCounterSpacing as number) ?? 0, + counterAxisSpacing: nc.stackCounterSpacing ?? 0, layoutPositioning: nc.stackPositioning === 'ABSOLUTE' ? 'ABSOLUTE' : 'AUTO', - layoutGrow: (nc.stackChildPrimaryGrow as number) ?? 0, + layoutGrow: nc.stackChildPrimaryGrow ?? 0, layoutAlignSelf: (nc.stackChildAlignSelf as string) === 'STRETCH' ? 'STRETCH' : 'AUTO', vectorNetwork: resolveVectorNetwork(nc, blobs), fillGeometry: resolveGeometryPaths(nc.fillGeometry, blobs), @@ -548,25 +547,25 @@ export function nodeChangeToProps( strokeCap: (nc.strokeCap ?? 'NONE') as StrokeCap, strokeJoin: (nc.strokeJoin ?? 'MITER') as StrokeJoin, dashPattern, - borderTopWeight: (nc.borderTopWeight as number) ?? 0, - borderRightWeight: (nc.borderRightWeight as number) ?? 0, - borderBottomWeight: (nc.borderBottomWeight as number) ?? 0, - borderLeftWeight: (nc.borderLeftWeight as number) ?? 0, - independentStrokeWeights: (nc.borderStrokeWeightsIndependent as boolean) ?? false, + borderTopWeight: (nc.borderTopWeight ?? 0) as number, + borderRightWeight: (nc.borderRightWeight ?? 0) as number, + borderBottomWeight: (nc.borderBottomWeight ?? 0) as number, + borderLeftWeight: (nc.borderLeftWeight ?? 0) as number, + independentStrokeWeights: (nc.borderStrokeWeightsIndependent ?? false) as boolean, strokeMiterLimit: DEFAULT_STROKE_MITER_LIMIT, - minWidth: (nc.minWidth as number) ?? null, - maxWidth: (nc.maxWidth as number) ?? null, - minHeight: (nc.minHeight as number) ?? null, - maxHeight: (nc.maxHeight as number) ?? null, - isMask: (nc.isMask as boolean) ?? false, - maskType: ((nc.maskType as string) ?? 'ALPHA') as 'ALPHA' | 'VECTOR' | 'LUMINANCE', + minWidth: (nc.minWidth ?? null) as number | null, + maxWidth: (nc.maxWidth ?? null) as number | null, + minHeight: (nc.minHeight ?? null) as number | null, + maxHeight: (nc.maxHeight ?? null) as number | null, + isMask: (nc.isMask ?? false) as boolean, + maskType: (nc.maskType ?? 'ALPHA') as 'ALPHA' | 'VECTOR' | 'LUMINANCE', counterAxisAlignContent: (nc.stackCounterAlignContent as string) === 'SPACE_BETWEEN' ? 'SPACE_BETWEEN' : 'AUTO', - itemReverseZIndex: (nc.stackReverseZIndex as boolean) ?? false, - strokesIncludedInLayout: (nc.strokesIncludedInLayout as boolean) ?? false, + itemReverseZIndex: (nc.stackReverseZIndex ?? false) as boolean, + strokesIncludedInLayout: (nc.strokesIncludedInLayout ?? false) as boolean, expanded: true, textTruncation: (nc.textTruncation as string) === 'ENDING' ? 'ENDING' : 'DISABLED', - autoRename: (nc.autoRename as boolean) ?? true, + autoRename: (nc.autoRename ?? true) as boolean, boundVariables: extractBoundVariables(nc), clipsContent: nc.frameMaskDisabled === false, componentId: extractSymbolId(nc) @@ -596,7 +595,7 @@ export function sortChildren( children.sort((a, b) => { const aPos = nodeMap.get(a)?.parentIndex?.position ?? '' const bPos = nodeMap.get(b)?.parentIndex?.position ?? '' - return aPos < bPos ? -1 : aPos > bPos ? 1 : 0 + return aPos < bPos ? -1 : (aPos > bPos ? 1 : 0) }) } } @@ -680,9 +679,8 @@ export function convertOverrideToProps(ov: Record): Partial= MAX_PENDING_QUERIES) return const query = this.gl.createQuery() - if (!query) return this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query) this.activeQuery = query diff --git a/packages/core/src/render-image.ts b/packages/core/src/render-image.ts index 961bb0afb..9fc896b98 100644 --- a/packages/core/src/render-image.ts +++ b/packages/core/src/render-image.ts @@ -1,6 +1,6 @@ import type { SkiaRenderer } from './renderer' import type { SceneGraph } from './scene-graph' -import type { CanvasKit } from 'canvaskit-wasm' +import type { CanvasKit, Canvas } from 'canvaskit-wasm' export type ExportFormat = 'PNG' | 'JPG' | 'WEBP' | 'SVG' @@ -53,7 +53,7 @@ function renderToSurface( height: number, format: ExportFormat, quality: number, - setup: (canvas: import('canvaskit-wasm').Canvas) => void + setup: (canvas: Canvas) => void ): Uint8Array | null { const surface = ck.MakeSurface(width, height) if (!surface) return null diff --git a/packages/core/src/render/export-jsx.ts b/packages/core/src/render/export-jsx.ts index 2c7ebaad9..048d694b6 100644 --- a/packages/core/src/render/export-jsx.ts +++ b/packages/core/src/render/export-jsx.ts @@ -110,7 +110,7 @@ const JSX_ENTITY: Record = { } function escapeJSXText(text: string): string { - return text.replace(/[{}<>&]/g, (c) => JSX_ENTITY[c]!) + return text.replace(/[{}<>&]/g, (c) => JSX_ENTITY[c]) } function formatProp(key: string, value: unknown): string { @@ -476,7 +476,7 @@ function collectTailwindClasses(node: SceneNode, graph: SceneGraph): string[] { if (shadow) classes.push(`shadow-[${shadow}]`) } else if (effect.type === 'LAYER_BLUR' || effect.type === 'FOREGROUND_BLUR') { classes.push(`blur-[${effect.radius}px]`) - } else if (effect.type === 'BACKGROUND_BLUR') { + } else { classes.push(`backdrop-blur-[${effect.radius}px]`) } } diff --git a/packages/core/src/render/mini-react.ts b/packages/core/src/render/mini-react.ts index 931faa0bc..ff892a43e 100644 --- a/packages/core/src/render/mini-react.ts +++ b/packages/core/src/render/mini-react.ts @@ -19,10 +19,10 @@ export function createElement( ...props, children: flatChildren.length === 1 - ? (flatChildren as ReactNode[]) - : flatChildren.length > 0 + ? flatChildren + : (flatChildren.length > 0 ? flatChildren - : undefined + : undefined) } } } diff --git a/packages/core/src/render/renderer.ts b/packages/core/src/render/renderer.ts index e2c33bcbc..7b80bf9fd 100644 --- a/packages/core/src/render/renderer.ts +++ b/packages/core/src/render/renderer.ts @@ -5,7 +5,7 @@ import { isTreeNode } from './tree' import type { SceneGraph, SceneNode, NodeType, LayoutMode, Stroke } from '../scene-graph' import type { TreeNode } from './tree' -const TYPE_MAP: Record = { +const TYPE_MAP: Partial> = { frame: 'FRAME', view: 'FRAME', rectangle: 'RECTANGLE', @@ -83,7 +83,7 @@ export function renderTree( tree: TreeNode, options: RenderOptions = {} ): RenderResult { - const parentId = options.parentId ?? graph.getPages()[0]?.id ?? graph.rootId + const parentId = options.parentId ?? graph.getPages()[0].id const result = renderNode(graph, tree, parentId) @@ -149,7 +149,7 @@ function propsToOverrides(props: Record, isText: boolean): Part } if (typeof props.stroke === 'string') { - const strokeWidth = (props.strokeWidth as number) ?? 1 + const strokeWidth = (props.strokeWidth as number | undefined) ?? 1 o.strokes = [parseStroke(props.stroke, strokeWidth)] } @@ -264,7 +264,7 @@ function propsToOverrides(props: Record, isText: boolean): Part if (props.pointCount !== undefined) o.pointCount = props.pointCount as number if (typeof props.shadow === 'string') { - const parts = (props.shadow as string).split(/\s+/) + const parts = props.shadow.split(/\s+/) if (parts.length >= 4) { const c = parseColor(parts.slice(3).join(' ')) o.effects = [ @@ -272,8 +272,8 @@ function propsToOverrides(props: Record, isText: boolean): Part { type: 'DROP_SHADOW', color: c, - offset: { x: parseFloat(parts[0]!), y: parseFloat(parts[1]!) }, - radius: parseFloat(parts[2]!), + offset: { x: parseFloat(parts[0]), y: parseFloat(parts[1]) }, + radius: parseFloat(parts[2]), spread: 0, visible: true } @@ -286,7 +286,7 @@ function propsToOverrides(props: Record, isText: boolean): Part ...(o.effects ?? []), { type: 'LAYER_BLUR', - radius: props.blur as number, + radius: props.blur, visible: true, color: { ...TRANSPARENT }, offset: { x: 0, y: 0 }, diff --git a/packages/core/src/renderer/overlays.ts b/packages/core/src/renderer/overlays.ts index 3bc12ec3a..53e2d1db6 100644 --- a/packages/core/src/renderer/overlays.ts +++ b/packages/core/src/renderer/overlays.ts @@ -31,8 +31,7 @@ import type { SnapGuide } from '../snap' import type { TextEditor } from '../text-editor' import type { Rect, Vector } from '../types' import type { Canvas } from 'canvaskit-wasm' -import type { SkiaRenderer } from './renderer' -import type { RenderOverlays } from './renderer' +import type { SkiaRenderer, RenderOverlays } from './renderer' export function drawHoverHighlight( r: SkiaRenderer, @@ -219,7 +218,7 @@ export function drawSelectionLabels( const glyphIds = r.sizeFont.getGlyphIDs(sizeText) const widths = r.sizeFont.getGlyphWidths(glyphIds) let textWidth = 0 - for (let i = 0; i < widths.length; i++) textWidth += widths[i] + for (const w of widths) textWidth += w const pillW = textWidth + SIZE_PILL_PADDING_X * 2 const pillH = SIZE_PILL_HEIGHT const pillX = smx - pillW / 2 @@ -737,7 +736,7 @@ export function drawRemoteCursors( const glyphIds = font.getGlyphIDs(cursor.name) const widths = font.getGlyphWidths(glyphIds) let textWidth = 0 - for (let i = 0; i < widths.length; i++) textWidth += widths[i] + for (const w of widths) textWidth += w r.auxFill.setColor(r.ck.Color4f(cr, g, b, 1)) const bgRect = r.ck.RRectXY( diff --git a/packages/core/src/renderer/renderer.ts b/packages/core/src/renderer/renderer.ts index 03f340a21..8d65e911b 100644 --- a/packages/core/src/renderer/renderer.ts +++ b/packages/core/src/renderer/renderer.ts @@ -33,8 +33,9 @@ import type { SceneNode, SceneGraph, Fill, Stroke } from '../scene-graph' import type { SnapGuide } from '../snap' import type { TextEditor } from '../text-editor' import type { Color, Rect, Vector } from '../types' -import type { Image as CKImage, Path } from 'canvaskit-wasm' import type { + Image as CKImage, + Path, CanvasKit, Surface, Canvas, @@ -45,7 +46,8 @@ import type { TypefaceFontProvider, SkPicture, ImageFilter, - MaskFilter + MaskFilter, + Paragraph } from 'canvaskit-wasm' import { @@ -399,7 +401,7 @@ export class SkiaRenderer { this.fontsLoaded = true this.invalidateAllPictures() - ensureCJKFallback().then((family) => { + void ensureCJKFallback().then((family) => { if (family) this.invalidateAllPictures() }) } @@ -564,7 +566,7 @@ export class SkiaRenderer { const id = [...selectedIds][0] const node = graph.getNode(id) - if (!node || node.type !== 'FRAME') return null + if (node?.type !== 'FRAME') return null const parent = node.parentId ? graph.getNode(node.parentId) : null const isTopLevel = !parent || parent.type === 'CANVAS' || parent.type === 'SECTION' @@ -730,7 +732,7 @@ export class SkiaRenderer { this.worldViewport = prevViewport this.scenePictureVersion = sceneVersion this.scenePicturePageId = this.pageId - canvas.drawPicture(this.scenePicture!) + canvas.drawPicture(this.scenePicture) } invalidateVectorPath(nodeId: string): void { @@ -794,7 +796,7 @@ export class SkiaRenderer { node: SceneNode, color?: Float32Array, { halfLeading = false }: { halfLeading?: boolean } = {} - ): import('canvaskit-wasm').Paragraph { + ): Paragraph { const ck = this.ck const baseColor = color ?? ck.BLACK const baseFontSize = node.fontSize || DEFAULT_FONT_SIZE @@ -969,11 +971,11 @@ export class SkiaRenderer { this.penVertexFill.delete() this.penVertexStroke.delete() this.effectLayerPaint.delete() - for (const filter of this.imageFilterCache.values()) filter?.delete() + for (const filter of this.imageFilterCache.values()) filter.delete() this.imageFilterCache.clear() - for (const filter of this.maskFilterCache.values()) filter?.delete() + for (const filter of this.maskFilterCache.values()) filter.delete() this.maskFilterCache.clear() - for (const pic of this.nodePictureCache.values()) pic?.delete() + for (const pic of this.nodePictureCache.values()) pic.delete() this.nodePictureCache.clear() this.scenePicture?.delete() this._flashPaint?.delete() diff --git a/packages/core/src/renderer/scene.ts b/packages/core/src/renderer/scene.ts index 7284cee41..206e5dfcb 100644 --- a/packages/core/src/renderer/scene.ts +++ b/packages/core/src/renderer/scene.ts @@ -1,8 +1,7 @@ import { DROP_HIGHLIGHT_ALPHA, DROP_HIGHLIGHT_STROKE, SECTION_CORNER_RADIUS } from '../constants' import type { SceneNode, SceneGraph } from '../scene-graph' -import type { Canvas } from 'canvaskit-wasm' -import type { SkiaRenderer } from './renderer' -import type { RenderOverlays } from './renderer' +import type { Canvas, EmbindEnumEntity } from 'canvaskit-wasm' +import type { SkiaRenderer, RenderOverlays } from './renderer' export function renderNode( r: SkiaRenderer, @@ -133,7 +132,7 @@ export function renderSection( const rrect = r.ck.RRectXY(rect, SECTION_CORNER_RADIUS, SECTION_CORNER_RADIUS) for (let fi = 0; fi < node.fills.length; fi++) { - const fill = node.fills[fi]! + const fill = node.fills[fi] if (!fill.visible) continue r.applyFill(fill, node, graph, fi) r.fillPaint.setAlphaf(fill.opacity) @@ -142,7 +141,7 @@ export function renderSection( } for (let si = 0; si < node.strokes.length; si++) { - const stroke = node.strokes[si]! + const stroke = node.strokes[si] if (!stroke.visible) continue const sc = r.resolveStrokeColor(stroke, si, node, graph) r.strokePaint.setColor(r.ck.Color4f(sc.r, sc.g, sc.b, sc.a)) @@ -167,7 +166,7 @@ export function renderComponentSet( const rrect = r.ck.RRectXY(rect, 5, 5) for (let fi = 0; fi < node.fills.length; fi++) { - const fill = node.fills[fi]! + const fill = node.fills[fi] if (!fill.visible) continue r.applyFill(fill, node, graph, fi) r.fillPaint.setAlphaf(fill.opacity) @@ -235,7 +234,7 @@ export function renderShapeUncached( r.renderEffects(canvas, node, rect, hasRadius, 'behind') for (let fi = 0; fi < node.fills.length; fi++) { - const fill = node.fills[fi]! + const fill = node.fills[fi] if (!fill.visible) continue r.applyFill(fill, node, graph, fi) r.fillPaint.setAlphaf(fill.opacity) @@ -247,7 +246,7 @@ export function renderShapeUncached( const sg = node.type === 'VECTOR' ? r.getStrokeGeometry(node) : null const vectorPaths = !sg && node.type === 'VECTOR' ? r.getVectorPaths(node) : null for (let si = 0; si < node.strokes.length; si++) { - const stroke = node.strokes[si]! + const stroke = node.strokes[si] if (!stroke.visible) continue const sc = r.resolveStrokeColor(stroke, si, node, graph) @@ -260,12 +259,12 @@ export function renderShapeUncached( } if (vectorPaths) { - const capMap: Record = { + const capMap: Record = { NONE: r.ck.StrokeCap.Butt, ROUND: r.ck.StrokeCap.Round, SQUARE: r.ck.StrokeCap.Square } - const joinMap: Record = { + const joinMap: Record = { MITER: r.ck.StrokeJoin.Miter, ROUND: r.ck.StrokeJoin.Round, BEVEL: r.ck.StrokeJoin.Bevel @@ -294,7 +293,7 @@ export function renderShapeUncached( r.strokePaint.setAlphaf(stroke.opacity) if (stroke.cap) { - const capMap: Record = { + const capMap: Record = { NONE: r.ck.StrokeCap.Butt, ROUND: r.ck.StrokeCap.Round, SQUARE: r.ck.StrokeCap.Square @@ -302,7 +301,7 @@ export function renderShapeUncached( r.strokePaint.setStrokeCap(capMap[stroke.cap] ?? r.ck.StrokeCap.Butt) } if (stroke.join) { - const joinMap: Record = { + const joinMap: Record = { MITER: r.ck.StrokeJoin.Miter, ROUND: r.ck.StrokeJoin.Round, BEVEL: r.ck.StrokeJoin.Bevel diff --git a/packages/core/src/renderer/strokes.ts b/packages/core/src/renderer/strokes.ts index f6ac28321..3ff5aefe5 100644 --- a/packages/core/src/renderer/strokes.ts +++ b/packages/core/src/renderer/strokes.ts @@ -1,5 +1,5 @@ import type { SceneNode, Stroke } from '../scene-graph' -import type { Canvas } from 'canvaskit-wasm' +import type { Canvas, Paint } from 'canvaskit-wasm' import type { SkiaRenderer } from './renderer' export function drawNodeStroke( @@ -129,28 +129,28 @@ export function drawIndividualSideStrokes( const tw = node.borderTopWeight if (tw > 0) { - const y = inside ? tw / 2 : outside ? -tw / 2 : 0 + const y = inside ? tw / 2 : (outside ? -tw / 2 : 0) r.strokePaint.setStrokeWidth(tw) canvas.drawLine(0, y, w, y, r.strokePaint) } const rw = node.borderRightWeight if (rw > 0) { - const x = inside ? w - rw / 2 : outside ? w + rw / 2 : w + const x = inside ? w - rw / 2 : (outside ? w + rw / 2 : w) r.strokePaint.setStrokeWidth(rw) canvas.drawLine(x, 0, x, h, r.strokePaint) } const bw = node.borderBottomWeight if (bw > 0) { - const y = inside ? h - bw / 2 : outside ? h + bw / 2 : h + const y = inside ? h - bw / 2 : (outside ? h + bw / 2 : h) r.strokePaint.setStrokeWidth(bw) canvas.drawLine(0, y, w, y, r.strokePaint) } const lw = node.borderLeftWeight if (lw > 0) { - const x = inside ? lw / 2 : outside ? -lw / 2 : 0 + const x = inside ? lw / 2 : (outside ? -lw / 2 : 0) r.strokePaint.setStrokeWidth(lw) canvas.drawLine(x, 0, x, h, r.strokePaint) } @@ -160,7 +160,7 @@ export function strokeNodeShape( r: SkiaRenderer, canvas: Canvas, node: SceneNode, - paint: import('canvaskit-wasm').Paint + paint: Paint ): void { const rect = r.ck.LTRBRect(0, 0, node.width, node.height) diff --git a/packages/core/src/rpc/commands.ts b/packages/core/src/rpc/commands.ts index 910475e9c..a7b48fac2 100644 --- a/packages/core/src/rpc/commands.ts +++ b/packages/core/src/rpc/commands.ts @@ -259,7 +259,7 @@ export const nodeCommand: RpcCommand = fontFamily: node.fontFamily, fontSize: node.fontSize, fontWeight: node.fontWeight, - text: node.text?.length ? (node.text.length > 200 ? node.text.slice(0, 200) + '…' : node.text) : null, + text: node.text.length ? (node.text.length > 200 ? node.text.slice(0, 200) + '…' : node.text) : null, parent: parent ? { id: parent.id, name: parent.name, type: parent.type } : null, children: node.childIds.length, boundVariables: boundVars @@ -277,15 +277,14 @@ export interface VariablesArgs { function formatVariableValue(variable: Variable, graph: SceneGraph): string { const modeId = graph.getActiveModeId(variable.collectionId) const raw = variable.valuesByMode[modeId] - if (raw === undefined) return '–' - if (typeof raw === 'object' && raw !== null && 'aliasId' in raw) { - const alias = graph.variables.get((raw as { aliasId: string }).aliasId) - return alias ? `→ ${alias.name}` : `→ ${(raw as { aliasId: string }).aliasId}` + if (typeof raw === 'object' && 'aliasId' in raw) { + const alias = graph.variables.get(raw.aliasId) + return alias ? `→ ${alias.name}` : `→ ${raw.aliasId}` } - if (typeof raw === 'object' && raw !== null && 'r' in raw) { - return colorToHex(raw as Color).toLowerCase() + if (typeof raw === 'object' && 'r' in raw) { + return colorToHex(raw).toLowerCase() } return String(raw) diff --git a/packages/core/src/scene-graph.ts b/packages/core/src/scene-graph.ts index c393e086e..7e8c26752 100644 --- a/packages/core/src/scene-graph.ts +++ b/packages/core/src/scene-graph.ts @@ -2,7 +2,7 @@ import { BLACK, DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from './consta import { copyEffects, copyFills, copyStrokes, copyStyleRuns } from './copy' export type { GUID, Color } from './types' -import type { Matrix, Vector } from './types' +import type { Matrix, Vector, Color, Rect } from './types' export type HandleMirroring = 'NONE' | 'ANGLE' | 'ANGLE_AND_LENGTH' export type WindingRule = 'NONZERO' | 'EVENODD' @@ -58,7 +58,6 @@ export type NodeType = | 'CONNECTOR' | 'SHAPE_WITH_TEXT' -import type { Color, Matrix, Rect } from './types' export type FillType = | 'SOLID' @@ -526,9 +525,18 @@ export class SceneGraph { const collection = this.variableCollections.get(collectionId) if (!collection) throw new Error(`Collection "${collectionId}" not found`) const id = generateId() - const defaultValue = - value ?? - (type === 'COLOR' ? { ...BLACK } : type === 'FLOAT' ? 0 : type === 'BOOLEAN' ? false : '') + let defaultValue: VariableValue + if (value !== undefined) { + defaultValue = value + } else if (type === 'COLOR') { + defaultValue = { ...BLACK } + } else if (type === 'FLOAT') { + defaultValue = 0 + } else if (type === 'BOOLEAN') { + defaultValue = false + } else { + defaultValue = '' + } const valuesByMode: Record = {} for (const mode of collection.modes) { valuesByMode[mode.modeId] = structuredClone(defaultValue) @@ -592,8 +600,7 @@ export class SceneGraph { if (!variable) return undefined const resolvedModeId = modeId ?? this.getActiveModeId(variable.collectionId) const value = variable.valuesByMode[resolvedModeId] - if (value === undefined) return undefined - if (typeof value === 'object' && value !== null && 'aliasId' in value) { + if (typeof value === 'object' && 'aliasId' in value) { const seen = visited ?? new Set() seen.add(variableId) return this.resolveVariable(value.aliasId, undefined, seen) @@ -603,7 +610,7 @@ export class SceneGraph { resolveColorVariable(variableId: string): Color | undefined { const value = this.resolveVariable(variableId) - if (value && typeof value === 'object' && 'r' in value) return value as Color + if (value && typeof value === 'object' && 'r' in value) return value return undefined } @@ -962,10 +969,10 @@ export class SceneGraph { 'borderLeftWeight' ] - private static copyProp( + private static copyProp( target: Partial | SceneNode, source: SceneNode, - key: K + key: keyof SceneNode ): void { const val = source[key] if (key === 'fills') { @@ -977,7 +984,7 @@ export class SceneGraph { } else if (key === 'styleRuns') { ;(target as Record)[key] = copyStyleRuns(val as StyleRun[]) } else { - target[key] = (Array.isArray(val) ? structuredClone(val) : val) as SceneNode[K] + ;(target as Record)[key] = Array.isArray(val) ? structuredClone(val) : val } } @@ -987,7 +994,7 @@ export class SceneGraph { overrides: Partial = {} ): SceneNode | null { const component = this.nodes.get(componentId) - if (!component || component.type !== 'COMPONENT') return null + if (component?.type !== 'COMPONENT') return null const props: Partial = { name: component.name, componentId } for (const key of SceneGraph.INSTANCE_SYNC_PROPS) { @@ -1030,7 +1037,7 @@ export class SceneGraph { syncInstances(componentId: string): void { const component = this.nodes.get(componentId) - if (!component || component.type !== 'COMPONENT') return + if (component?.type !== 'COMPONENT') return for (const instance of this.getInstances(componentId)) { // Sync instance-level props (unless overridden) @@ -1112,7 +1119,7 @@ export class SceneGraph { detachInstance(instanceId: string): void { const node = this.nodes.get(instanceId) - if (!node || node.type !== 'INSTANCE') return + if (node?.type !== 'INSTANCE') return node.type = 'FRAME' node.componentId = null node.overrides = {} diff --git a/packages/core/src/svg-export.ts b/packages/core/src/svg-export.ts index 0a6337a43..3cdac403b 100644 --- a/packages/core/src/svg-export.ts +++ b/packages/core/src/svg-export.ts @@ -42,7 +42,7 @@ function formatColor(color: Color, opacity = 1): string { // --- Path data --- export function geometryBlobToSVGPath(blob: Uint8Array): string { - if (!blob || blob.length === 0) return '' + if (blob.length === 0) return '' const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength) let o = 0 const parts: string[] = [] @@ -301,11 +301,7 @@ function createFilterDef(effects: Effect[], ctx: SVGExportContext): { id: string operator: 'over' }) ) - } else if ( - effect.type === 'LAYER_BLUR' || - effect.type === 'BACKGROUND_BLUR' || - effect.type === 'FOREGROUND_BLUR' - ) { + } else { const stdDev = round(effect.radius / 2) primitives.push(svg('feGaussianBlur', { stdDeviation: stdDev })) } @@ -606,24 +602,24 @@ function renderTextNode(node: SceneNode, fillAttr: string | null): SVGNode { 'text-anchor': node.textAlignHorizontal === 'CENTER' ? 'middle' - : node.textAlignHorizontal === 'RIGHT' + : (node.textAlignHorizontal === 'RIGHT' ? 'end' - : undefined, + : undefined), 'text-decoration': node.textDecoration === 'UNDERLINE' ? 'underline' - : node.textDecoration === 'STRIKETHROUGH' + : (node.textDecoration === 'STRIKETHROUGH' ? 'line-through' - : undefined, + : undefined), 'letter-spacing': node.letterSpacing ? round(node.letterSpacing) : undefined } const x = node.textAlignHorizontal === 'CENTER' ? round(node.width / 2) - : node.textAlignHorizontal === 'RIGHT' + : (node.textAlignHorizontal === 'RIGHT' ? round(node.width) - : 0 + : 0) const y = node.fontSize || 14 if (node.styleRuns.length > 0) { @@ -833,7 +829,7 @@ export function renderNodesToSVG( ? { ...node, x: round(offsetX), y: round(offsetY) } : node - const rendered = renderNode(clone as SceneNode, ctx) + const rendered = renderNode(clone, ctx) if (rendered) contentNodes.push(rendered) } diff --git a/packages/core/src/tools/ai-adapter.ts b/packages/core/src/tools/ai-adapter.ts index 26878ee40..a53156cc7 100644 --- a/packages/core/src/tools/ai-adapter.ts +++ b/packages/core/src/tools/ai-adapter.ts @@ -7,6 +7,7 @@ import type { FigmaAPI } from '../figma-api' import type { ToolDef, ParamDef, ParamType } from './schema' +import type * as valibot from 'valibot' export interface AIAdapterOptions { getFigma: () => FigmaAPI @@ -39,7 +40,7 @@ export function toolsToAI( tools: ToolDef[], options: AIAdapterOptions, deps: { - v: typeof import('valibot') + v: typeof valibot valibotSchema: (schema: any) => any tool: (opts: any) => any } @@ -77,7 +78,7 @@ export function toolsToAI( return result } -function paramToValibot(v: typeof import('valibot'), param: ParamDef): unknown { +function paramToValibot(v: typeof valibot, param: ParamDef): unknown { const typeMap: Record unknown> = { string: () => (param.enum ? v.picklist(param.enum as [string, ...string[]]) : v.string()), number: () => { diff --git a/packages/core/src/tools/analyze.ts b/packages/core/src/tools/analyze.ts index b61636ebe..e92669e03 100644 --- a/packages/core/src/tools/analyze.ts +++ b/packages/core/src/tools/analyze.ts @@ -67,10 +67,10 @@ function serializeNodeProps(raw: SceneNode): string { for (const effect of raw.effects) { const parts: string[] = [effect.type] - if (effect.radius !== undefined) parts.push(`r=${effect.radius}`) - if (effect.color) parts.push(`c=${colorToHex(effect.color)}`) - if (effect.offset) parts.push(`x=${effect.offset.x} y=${effect.offset.y}`) - if (effect.spread !== undefined) parts.push(`s=${effect.spread}`) + parts.push(`r=${effect.radius}`) + parts.push(`c=${colorToHex(effect.color)}`) + parts.push(`x=${effect.offset.x} y=${effect.offset.y}`) + parts.push(`s=${effect.spread}`) lines.push(`effect: ${parts.join(' ')}`) } @@ -156,12 +156,12 @@ export const analyzeColors = defineTool({ const boundVars = raw.boundVariables for (const fill of raw.fills) { if (fill.type === 'SOLID' && fill.visible) { - trackColor(colorMap, fill.color, boundVars?.['fills'] ? String(boundVars['fills']) : null) + trackColor(colorMap, fill.color, boundVars['fills'] ? String(boundVars['fills']) : null) } } for (const stroke of raw.strokes) { if (stroke.visible) { - trackColor(colorMap, stroke.color, boundVars?.['strokes'] ? String(boundVars['strokes']) : null) + trackColor(colorMap, stroke.color, boundVars['strokes'] ? String(boundVars['strokes']) : null) } } return false @@ -415,7 +415,7 @@ export const analyzeClusters = defineTool({ let confidence = 100 if (nodes.length >= 2) { - const base = nodes[0]! + const base = nodes[0] let score = 0 for (const n of nodes.slice(1)) { const sizeDiff = Math.abs(n.width - base.width) + Math.abs(n.height - base.height) @@ -582,6 +582,7 @@ export const evalCode = defineTool({ code: { type: 'string', description: 'JavaScript code to execute', required: true } }, execute: async (figma, { code }) => { + // eslint-disable-next-line no-empty-function const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor const wrapped = code.trim().startsWith('return') ? code : `return (async () => { ${code} })()` const fn = new AsyncFunction('figma', wrapped) diff --git a/packages/core/src/tools/create.ts b/packages/core/src/tools/create.ts index cd7b16fb7..21d1a09bb 100644 --- a/packages/core/src/tools/create.ts +++ b/packages/core/src/tools/create.ts @@ -36,7 +36,7 @@ export const createShape = defineTool({ POLYGON: () => figma.createPolygon(), SECTION: () => figma.createSection() } - const node = createMap[args.type]!() + const node = createMap[args.type]() node.x = args.x node.y = args.y node.resize(args.width, args.height) diff --git a/packages/core/src/tools/structure.ts b/packages/core/src/tools/structure.ts index 9e379f27f..bd967adde 100644 --- a/packages/core/src/tools/structure.ts +++ b/packages/core/src/tools/structure.ts @@ -78,7 +78,7 @@ export const groupNodes = defineTool({ .map((id) => figma.getNodeById(id)) .filter((n): n is FigmaNodeProxy => n !== null) if (nodes.length < 2) return { error: 'Need at least 2 nodes to group' } - const parent = nodes[0]!.parent ?? figma.currentPage + const parent = nodes[0].parent ?? figma.currentPage const group = figma.group(nodes, parent) return nodeSummary(group) } @@ -304,7 +304,7 @@ export const arrangeNodes = defineTool({ } if (nodes.length === 0) return { error: 'No nodes to arrange' } - const first = nodes[0]! + const first = nodes[0] if (mode === 'row') { let x = first.x @@ -330,7 +330,7 @@ export const arrangeNodes = defineTool({ let rowHeight = 0 for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]! + const node = nodes[i] if (i > 0 && i % cols === 0) { x = startX y += rowHeight + gap diff --git a/packages/core/src/tools/vector.ts b/packages/core/src/tools/vector.ts index 711711f20..4688164a6 100644 --- a/packages/core/src/tools/vector.ts +++ b/packages/core/src/tools/vector.ts @@ -103,14 +103,10 @@ export const pathScale = defineTool({ v.y = cy + (v.y - cy) * factor } for (const s of vn.segments) { - if (s.tangentStart) { - s.tangentStart.x *= factor - s.tangentStart.y *= factor - } - if (s.tangentEnd) { - s.tangentEnd.x *= factor - s.tangentEnd.y *= factor - } + s.tangentStart.x *= factor + s.tangentStart.y *= factor + s.tangentEnd.x *= factor + s.tangentEnd.y *= factor } figma.graph.updateNode(id, { vectorNetwork: vn } as any) return { id, factor } @@ -142,14 +138,10 @@ export const pathFlip = defineTool({ else v.y = h - v.y } for (const s of vn.segments) { - if (s.tangentStart) { - if (axis === 'horizontal') s.tangentStart.x = -s.tangentStart.x - else s.tangentStart.y = -s.tangentStart.y - } - if (s.tangentEnd) { - if (axis === 'horizontal') s.tangentEnd.x = -s.tangentEnd.x - else s.tangentEnd.y = -s.tangentEnd.y - } + if (axis === 'horizontal') s.tangentStart.x = -s.tangentStart.x + else s.tangentStart.y = -s.tangentStart.y + if (axis === 'horizontal') s.tangentEnd.x = -s.tangentEnd.x + else s.tangentEnd.y = -s.tangentEnd.y } figma.graph.updateNode(id, { vectorNetwork: vn } as any) return { id, axis } @@ -288,7 +280,7 @@ export const exportImage = defineTool({ args.ids && args.ids.length > 0 ? args.ids : figma.currentPage.children.map((n) => n.id) - const format = ((args.format as string) ?? 'PNG').toUpperCase() as 'PNG' | 'JPG' | 'WEBP' + const format = (args.format ?? 'PNG').toUpperCase() as 'PNG' | 'JPG' | 'WEBP' const data = await figma.exportImage(ids, { scale: args.scale ?? 1, format diff --git a/packages/core/src/vector.ts b/packages/core/src/vector.ts index 92fb942dc..d69d6d015 100644 --- a/packages/core/src/vector.ts +++ b/packages/core/src/vector.ts @@ -54,7 +54,7 @@ export function decodeVectorNetworkBlob( vertices.push({ x, y, - handleMirroring: (override?.handleMirroring as HandleMirroring) ?? 'NONE' + handleMirroring: (override?.handleMirroring as HandleMirroring | undefined) ?? 'NONE' }) } @@ -301,6 +301,7 @@ function buildChains(segments: VectorSegment[], _vertexCount: number): number[][ let current = startVertex const chain: number[] = [] + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { const segs = adj.get(current) if (!segs) break @@ -367,7 +368,7 @@ export function geometryBlobToPath( windingRule: WindingRule ): Path { const path = new ck.Path() - if (!blob || !(blob.buffer instanceof ArrayBuffer)) return path + if (!(blob.buffer instanceof ArrayBuffer)) return path const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength) let o = 0 diff --git a/packages/mcp/src/http.ts b/packages/mcp/src/http.ts index d6effa3ff..61998164b 100644 --- a/packages/mcp/src/http.ts +++ b/packages/mcp/src/http.ts @@ -20,6 +20,7 @@ const sessions = new Map; tran async function getOrCreateSession(sessionId?: string) { if (sessionId && sessions.has(sessionId)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by has() above return sessions.get(sessionId)! } @@ -79,7 +80,7 @@ app.all('/mcp', async (c) => { return transport.handleRequest(c.req.raw) }) -const isBun = typeof globalThis.Bun !== 'undefined' +const isBun = 'Bun' in globalThis if (isBun) { Bun.serve({ fetch: app.fetch, port, hostname: host }) diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index dee9e8e9c..1792b96cb 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -77,17 +77,19 @@ export function createServer(version: string, options: CreateServerOptions = {}) function makeFigma(): FigmaAPI { if (!graph) throw new Error('No document loaded. Use open_file or new_document first.') - const api = new FigmaAPI(graph) + const g = graph + const api = new FigmaAPI(g) if (currentPageId) api.currentPage = api.wrapNode(currentPageId) api.exportImage = async (nodeIds, opts) => { const ck = await getCanvasKit() - const surface = ck.MakeSurface(1, 1)! + const surface = ck.MakeSurface(1, 1) + if (!surface) throw new Error('Failed to create CanvasKit surface') const renderer = new SkiaRenderer(ck, surface) renderer.viewportWidth = 1 renderer.viewportHeight = 1 renderer.dpr = 1 - const pageId = currentPageId ?? graph!.getPages()[0]?.id ?? graph!.rootId - return renderNodesToImage(ck, renderer, graph!, pageId, nodeIds, { + const pageId = currentPageId ?? g.getPages()[0].id + return renderNodesToImage(ck, renderer, g, pageId, nodeIds, { scale: opts.scale ?? 1, format: (opts.format ?? 'PNG') as ExportFormat })