chore: enable stricter oxlint rules

This commit is contained in:
Danila Poyarkov 2026-05-18 18:41:01 +03:00
parent 99484bcee5
commit f3649550f2
46 changed files with 400 additions and 120 deletions

View file

@ -1,7 +1,16 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn", "vue"],
"jsPlugins": ["./lint/plugin.js"],
"plugins": [
"typescript",
"import",
"unicorn",
"vue",
"promise",
"node"
],
"jsPlugins": [
"./lint/plugin.js"
],
"env": {
"browser": true,
"es2024": true
@ -11,7 +20,13 @@
"no-console": [
"warn",
{
"allow": ["warn", "error", "debug", "time", "timeEnd"]
"allow": [
"warn",
"error",
"debug",
"time",
"timeEnd"
]
}
],
"eqeqeq": [
@ -34,12 +49,22 @@
],
"patterns": [
{
"group": ["@/engine/*"],
"group": [
"@/engine/*"
],
"message": "Import from @open-pencil/core unless the module adds platform-specific logic.",
"allowImportNames": ["loadFont", "listFamilies", "listFonts", "preloadFonts"]
"allowImportNames": [
"loadFont",
"listFamilies",
"listFonts",
"preloadFonts"
]
},
{
"group": ["../../packages/*", "../../../packages/*"],
"group": [
"../../packages/*",
"../../../packages/*"
],
"message": "Use the workspace package name (e.g. @open-pencil/core) instead of relative paths to packages/."
}
]
@ -97,7 +122,12 @@
"vue/define-emits-declaration": "error",
"vue/define-props-declaration": "error",
"vue/define-props-destructuring": "error",
"vue/max-props": ["error", { "maxProps": 16 }],
"vue/max-props": [
"error",
{
"maxProps": 16
}
],
"vue/require-default-export": "error",
"vue/require-typed-ref": "error",
"open-pencil/no-inline-named-types": [
@ -136,7 +166,6 @@
"open-pencil/no-broad-double-cast": "error",
"open-pencil/no-unknown-record-double-cast": "error",
"open-pencil/no-ts-suppression-comments": "error",
"open-pencil/no-function-type": "error",
"open-pencil/no-reflect-delete-global-this-outside-tests": "error",
"open-pencil/no-core-browser-globals": "error",
"open-pencil/no-direct-open-pencil-window-internals": "error",
@ -169,17 +198,216 @@
"open-pencil/no-component-root-sibling-folder": "error",
"open-pencil/no-flat-kiwi-modules": "error",
"open-pencil/no-top-level-prefixed-test-files": "error",
"open-pencil/no-sibling-domain-prefixed-files": "error"
"open-pencil/no-sibling-domain-prefixed-files": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
"default-case-last": "error",
"default-param-last": "error",
"no-array-constructor": "error",
"no-case-declarations": "error",
"no-caller": "error",
"no-constructor-return": "error",
"no-else-return": "error",
"no-empty-pattern": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-implied-eval": "error",
"no-iterator": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-multi-str": "error",
"no-new": "error",
"no-new-wrappers": "error",
"no-object-constructor": "error",
"no-proto": "error",
"no-return-assign": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-template-curly-in-string": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unused-expressions": "error",
"no-useless-assignment": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"prefer-numeric-literals": "error",
"prefer-object-has-own": "error",
"prefer-promise-reject-errors": "error",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"symbol-description": "error",
"unicode-bom": "error",
"yoda": "error",
"typescript/adjacent-overload-signatures": "error",
"typescript/await-thenable": "error",
"typescript/ban-ts-comment": "error",
"typescript/ban-tslint-comment": "error",
"typescript/no-array-delete": "error",
"typescript/no-base-to-string": "error",
"typescript/no-confusing-non-null-assertion": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-duplicate-type-constituents": "error",
"typescript/no-extraneous-class": "error",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-for-in-array": "error",
"typescript/no-implied-eval": "error",
"typescript/no-invalid-void-type": "error",
"typescript/no-meaningless-void-operator": "error",
"typescript/no-misused-new": "error",
"typescript/no-misused-spread": "error",
"typescript/no-mixed-enums": "error",
"typescript/no-non-null-asserted-nullish-coalescing": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-redundant-type-constituents": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-parameter-property-assignment": "error",
"typescript/no-unnecessary-qualifier": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-enum-comparison": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-unsafe-unary-minus": "error",
"typescript/no-useless-default-assignment": "error",
"typescript/no-useless-empty-export": "error",
"typescript/no-var-requires": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/only-throw-error": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-find": "error",
"typescript/prefer-function-type": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/prefer-reduce-type-parameter": "error",
"typescript/prefer-return-this-type": "error",
"typescript/prefer-ts-expect-error": "error",
"typescript/require-array-sort-compare": "error",
"typescript/triple-slash-reference": "error",
"import/no-absolute-path": "error",
"import/no-amd": "error",
"import/no-commonjs": "error",
"import/no-dynamic-require": "error",
"import/no-empty-named-blocks": "error",
"import/no-webpack-loader-syntax": "error",
"promise/no-callback-in-promise": "error",
"promise/no-multiple-resolved": "error",
"promise/no-new-statics": "error",
"promise/no-promise-in-callback": "error",
"promise/no-return-in-finally": "error",
"promise/no-return-wrap": "error",
"promise/valid-params": "error",
"node/no-exports-assign": "error",
"node/no-new-require": "error",
"node/no-path-concat": "error",
"oxc/approx-constant": "error",
"oxc/missing-throw": "error",
"oxc/no-accumulating-spread": "error",
"oxc/no-async-endpoint-handlers": "error",
"oxc/no-map-spread": "error",
"oxc/number-arg-out-of-range": "error",
"oxc/uninvoked-array-callback": "error",
"unicorn/consistent-assert": "error",
"unicorn/consistent-date-clone": "error",
"unicorn/consistent-empty-array-spread": "error",
"unicorn/consistent-template-literal-escape": "error",
"unicorn/custom-error-definition": "error",
"unicorn/error-message": "error",
"unicorn/new-for-builtins": "error",
"unicorn/no-abusive-eslint-disable": "error",
"unicorn/no-accessor-recursion": "error",
"unicorn/no-anonymous-default-export": "error",
"unicorn/no-array-method-this-argument": "error",
"unicorn/no-await-in-promise-methods": "error",
"unicorn/no-console-spaces": "error",
"unicorn/no-document-cookie": "error",
"unicorn/no-empty-file": "error",
"unicorn/no-instanceof-builtins": "error",
"unicorn/no-invalid-fetch-options": "error",
"unicorn/no-invalid-remove-event-listener": "error",
"unicorn/no-magic-array-flat-depth": "error",
"unicorn/no-negation-in-equality-check": "error",
"unicorn/no-new-array": "error",
"unicorn/no-new-buffer": "error",
"unicorn/no-single-promise-in-promise-methods": "error",
"unicorn/no-static-only-class": "error",
"unicorn/no-thenable": "error",
"unicorn/no-this-assignment": "error",
"unicorn/no-unnecessary-array-flat-depth": "error",
"unicorn/no-unnecessary-array-splice-count": "error",
"unicorn/no-unnecessary-slice-end": "error",
"unicorn/no-unreadable-iife": "error",
"unicorn/no-useless-collection-argument": "error",
"unicorn/no-useless-error-capture-stack-trace": "error",
"unicorn/no-useless-iterator-to-array": "error",
"unicorn/no-useless-length-check": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-useless-spread": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/prefer-array-find": "error",
"unicorn/prefer-array-flat": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/prefer-array-index-of": "error",
"unicorn/prefer-array-some": "error",
"unicorn/prefer-blob-reading-methods": "error",
"unicorn/prefer-classlist-toggle": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-event-target": "error",
"unicorn/prefer-keyboard-event-key": "error",
"unicorn/prefer-modern-dom-apis": "error",
"unicorn/prefer-negative-index": "error",
"unicorn/prefer-native-coercion-functions": "error",
"unicorn/prefer-object-from-entries": "error",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-size": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-type-error": "error",
"unicorn/require-array-join-separator": "error",
"unicorn/require-number-to-fixed-digits-argument": "error",
"unicorn/throw-new-error": "error",
"vue/no-deprecated-data-object-declaration": "error",
"vue/no-deprecated-delete-set": "error",
"vue/no-deprecated-events-api": "error",
"vue/no-deprecated-model-definition": "error",
"vue/no-deprecated-vue-config-keycodes": "error",
"vue/no-this-in-before-route-enter": "error",
"vue/return-in-computed-property": "error",
"unicorn/require-post-message-target-origin": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-modern-math-apis": "error",
"unicorn/prefer-node-protocol": "error",
"unicorn/prefer-number-properties": "error",
"prefer-exponentiation-operator": "error",
"no-promise-executor-return": "error",
"typescript/no-empty-object-type": "error",
"typescript/return-await": "error",
"typescript/restrict-plus-operands": "error",
"unicorn/prefer-includes": "error",
"no-loop-func": "error",
"typescript/no-dynamic-delete": "error"
},
"overrides": [
{
"files": ["src/**/*.ts", "packages/*/src/**/*.ts"],
"files": [
"src/**/*.ts",
"packages/*/src/**/*.ts"
],
"rules": {
"open-pencil/no-useless-pass-through-wrappers": "error"
}
},
{
"files": ["**/kiwi/kiwi-schema/**"],
"files": [
"**/kiwi/kiwi-schema/**"
],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/no-non-null-assertion": "off",
@ -196,13 +424,18 @@
}
},
{
"files": ["packages/cli/src/**", "packages/mcp/src/**"],
"files": [
"packages/cli/src/**",
"packages/mcp/src/**"
],
"rules": {
"no-console": "off"
}
},
{
"files": ["packages/cli/src/commands/**"],
"files": [
"packages/cli/src/commands/**"
],
"rules": {
"open-pencil/no-raw-console-format": "error"
}
@ -218,11 +451,30 @@
}
},
{
"files": ["scripts/**/*.ts"],
"files": [
"scripts/**/*.ts"
],
"rules": {
"no-console": "off"
}
},
{
"files": [
"packages/core/src/io/formats/fig/export.ts",
"packages/core/src/kiwi/fig/parse/worker.ts"
],
"rules": {
"unicorn/require-post-message-target-origin": "off"
}
}
],
"ignorePatterns": ["node_modules", "dist", "desktop", "*.config.*"]
"ignorePatterns": [
"node_modules",
"dist",
"desktop",
"*.config.*"
],
"categories": {
"correctness": "error"
}
}

