diff --git a/oxlint.json b/oxlint.json index b9889dbec..57beab259 100644 --- a/oxlint.json +++ b/oxlint.json @@ -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" + } } diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 87b832446..b3e65eb46 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -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) } diff --git a/packages/core/src/canvas/overlays/feedback.ts b/packages/core/src/canvas/overlays/feedback.ts index 4c922a322..59004d3e6 100644 --- a/packages/core/src/canvas/overlays/feedback.ts +++ b/packages/core/src/canvas/overlays/feedback.ts @@ -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) { diff --git a/packages/core/src/canvas/rulers.ts b/packages/core/src/canvas/rulers.ts index 309646e6f..1966d83bf 100644 --- a/packages/core/src/canvas/rulers.ts +++ b/packages/core/src/canvas/rulers.ts @@ -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 diff --git a/packages/core/src/canvas/scene.ts b/packages/core/src/canvas/scene.ts index 0f2209287..1296aef9e 100644 --- a/packages/core/src/canvas/scene.ts +++ b/packages/core/src/canvas/scene.ts @@ -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 ( diff --git a/packages/core/src/design-jsx/jsx-runtime.ts b/packages/core/src/design-jsx/jsx-runtime.ts index a40452ed1..f2fc2dd97 100644 --- a/packages/core/src/design-jsx/jsx-runtime.ts +++ b/packages/core/src/design-jsx/jsx-runtime.ts @@ -31,6 +31,6 @@ export namespace JSX { } export interface ElementChildrenAttribute { - children: {} + children: unknown } } diff --git a/packages/core/src/design-jsx/props-overrides.ts b/packages/core/src/design-jsx/props-overrides.ts index a3e1aa680..801a121e0 100644 --- a/packages/core/src/design-jsx/props-overrides.ts +++ b/packages/core/src/design-jsx/props-overrides.ts @@ -181,12 +181,12 @@ function hasAutoLayoutTriggerProps(props: Record): 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, 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 } diff --git a/packages/core/src/editor/components/variants.ts b/packages/core/src/editor/components/variants.ts index 45e21331d..0ff79de42 100644 --- a/packages/core/src/editor/components/variants.ts +++ b/packages/core/src/editor/components/variants.ts @@ -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) => { diff --git a/packages/core/src/icons/svg.ts b/packages/core/src/icons/svg.ts index 294846823..3e1a66187 100644 --- a/packages/core/src/icons/svg.ts +++ b/packages/core/src/icons/svg.ts @@ -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' diff --git a/packages/core/src/io/formats/svg/defs.ts b/packages/core/src/io/formats/svg/defs.ts index cb2ce2997..f0ca2fc8a 100644 --- a/packages/core/src/io/formats/svg/defs.ts +++ b/packages/core/src/io/formats/svg/defs.ts @@ -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( diff --git a/packages/core/src/kiwi/binary/codec.ts b/packages/core/src/kiwi/binary/codec.ts index 4ed43f5e4..1a8a23231 100644 --- a/packages/core/src/kiwi/binary/codec.ts +++ b/packages/core/src/kiwi/binary/codec.ts @@ -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) } diff --git a/packages/core/src/kiwi/binary/variable-bindings.ts b/packages/core/src/kiwi/binary/variable-bindings.ts index b09d9331c..7bb425190 100644 --- a/packages/core/src/kiwi/binary/variable-bindings.ts +++ b/packages/core/src/kiwi/binary/variable-bindings.ts @@ -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) } } diff --git a/packages/core/src/kiwi/kiwi-schema/bb.ts b/packages/core/src/kiwi/kiwi-schema/bb.ts index 5d35973c2..e99b89e69 100644 --- a/packages/core/src/kiwi/kiwi-schema/bb.ts +++ b/packages/core/src/kiwi/kiwi-schema/bb.ts @@ -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) diff --git a/packages/core/src/kiwi/kiwi-schema/parser.ts b/packages/core/src/kiwi/kiwi-schema/parser.ts index c2a0a80ff..7f599623d 100644 --- a/packages/core/src/kiwi/kiwi-schema/parser.ts +++ b/packages/core/src/kiwi/kiwi-schema/parser.ts @@ -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) { diff --git a/packages/core/src/kiwi/node-change/guid.ts b/packages/core/src/kiwi/node-change/guid.ts index 035ad6ffa..382b99e27 100644 --- a/packages/core/src/kiwi/node-change/guid.ts +++ b/packages/core/src/kiwi/node-change/guid.ts @@ -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) } } diff --git a/packages/core/src/lint/rules/consistent-radius.ts b/packages/core/src/lint/rules/consistent-radius.ts index 89ed17c52..eaddfa7e2 100644 --- a/packages/core/src/lint/rules/consistent-radius.ts +++ b/packages/core/src/lint/rules/consistent-radius.ts @@ -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`, diff --git a/packages/core/src/profiler/gpu-timer.ts b/packages/core/src/profiler/gpu-timer.ts index 9dda154be..4928c9c91 100644 --- a/packages/core/src/profiler/gpu-timer.ts +++ b/packages/core/src/profiler/gpu-timer.ts @@ -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 diff --git a/packages/core/src/rpc/analyze-commands.ts b/packages/core/src/rpc/analyze-commands.ts index 60b4cda69..6396226b8 100644 --- a/packages/core/src/rpc/analyze-commands.ts +++ b/packages/core/src/rpc/analyze-commands.ts @@ -124,7 +124,7 @@ export const analyzeColorsCommand: RpcCommand export interface TypographyStyle { family: string diff --git a/packages/core/src/rpc/read-commands.ts b/packages/core/src/rpc/read-commands.ts index ca78f1b5e..bdb790059 100644 --- a/packages/core/src/rpc/read-commands.ts +++ b/packages/core/src/rpc/read-commands.ts @@ -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 = { const fonts = new Set() const pageCounts: Record = {} + 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 } diff --git a/packages/core/src/scene-graph/index.ts b/packages/core/src/scene-graph/index.ts index a5eb53ae3..6b35e1848 100644 --- a/packages/core/src/scene-graph/index.ts +++ b/packages/core/src/scene-graph/index.ts @@ -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 } diff --git a/packages/core/src/scene-graph/variables.ts b/packages/core/src/scene-graph/variables.ts index 4efe31e9c..0215b6b6c 100644 --- a/packages/core/src/scene-graph/variables.ts +++ b/packages/core/src/scene-graph/variables.ts @@ -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) + ) + } } diff --git a/packages/core/src/text/style-runs.ts b/packages/core/src/text/style-runs.ts index 5ce2e6bfb..a7fe3f21e 100644 --- a/packages/core/src/text/style-runs.ts +++ b/packages/core/src/text/style-runs.ts @@ -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 } } diff --git a/packages/core/src/tools/create/svg.ts b/packages/core/src/tools/create/svg.ts index 70e82f1cc..8fd4dd45c 100644 --- a/packages/core/src/tools/create/svg.ts +++ b/packages/core/src/tools/create/svg.ts @@ -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 } diff --git a/packages/core/src/vector/curve-math.ts b/packages/core/src/vector/curve-math.ts index a7fdf4ff8..e7d02c670 100644 --- a/packages/core/src/vector/curve-math.ts +++ b/packages/core/src/vector/curve-math.ts @@ -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. */ diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index ebdc08da7..7e4ae290e 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -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({ diff --git a/packages/mcp/src/stdio.ts b/packages/mcp/src/stdio.ts index 338207d3c..0e61949ee 100644 --- a/packages/mcp/src/stdio.ts +++ b/packages/mcp/src/stdio.ts @@ -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() diff --git a/packages/vue/example/vite.config.ts b/packages/vue/example/vite.config.ts index 5e26b6a7e..b5faaf704 100644 --- a/packages/vue/example/vite.config.ts +++ b/packages/vue/example/vite.config.ts @@ -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' diff --git a/packages/vue/src/canvas/surface/kit-loader.ts b/packages/vue/src/canvas/surface/kit-loader.ts index 200455314..830067429 100644 --- a/packages/vue/src/canvas/surface/kit-loader.ts +++ b/packages/vue/src/canvas/surface/kit-loader.ts @@ -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 diff --git a/packages/vue/src/variables/helpers.ts b/packages/vue/src/variables/helpers.ts index 9b0102481..ad16bb676 100644 --- a/packages/vue/src/variables/helpers.ts +++ b/packages/vue/src/variables/helpers.ts @@ -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 diff --git a/scripts/visual-compare.ts b/scripts/visual-compare.ts index 29663948a..6858081b8 100644 --- a/scripts/visual-compare.ts +++ b/scripts/visual-compare.ts @@ -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) diff --git a/src/app/document/io/browser.ts b/src/app/document/io/browser.ts index d89f0504d..804f73cf1 100644 --- a/src/app/document/io/browser.ts +++ b/src/app/document/io/browser.ts @@ -1,5 +1,7 @@ export function yieldToUI(): Promise { - return new Promise((resolve) => requestAnimationFrame(() => resolve())) + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) } type ViewportSize = { width: number; height: number } diff --git a/src/app/editor/clipboard/paste-to-replace.ts b/src/app/editor/clipboard/paste-to-replace.ts index d21113578..a9459f729 100644 --- a/src/app/editor/clipboard/paste-to-replace.ts +++ b/src/app/editor/clipboard/paste-to-replace.ts @@ -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 } diff --git a/src/app/tabs/index.ts b/src/app/tabs/index.ts index 42c8cda11..7364fd46b 100644 --- a/src/app/tabs/index.ts +++ b/src/app/tabs/index.ts @@ -85,7 +85,9 @@ export function closeTab(tabId: string) { } function yieldToUI(): Promise { - return new Promise((resolve) => requestAnimationFrame(() => resolve())) + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) } export async function openFileInNewTab( diff --git a/tests/e2e/fonts/cjk-fallback.spec.ts b/tests/e2e/fonts/cjk-fallback.spec.ts index 53ba56815..7dc7f4e3a 100644 --- a/tests/e2e/fonts/cjk-fallback.spec.ts +++ b/tests/e2e/fonts/cjk-fallback.spec.ts @@ -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 { diff --git a/tests/engine/acp/permission.test.ts b/tests/engine/acp/permission.test.ts index fb33c44f1..5069630fd 100644 --- a/tests/engine/acp/permission.test.ts +++ b/tests/engine/acp/permission.test.ts @@ -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) }) diff --git a/tests/engine/cli/eval.test.ts b/tests/engine/cli/eval.test.ts index eec79f890..95cc812da 100644 --- a/tests/engine/cli/eval.test.ts +++ b/tests/engine/cli/eval.test.ts @@ -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) }) diff --git a/tests/engine/editor/undo/batch-idle.test.ts b/tests/engine/editor/undo/batch-idle.test.ts index 787f50503..d2df7393b 100644 --- a/tests/engine/editor/undo/batch-idle.test.ts +++ b/tests/engine/editor/undo/batch-idle.test.ts @@ -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() diff --git a/tests/engine/io/fig/export/worker.test.ts b/tests/engine/io/fig/export/worker.test.ts index d7d9a67fe..e9bddb4c0 100644 --- a/tests/engine/io/fig/export/worker.test.ts +++ b/tests/engine/io/fig/export/worker.test.ts @@ -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, diff --git a/tests/engine/io/fig/heavy/component-metadata.test.ts b/tests/engine/io/fig/heavy/component-metadata.test.ts index 99ce72db9..238ee8f4a 100644 --- a/tests/engine/io/fig/heavy/component-metadata.test.ts +++ b/tests/engine/io/fig/heavy/component-metadata.test.ts @@ -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' diff --git a/tests/engine/perf.bench.ts b/tests/engine/perf.bench.ts index c12578572..66e7371f3 100644 --- a/tests/engine/perf.bench.ts +++ b/tests/engine/perf.bench.ts @@ -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' diff --git a/tests/engine/profiler/frame-stats.test.ts b/tests/engine/profiler/frame-stats.test.ts index c91536317..7882682e4 100644 --- a/tests/engine/profiler/frame-stats.test.ts +++ b/tests/engine/profiler/frame-stats.test.ts @@ -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) diff --git a/tests/engine/render/canvas/cache.test.ts b/tests/engine/render/canvas/cache.test.ts index dc1a848de..4622fde9d 100644 --- a/tests/engine/render/canvas/cache.test.ts +++ b/tests/engine/render/canvas/cache.test.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, test } from 'bun:test' -import { readFileSync } from 'fs' +import { readFileSync } from 'node:fs' import { computeAllLayouts, diff --git a/tests/helpers/fig-fixtures.ts b/tests/helpers/fig-fixtures.ts index ecee3507c..806abac0c 100644 --- a/tests/helpers/fig-fixtures.ts +++ b/tests/helpers/fig-fixtures.ts @@ -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' diff --git a/vite/aliases.ts b/vite/aliases.ts index 2dbf0499c..b15fbf664 100644 --- a/vite/aliases.ts +++ b/vite/aliases.ts @@ -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') diff --git a/vite/automation.ts b/vite/automation.ts index 54e28492f..8c129b474 100644 --- a/vite/automation.ts +++ b/vite/automation.ts @@ -1,4 +1,4 @@ -import { randomUUID } from 'crypto' +import { randomUUID } from 'node:crypto' import { automationPlugin } from '../src/app/automation/bridge/vite-plugin' diff --git a/vite/canvaskit-assets.ts b/vite/canvaskit-assets.ts index c53137d13..08091c189 100644 --- a/vite/canvaskit-assets.ts +++ b/vite/canvaskit-assets.ts @@ -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'