View file

@ -11,8 +11,8 @@ import { loadDocument } from '#cli/headless'
const io = new IORegistry(BUILTIN_IO_FORMATS)
const RASTER_FORMATS = ['PNG', 'JPG', 'WEBP']
const ALL_FORMATS = [...RASTER_FORMATS, 'SVG', 'PDF', 'JSX', 'FIG']
const JSX_STYLES = ['openpencil', 'tailwind']
const ALL_FORMATS = new Set([...RASTER_FORMATS, 'SVG', 'PDF', 'JSX', 'FIG'])
const JSX_STYLES = new Set(['openpencil', 'tailwind'])
interface ExportArgs {
file?: string
@ -191,12 +191,12 @@ export default defineCommand({
},
async run({ args }) {
const format = args.format.toUpperCase() as RasterExportFormat | 'SVG' | 'JSX' | 'FIG'
if (!ALL_FORMATS.includes(format)) {
if (!ALL_FORMATS.has(format)) {
printError(`Invalid format "${args.format}". Use png, jpg, webp, svg, pdf, jsx, or fig.`)
process.exit(1)
}
if (format === 'JSX' && !JSX_STYLES.includes(args.style)) {
if (format === 'JSX' && !JSX_STYLES.has(args.style)) {
printError(`Invalid JSX style "${args.style}". Use openpencil or tailwind.`)
process.exit(1)
}

View file

@ -66,7 +66,7 @@ export function drawFlashes(r: SkiaRenderer, canvas: Canvas, graph: SceneGraph):
if (elapsed < FLASH_ATTACK_MS) {
const t = elapsed / FLASH_ATTACK_MS
const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
const ease = t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
opacity = ease
extraPad = (1 - ease) * FLASH_OVERSHOOT
} else if (elapsed < FLASH_ATTACK_MS + FLASH_HOLD_MS) {

View file

@ -249,7 +249,7 @@ export function drawRulerBadge(
export function rulerStep(r: SkiaRenderer): number {
const pixelsPerUnit = r.zoom
const rawStep = RULER_TARGET_PIXEL_SPACING / pixelsPerUnit
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)))
const magnitude = 10 ** Math.floor(Math.log10(rawStep))
const normalized = rawStep / magnitude
if (normalized <= 1) return magnitude

View file

@ -43,7 +43,7 @@ function isCulled(r: SkiaRenderer, node: SceneNode, absX: number, absY: number):
const bw = node.width
const bh = node.height
if (node.rotation !== 0) {
const diag = Math.sqrt(bw * bw + bh * bh)
const diag = Math.hypot(bw, bh)
const cx = absX + bw / 2
const cy = absY + bh / 2
return (

View file

@ -31,6 +31,6 @@ export namespace JSX {
}
export interface ElementChildrenAttribute {
children: {}
children: unknown
}
}

View file

@ -181,12 +181,12 @@ function hasAutoLayoutTriggerProps(props: Record<string, unknown>): boolean {
function parseTrack(token: string): GridTrack {
if (token.endsWith('fr')) {
return { sizing: 'FR', value: parseFloat(token) || 1 }
return { sizing: 'FR', value: Number.parseFloat(token) || 1 }
}
if (token === 'auto') {
return { sizing: 'AUTO', value: 0 }
}
return { sizing: 'FIXED', value: parseFloat(token) || 0 }
return { sizing: 'FIXED', value: Number.parseFloat(token) || 0 }
}
function parseTrackList(value: string): GridTrack[] {
@ -410,8 +410,8 @@ function applyShapeAndEffectOverrides(props: Record<string, unknown>, o: Partial
{
type: 'DROP_SHADOW',
color: c,
offset: { x: parseFloat(parts[0]), y: parseFloat(parts[1]) },
radius: parseFloat(parts[2]),
offset: { x: Number.parseFloat(parts[0]), y: Number.parseFloat(parts[1]) },
radius: Number.parseFloat(parts[2]),
spread: 0,
visible: true
}

View file

@ -77,8 +77,9 @@ export function createVariantActions(ctx: EditorContext) {
for (const childId of node.childIds) {
const child = ctx.graph.getNode(childId)
if (!child) continue
const values = { ...child.componentPropertyValues }
delete values[def.name]
const values = Object.fromEntries(
Object.entries(child.componentPropertyValues).filter(([key]) => key !== def.name)
)
ctx.graph.updateNode(childId, { componentPropertyValues: values })
}
ctx.undo.push({
@ -94,8 +95,9 @@ export function createVariantActions(ctx: EditorContext) {
for (const cid of n.childIds) {
const c = ctx.graph.getNode(cid)
if (!c) continue
const v = { ...c.componentPropertyValues }
delete v[def.name]
const v = Object.fromEntries(
Object.entries(c.componentPropertyValues).filter(([key]) => key !== def.name)
)
ctx.graph.updateNode(cid, { componentPropertyValues: v })
}
}
@ -126,9 +128,11 @@ export function createVariantActions(ctx: EditorContext) {
if (!child) continue
const values = { ...child.componentPropertyValues }
if (prevName in values) {
values[newName] = values[prevName]
delete values[prevName]
ctx.graph.updateNode(childId, { componentPropertyValues: values })
const nextValues = Object.fromEntries(
Object.entries(values).filter(([key]) => key !== prevName)
)
nextValues[newName] = values[prevName]
ctx.graph.updateNode(childId, { componentPropertyValues: nextValues })
}
}
const renamePropertyDef = (name: string) => {

View file

@ -13,7 +13,7 @@ function attrValue(tag: string, attr: string): string | null {
function num(tag: string, attr: string, fallback = 0): number {
const value = attrValue(tag, attr)
return value !== null ? parseFloat(value) : fallback
return value !== null ? Number.parseFloat(value) : fallback
}
function shapeToD(tagName: string, tag: string): string | null {
@ -118,7 +118,7 @@ export function extractPaths(svgBody: string): IconPathInfo[] {
d,
fill: resolveAttr(attrValue(tag, 'fill'), groupAttrs.fill, 'currentColor'),
stroke: resolveAttr(attrValue(tag, 'stroke'), groupAttrs.stroke, null),
strokeWidth: parseFloat(attrValue(tag, 'stroke-width') ?? groupAttrs.strokeWidth ?? '1'),
strokeWidth: Number.parseFloat(attrValue(tag, 'stroke-width') ?? groupAttrs.strokeWidth ?? '1'),
strokeCap: attrValue(tag, 'stroke-linecap') ?? groupAttrs.strokeCap ?? 'butt',
strokeJoin: attrValue(tag, 'stroke-linejoin') ?? groupAttrs.strokeJoin ?? 'miter',
fillRule: fillRuleAttr === 'evenodd' ? 'EVENODD' : 'NONZERO'

View file

@ -74,7 +74,7 @@ function createGradientDef(
if (fill.type === 'GRADIENT_RADIAL' || fill.type === 'GRADIENT_DIAMOND') {
const cx = round(t.m02 * 100)
const cy = round(t.m12 * 100)
const r = round(Math.sqrt(t.m00 * t.m00 + t.m10 * t.m10) * 100)
const r = round(Math.hypot(t.m00, t.m10) * 100)
return {
id,
node: svg(

View file

@ -136,7 +136,7 @@ export function encodeMessage(message: FigmaMessage): Uint8Array {
const ncHex = Buffer.from(ncBytes).toString('hex')
const finalHex = beforeArray + ncHex + afterArray
const finalBytes = new Uint8Array(finalHex.match(/.{2}/g)?.map((b) => parseInt(b, 16)) ?? [])
const finalBytes = new Uint8Array(finalHex.match(/.{2}/g)?.map((b) => Number.parseInt(b, 16)) ?? [])
return compress(finalBytes)
}

View file

@ -47,8 +47,8 @@ export function parseVariableId(variableId: string): GUID | null {
const match = variableId.match(/VariableID:(\d+):(\d+)/)
if (!match) return null
return {
sessionID: parseInt(match[1] ?? '0', 10),
localID: parseInt(match[2] ?? '0', 10)
sessionID: Number.parseInt(match[1] ?? '0', 10),
localID: Number.parseInt(match[2] ?? '0', 10)
}
}

View file

@ -187,7 +187,7 @@ export class ByteBuffer {
writeVarUint64(value: bigint | string): void {
if (typeof value === 'string') value = BigInt(value)
else if (typeof value !== 'bigint')
throw new Error('Expected bigint but got ' + typeof value + ': ' + value)
throw new Error(`Expected bigint but got ${typeof value}: ${String(value)}`)
if (value < 0 || value > BigInt('0xFFFFFFFFFFFFFFFF'))
throw new Error('Outside uint64 range: ' + value)
let mask = BigInt(127)
@ -202,7 +202,7 @@ export class ByteBuffer {
writeVarInt64(value: bigint | string): void {
if (typeof value === 'string') value = BigInt(value)
else if (typeof value !== 'bigint')
throw new Error('Expected bigint but got ' + typeof value + ': ' + value)
throw new Error(`Expected bigint but got ${typeof value}: ${String(value)}`)
if (value < -BigInt('0x8000000000000000') || value > BigInt('0x7FFFFFFFFFFFFFFF'))
throw new Error('Outside int64 range: ' + value)
let one = BigInt(1)

View file

@ -191,14 +191,14 @@ function verify(root: Schema): void {
// Define definitions
for (let i = 0; i < root.definitions.length; i++) {
let definition = root.definitions[i]
if (definedTypes.indexOf(definition.name) !== -1) {
if (definedTypes.includes(definition.name)) {
error(
'The type ' + quote(definition.name) + ' is defined twice',
definition.line,
definition.column
)
}
if (reservedNames.indexOf(definition.name) !== -1) {
if (reservedNames.includes(definition.name)) {
error(
'The type name ' + quote(definition.name) + ' is reserved',
definition.line,
@ -221,7 +221,7 @@ function verify(root: Schema): void {
// Check types
for (let j = 0; j < fields.length; j++) {
let field = fields[j]
if (definedTypes.indexOf(field.type!) === -1) {
if (!definedTypes.includes(field.type!)) {
error(
'The type ' + quote(field.type!) + ' is not defined for field ' + quote(field.name),
field.line,
@ -234,7 +234,7 @@ function verify(root: Schema): void {
let values: number[] = []
for (let j = 0; j < fields.length; j++) {
let field = fields[j]
if (values.indexOf(field.value) !== -1) {
if (values.includes(field.value)) {
error('The id for field ' + quote(field.name) + ' is used twice', field.line, field.column)
}
if (field.value <= 0) {

View file

@ -6,7 +6,7 @@ export function guidToString(guid: GUID): string {
export function stringToGuid(str: string): GUID {
const match = str.match(/^(?:VariableID:|VariableCollectionId:)?(\d+):(\d+)$/)
if (match) return { sessionID: parseInt(match[1], 10), localID: parseInt(match[2], 10) }
if (match) return { sessionID: Number.parseInt(match[1], 10), localID: Number.parseInt(match[2], 10) }
const [session, local] = str.split(':')
return { sessionID: parseInt(session, 10), localID: parseInt(local, 10) }
return { sessionID: Number.parseInt(session, 10), localID: Number.parseInt(local, 10) }
}

View file

@ -1,5 +1,5 @@
import { defineRule } from '#core/lint/rule'
const SCALE = [0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 9999]
const SCALE = new Set([0, 2, 4, 6, 8, 12, 16, 20, 24, 32, 9999])
export default defineRule({
meta: {
id: 'consistent-radius',
@ -8,7 +8,7 @@ export default defineRule({
},
match: ['RECTANGLE', 'FRAME', 'COMPONENT', 'INSTANCE'],
check(node, context) {
if (node.cornerRadius > 0 && !SCALE.includes(node.cornerRadius))
if (node.cornerRadius > 0 && !SCALE.has(node.cornerRadius))
context.report({
node,
message: `Corner radius ${node.cornerRadius}px is not in scale`,

View file

@ -10,7 +10,7 @@ export class GPUTimer {
private ext: EXTDisjointTimerQuery | null = null
private pending: WebGLQuery[] = []
private activeQuery: WebGLQuery | null = null
private _lastGpuTimeMs = NaN
private _lastGpuTimeMs = Number.NaN
get available(): boolean {
return this.ext !== null

View file

@ -124,7 +124,7 @@ export const analyzeColorsCommand: RpcCommand<AnalyzeColorsArgs, AnalyzeColorsRe
// ── analyze typography ──
export interface AnalyzeTypographyArgs {}
export type AnalyzeTypographyArgs = Record<string, never>
export interface TypographyStyle {
family: string

View file

@ -14,18 +14,20 @@ function walkNodes(graph: SceneGraph, rootId: string, fn: (node: SceneNode) => b
return true
}
function countNodes(graph: SceneGraph, pageId: string): number {
function countDescendants(graph: SceneGraph, rootId: string): number {
let count = 0
const page = graph.getNode(pageId)
if (page)
for (const cid of page.childIds)
walkNodes(graph, cid, () => {
count++
return true
})
walkNodes(graph, rootId, () => {
count++
return true
})
return count
}
function countNodes(graph: SceneGraph, pageId: string): number {
const page = graph.getNode(pageId)
return page?.childIds.reduce((count, id) => count + countDescendants(graph, id), 0) ?? 0
}
function nodeFrame(node: SceneNode) {
return {
x: Math.round(node.x),
@ -54,18 +56,19 @@ export const infoCommand: RpcCommand<void, InfoResult> = {
const fonts = new Set<string>()
const pageCounts: Record<string, number> = {}
const countNode = (node: SceneNode) => {
totalNodes++
types[node.type] = (types[node.type] ?? 0) + 1
if (node.fontFamily) fonts.add(node.fontFamily)
return true
}
for (const page of pages) {
let pageCount = 0
const beforePage = totalNodes
for (const cid of page.childIds) {
walkNodes(graph, cid, (node) => {
totalNodes++
pageCount++
types[node.type] = (types[node.type] ?? 0) + 1
if (node.fontFamily) fonts.add(node.fontFamily)
return true
})
walkNodes(graph, cid, countNode)
}
pageCounts[page.name] = pageCount
pageCounts[page.name] = totalNodes - beforePage
}
return { pages: pages.length, totalNodes, types, fonts: [...fonts].sort(), pageCounts }

View file

@ -463,7 +463,7 @@ export class SceneGraph {
let idx = insertIndex
if (
oldParent === newParent &&
idx > (oldParent.childIds.indexOf(nodeId) === -1 ? idx : oldParent.childIds.length)
idx > (!oldParent.childIds.includes(nodeId) ? idx : oldParent.childIds.length)
) {
// Already removed above, no adjustment needed
}

View file

@ -21,9 +21,9 @@ export function removeVariable(graph: SceneGraph, id: string): void {
collection.variableIds = collection.variableIds.filter((vid) => vid !== id)
}
for (const node of graph.nodes.values()) {
for (const [field, varId] of Object.entries(node.boundVariables)) {
if (varId === id) delete node.boundVariables[field]
}
node.boundVariables = Object.fromEntries(
Object.entries(node.boundVariables).filter(([, varId]) => varId !== id)
)
}
}
@ -140,7 +140,11 @@ export function removeMode(graph: SceneGraph, collectionId: string, modeId: stri
}
for (const varId of collection.variableIds) {
const variable = graph.variables.get(varId)
if (variable) delete variable.valuesByMode[modeId]
if (variable) {
variable.valuesByMode = Object.fromEntries(
Object.entries(variable.valuesByMode).filter(([id]) => id !== modeId)
)
}
}
if (graph.activeMode.get(collectionId) === modeId) {
graph.activeMode.set(collectionId, collection.defaultModeId)
@ -232,5 +236,9 @@ export function bindVariable(
export function unbindVariable(graph: SceneGraph, nodeId: string, field: string): void {
const node = graph.nodes.get(nodeId)
if (node) delete node.boundVariables[field]
if (node) {
node.boundVariables = Object.fromEntries(
Object.entries(node.boundVariables).filter(([key]) => key !== field)
)
}
}

View file

@ -49,8 +49,13 @@ export function removeStyleFromRange(
for (let i = start; i < end && i < textLength; i++) {
if (chars[i]) {
const copy = { ...chars[i] }
for (const k of keys) delete copy[k]
const current = chars[i]
if (!current) continue
const copy = Object.fromEntries(
Object.entries(current).filter(
([key]) => !keys.includes(key as keyof CharacterStyleOverride)
)
) as CharacterStyleOverride
chars[i] = Object.keys(copy).length > 0 ? copy : null
}
}

View file

@ -18,7 +18,7 @@ function parseSvgViewBox(svg: string): Rect | null {
function parseSvgDimension(svg: string, attr: string): number | null {
const match = svg.match(new RegExp(`\\b${attr}="([^"]+)"`))
if (!match) return null
const n = parseFloat(match[1])
const n = Number.parseFloat(match[1])
return Number.isFinite(n) && n > 0 ? n : null
}

View file

@ -285,7 +285,7 @@ function nearestPointOnLine(px: number, py: number, p0: Vector, p1: Vector): Nea
const y = p0.y + t * dy
const ddx = x - px
const ddy = y - py
return { t, x, y, distance: Math.sqrt(ddx * ddx + ddy * ddy) }
return { t, x, y, distance: Math.hypot(ddx, ddy) }
}
/** Find nearest point across all segments in a VectorNetwork. */

View file

@ -3,8 +3,8 @@ import { serve } from '@hono/node-server'
import { startServer } from './server.js'
const port = parseInt(process.env.PORT ?? '7600', 10)
const wsPort = parseInt(process.env.WS_PORT ?? '7601', 10)
const port = Number.parseInt(process.env.PORT ?? '7600', 10)
const wsPort = Number.parseInt(process.env.WS_PORT ?? '7601', 10)
const host = process.env.HOST ?? '127.0.0.1'
const { app, httpPort } = startServer({

View file

@ -5,7 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { MCP_VERSION, registerTools } from './server.js'
import { createStdioRpcBridge } from './stdio-bridge.js'
const wsPort = parseInt(process.env.WS_PORT ?? '7601', 10)
const wsPort = Number.parseInt(process.env.WS_PORT ?? '7601', 10)
const wsHost = process.env.HOST ?? '127.0.0.1'
const enableEval = process.env.OPENPENCIL_MCP_EVAL === '1'
const mcpRoot = process.env.OPENPENCIL_MCP_ROOT?.trim() || process.cwd()

View file

@ -1,5 +1,5 @@
import { resolve } from 'path'
import { copyFileSync, existsSync, mkdirSync } from 'fs'
import { resolve } from 'node:path'
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

View file

@ -32,7 +32,9 @@ export function useCanvasKitLoader({
setCanvasKit(await getCanvasKit())
if (isDestroyed()) return
await new Promise((resolve) => requestAnimationFrame(resolve))
await new Promise((resolve) => {
requestAnimationFrame(resolve)
})
createSurface(canvas)
await loadFonts()
if (isDestroyed()) return

View file

@ -153,8 +153,8 @@ export function createVariableValueActions(
function parseVariableValue(variable: Variable, raw: string): VariableValue | undefined {
if (variable.type === 'COLOR') return parseColor(raw.startsWith('#') ? raw : `#${raw}`)
if (variable.type === 'FLOAT') {
const num = parseFloat(raw)
return isNaN(num) ? undefined : num
const num = Number.parseFloat(raw)
return Number.isNaN(num) ? undefined : num
}
if (variable.type === 'BOOLEAN') return raw.toLowerCase() === 'true'
return raw

View file

@ -187,7 +187,7 @@ async function diff() {
.quiet()
.nothrow()
const diffPixels = parseInt(result.stderr.toString().trim(), 10) || 0
const diffPixels = Number.parseInt(result.stderr.toString().trim(), 10) || 0
const [w, h] = figmaSize.split('x').map(Number)
const total = w * h
const pct = ((diffPixels / total) * 100).toFixed(2)

View file

@ -1,5 +1,7 @@
export function yieldToUI(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => resolve()))
return new Promise((resolve) => {
requestAnimationFrame(() => resolve())
})
}
type ViewportSize = { width: number; height: number }

View file

@ -6,7 +6,7 @@ async function readClipboardHtml() {
const items = await navigator.clipboard.read()
for (const item of items) {
if (!item.types.includes('text/html')) continue
return await (await item.getType('text/html')).text()
return (await item.getType('text/html')).text()
}
return null
}

View file

@ -85,7 +85,9 @@ export function closeTab(tabId: string) {
}
function yieldToUI(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => resolve()))
return new Promise((resolve) => {
requestAnimationFrame(() => resolve())
})
}
export async function openFileInNewTab(

View file

@ -86,7 +86,9 @@ test('CJK text waits for fallback fonts and repaints after they load', async ({
const beforeFallbackRenderCount = fallbackRenderCount
releaseCJKFallback?.()
await new Promise((resolve) => setTimeout(resolve, 0))
await new Promise((resolve) => {
setTimeout(resolve, 0)
})
await new Promise(requestAnimationFrame)
return {

View file

@ -107,7 +107,9 @@ describe('acp-permission', () => {
const result = await promise
expect(result.outcome.optionId).toBe('allow')
// Timer should be cleared — wait to ensure no stale timeout fires
await new Promise((r) => setTimeout(r, 50))
await new Promise((resolve) => {
setTimeout(resolve, 50)
})
expect(permissionQueue.value).toHaveLength(0)
})

View file

@ -1,7 +1,7 @@
import { expect, setDefaultTimeout, test } from 'bun:test'
import { randomUUID } from 'crypto'
import { tmpdir } from 'os'
import { join } from 'path'
import { randomUUID } from 'node:crypto'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { cliSourcePath, repoPath } from '#tests/helpers/paths'
import { heavy } from '#tests/helpers/test-utils'
@ -54,7 +54,7 @@ heavy('eval CLI', () => {
'return figma.root.children.length'
])
expect(exitCode).toBe(0)
const count = parseInt(stdout, 10)
const count = Number.parseInt(stdout, 10)
expect(count).toBeGreaterThan(0)
})

View file

@ -2,26 +2,24 @@ import { describe, test, expect } from 'bun:test'
import { createUndoManager, noop, undoEntry } from '#tests/helpers/undo'
function assignValue(setValue: (value: number) => void, value: number) {
return () => setValue(value)
}
describe('UndoManager idle-timer batching', () => {
test('rapid pushes inside a batch produce a single undo entry', () => {
const undo = createUndoManager()
let value = 0
const setValue = (nextValue: number) => {
value = nextValue
}
undo.beginBatch('drag color')
for (let i = 1; i <= 10; i++) {
const prev = value
const next = i
undo.push(
undoEntry(
`step ${i}`,
() => {
value = next
},
() => {
value = prev
}
)
)
undo.push(undoEntry(`step ${i}`, assignValue(setValue, next), assignValue(setValue, prev)))
value = next
}
undo.commitBatch()

View file

@ -1,6 +1,6 @@
import { describe, test, expect, beforeAll, setDefaultTimeout } from 'bun:test'
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
parseFigFile,

View file

@ -1,5 +1,5 @@
import { expect, setDefaultTimeout, test } from 'bun:test'
import { readFileSync } from 'fs'
import { readFileSync } from 'node:fs'
import { importNodeChanges } from '#core/kiwi'
import { parseFigBuffer } from '#core/kiwi/fig/parse/core'

View file

@ -1,5 +1,5 @@
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { bench, group, run } from 'mitata'

View file

@ -29,7 +29,7 @@ describe('FrameStats', () => {
expect(stats.scenePictureCacheHit).toBe(true)
})
it('handles GPU time as NaN initially', () => {
it('handles GPU time as Number.NaN initially', () => {
const stats = new FrameStats()
stats.recordFrame(1)
expect(stats.gpuTime).toBe(0)

View file

@ -1,5 +1,5 @@
import { beforeAll, describe, expect, test } from 'bun:test'
import { readFileSync } from 'fs'
import { readFileSync } from 'node:fs'
import {
computeAllLayouts,

View file

@ -1,5 +1,5 @@
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { parseFigFile } from '@open-pencil/core'
import type { SceneGraph, SceneNode } from '@open-pencil/core'

View file

@ -1,4 +1,4 @@
import { resolve } from 'path'
import { resolve } from 'node:path'
export function createOpenPencilAliases(rootDir: string) {
const emptyNodeModule = resolve(rootDir, 'vite/empty-node-module.ts')

View file

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto'
import { randomUUID } from 'node:crypto'
import { automationPlugin } from '../src/app/automation/bridge/vite-plugin'

View file

@ -1,4 +1,4 @@
import { copyFileSync, createReadStream, existsSync, mkdirSync } from 'fs'
import { copyFileSync, createReadStream, existsSync, mkdirSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import type { Connect, Plugin, ResolvedConfig } from 'vite'