From 1649958ec084fb6d81bd94e8886db14274278509 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Thu, 9 Jan 2025 08:35:24 +0200 Subject: [PATCH 01/15] refactor: migrate from eslint to biome and improve architecture - Replace ESLint with Biome for linting and formatting - Configure Biome with strict rules for: - Type checking and type safety - Code correctness (unused imports, variables, parameters) - Security (noExplicitAny, noConsole, dangerouslySetInnerHTML) - Improve VSCode integration: - Set up Biome formatters for TypeScript, JavaScript, and JSON - Configure Prettier for Markdown and HTML - Add automatic code actions and formatting - Improve React internals: - Remove react-reconciler dependency - Use bippy for Fiber/FiberRoot types - Use installRDTHook for React DevTools hook to avoid side effects - Update package dependencies and development tooling - General improvements and code cleanup BREAKING CHANGE: ESLint configuration removed in favor of Biome --- .eslintrc.js | 89 -- .vscode/settings.json | 34 +- biome.json | 67 +- examples/sierpinski/package.json | 2 +- git | 0 package.json | 14 +- packages/extension/.env.example | 2 +- packages/extension/package.json | 12 +- packages/extension/src/background/icon.ts | 4 +- packages/extension/src/background/index.ts | 2 +- packages/extension/src/content/index.ts | 6 +- packages/extension/src/types/global.d.ts | 26 +- packages/extension/src/utils/helpers.ts | 54 +- packages/extension/tsconfig.json | 5 +- packages/extension/vite.config.ts | 50 +- packages/scan/global.d.ts | 5 + packages/scan/package.json | 5 +- packages/scan/scripts/bump-version.js | 14 +- packages/scan/src/auto.ts | 6 +- packages/scan/src/cli.mts | 52 +- packages/scan/src/core/fast-serialize.test.ts | 10 +- packages/scan/src/core/index.ts | 106 +- packages/scan/src/core/instrumentation.ts | 42 +- packages/scan/src/core/monitor/constants.ts | 31 +- packages/scan/src/core/monitor/index.ts | 16 +- packages/scan/src/core/monitor/network.ts | 33 +- .../scan/src/core/monitor/params/utils.ts | 2 +- packages/scan/src/core/monitor/performance.ts | 34 +- packages/scan/src/core/monitor/types.ts | 2 +- packages/scan/src/core/monitor/utils.ts | 23 +- packages/scan/src/core/utils.ts | 47 +- packages/scan/src/index.ts | 4 +- packages/scan/src/install-hook.ts | 11 +- .../react-component-name/__tests__/utils.ts | 10 +- .../scan/src/react-component-name/astro.ts | 3 +- .../scan/src/react-component-name/index.ts | 19 +- packages/scan/src/types.d.ts | 61 - packages/scan/src/types.ts | 50 + .../src/web/assets/css/styles.tailwind.css | 16 +- .../scan/src/web/components/icon/index.tsx | 72 +- .../src/web/components/inspector/index.tsx | 34 +- .../components/inspector/overlay/index.tsx | 14 +- .../web/components/inspector/overlay/utils.ts | 114 +- .../src/web/components/inspector/utils.ts | 159 ++- .../scan/src/web/components/widget/header.tsx | 3 + .../scan/src/web/components/widget/helpers.ts | 10 +- .../scan/src/web/components/widget/index.tsx | 22 +- .../web/components/widget/resize-handle.tsx | 351 +++--- .../web/components/widget/toolbar/arrows.tsx | 7 +- .../web/components/widget/toolbar/index.tsx | 27 +- .../web/components/widget/toolbar/search.tsx | 44 +- packages/scan/src/web/overlay.ts | 1 - packages/scan/src/web/state.ts | 8 +- packages/scan/src/web/toolbar.tsx | 2 +- packages/scan/src/web/utils/helpers.ts | 30 +- packages/scan/src/web/utils/log.ts | 16 +- packages/scan/src/web/utils/outline-worker.ts | 2 +- packages/scan/src/web/utils/outline.ts | 64 +- .../scan/src/web/utils/preact/constant.ts | 17 +- packages/scan/tsconfig.json | 2 +- packages/scan/tsup.config.ts | 31 +- pnpm-lock.yaml | 1068 ++++++++--------- scripts/bump-version.js | 11 +- scripts/version-warning.mjs | 66 +- scripts/workspace.mjs | 59 +- 65 files changed, 1645 insertions(+), 1558 deletions(-) delete mode 100644 .eslintrc.js delete mode 100644 git delete mode 100644 packages/scan/src/types.d.ts create mode 100644 packages/scan/src/types.ts diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 506f30f8..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,89 +0,0 @@ -const { resolve } = require("node:path"); - -module.exports = { - root: true, - extends: [ - require.resolve("@vercel/style-guide/eslint/node"), - require.resolve("@vercel/style-guide/eslint/browser"), - require.resolve("@vercel/style-guide/eslint/typescript"), - "plugin:tailwindcss/recommended", - ], - ignorePatterns: ["**/dist/**", "**/node_modules/**", "**/test/**"], - parserOptions: { - project: [ - resolve(__dirname, "tsconfig.json"), // Root tsconfig - resolve(__dirname, "packages/scan/tsconfig.json"), // Scan package tsconfig - ], - ecmaVersion: 2020, - sourceType: "module", - }, - rules: { - "@typescript-eslint/restrict-plus-operands": "off", - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/explicit-function-return-type": "off", - "import/no-default-export": "off", - "no-bitwise": "off", - "@typescript-eslint/prefer-optional-chain": "off", - "@typescript-eslint/consistent-indexed-object-style": "off", - "import/no-extraneous-dependencies": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-shadow": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-loop-func": "off", - "eslint-comments/disable-enable-pair": "off", - "import/no-cycle": "off", - "no-nested-ternary": "off", - "no-param-reassign": "off", - "tsdoc/syntax": "off", - "eslint-comments/require-description": "off", - "import/no-relative-packages": "off", - "@typescript-eslint/no-unnecessary-condition": "off", - "@typescript-eslint/no-confusing-void-expression": "off", - "@typescript-eslint/require-await": "off", - "import/no-named-as-default": "off", - "no-implicit-coercion": "off", - "@typescript-eslint/no-redundant-type-constituents": "off", - "object-shorthand": "off", - "@typescript-eslint/no-non-null-asserted-optional-chain": "off", - "no-useless-return": "off", - "func-names": "off", - "@typescript-eslint/prefer-for-of": "off", - "@typescript-eslint/restrict-template-expressions": "off", - "@typescript-eslint/array-type": ["error", { default: "generic" }], - "no-console": "warn", - eqeqeq: ["error", "null"], - }, - settings: { - "import/resolver": { - typescript: { - project: [ - resolve(__dirname, "tsconfig.json"), // Root tsconfig - resolve(__dirname, "packages/**/tsconfig.json"), // Scan package tsconfig - ], - }, - }, - }, - overrides: [ - { - files: ["*.json"], - parser: "jsonc-eslint-parser", - plugins: ["jsonc"], - rules: { - "jsonc/no-comments": "off", - }, - }, - { - files: ["*.tsx", "*.ts", "*.js"], - plugins: ["tailwindcss"], - }, - { - files: ["*.mts"], - parser: "@typescript-eslint/parser", - }, - ], -}; diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ff26bff..447349ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,24 @@ { - "editor.formatOnSave": true, "editor.defaultFormatter": "biomejs.biome", - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true, - "typescript.preferences.importModuleSpecifier": "non-relative", - "typescript.preferences.includePackageJsonAutoImports": "on", - "files.associations": { - "*.css": "css" + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "always", + "quickfix.biome": "always" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "editor.quickSuggestions": { - "strings": true + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "css.validate": false, - "tailwindCSS.validate": true, - "editor.colorDecorators": true, - "[css]": { - "editor.formatOnSave": false - } + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/biome.json b/biome.json index 6be94cf5..4bc92391 100644 --- a/biome.json +++ b/biome.json @@ -7,28 +7,69 @@ }, "files": { "ignoreUnknown": false, - "ignore": [] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 80, - "lineEnding": "lf" + "ignore": [ + "examples/**", + "**/dist/**", + "**/build/**", + "node_modules", + "**/node_modules/**", + "**/*.css", + "**/*.astro", + "packages/website", + "kitchen-sink" + ] }, "organizeImports": { "enabled": true }, "linter": { - "enabled": false + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedFunctionParameters": { + "level": "warn", + "fix": "unsafe" + }, + "noUnusedImports": { + "level": "warn", + "fix": "unsafe" + }, + "noUnusedLabels": { + "level": "warn", + "fix": "unsafe" + }, + "noUnusedPrivateClassMembers": { + "level": "warn", + "fix": "unsafe" + }, + "noUnusedVariables": { + "level": "warn", + "fix": "unsafe" + } + }, + "suspicious": { + "noExplicitAny": "error", + "noConsole": "error" + }, + "security": { + "noDangerouslySetInnerHtml": "error" + }, + "style": { + "noNonNullAssertion": "error" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 }, "javascript": { "formatter": { "quoteStyle": "single", - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 80, - "lineEnding": "lf" + "trailingCommas": "all" } } } diff --git a/examples/sierpinski/package.json b/examples/sierpinski/package.json index 8053a8ce..a5e0f6bd 100644 --- a/examples/sierpinski/package.json +++ b/examples/sierpinski/package.json @@ -15,7 +15,7 @@ "vite-plugin-inspect": "^0.8.7" }, "devDependencies": { - "@vitejs/plugin-react": "^4.3.1", + "@vitejs/plugin-react": "^4.3.4", "vite": "^5.4.3" } } diff --git a/git b/git deleted file mode 100644 index e69de29b..00000000 diff --git a/package.json b/package.json index 772ed7a4..f41cfbe0 100644 --- a/package.json +++ b/package.json @@ -4,28 +4,24 @@ "scripts": { "build": "WORKSPACE_BUILD=true node scripts/workspace.mjs build", "postbuild": "node scripts/version-warning.mjs", + "postinstall": "pnpm build", "dev": "node scripts/workspace.mjs dev", "pack": "node scripts/workspace.mjs pack", "pack:bump": "pnpm --filter scan pack:bump", - "lint": "pnpm --parallel lint", - "eslint:fix": "eslint --fix packages/*" + "lint": "biome lint .", + "format": "biome format . --write", + "check": "biome check . --write" }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", "@vercel/style-guide": "^6.0.0", "autoprefixer": "^10.4.20", "boxen": "^8.0.1", "chalk": "^5.3.0", - "eslint": "^8.57.1", - "eslint-import-resolver-typescript": "^3.7.0", - "eslint-plugin-jsonc": "^2.18.2", - "eslint-plugin-tailwindcss": "^3.17.5", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", - "typescript": "5.4.5", + "typescript": "latest", "vite-tsconfig-paths": "^5.1.4" }, "packageManager": "pnpm@9.1.0", diff --git a/packages/extension/.env.example b/packages/extension/.env.example index aa0d82c8..ba37f4c6 100644 --- a/packages/extension/.env.example +++ b/packages/extension/.env.example @@ -2,7 +2,7 @@ # You only need to set these if the browsers are not in standard locations # For macOS, use paths like: -BRAVE_BINARY="/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser" +BRAVE_BINARY="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" CHROME_BINARY="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" FIREFOX_BINARY="/Applications/Firefox.app/Contents/MacOS/firefox-bin" diff --git a/packages/extension/package.json b/packages/extension/package.json index 1fb759b9..3f5eb7ca 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@react-scan/extension", - "version": "1.0.1", + "version": "1.0.0", "private": true, "type": "module", "scripts": { @@ -18,19 +18,23 @@ "pack:all": "rm -rf build && pnpm pack:chrome && pnpm pack:firefox && pnpm pack:brave" }, "dependencies": { + "@pivanov/utils": "^0.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-scan": "workspace:*", "zod": "^3.23.8" }, "devDependencies": { - "@pivanov/utils": "^0.0.1", "@types/chrome": "^0.0.281", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/webextension-polyfill": "^0.10.0", + "@vitejs/plugin-react": "^4.2.1", "bestzip": "^2.2.1", "cross-env": "^7.0.3", - "vite": "^5.4.3", - "vite-plugin-web-extension": "^4.3.1", + "vite": "^6.0.7", + "vite-plugin-web-extension": "^4.4.3", + "vite-tsconfig-paths": "^5.1.4", "webextension-polyfill": "^0.10.0" } } diff --git a/packages/extension/src/background/icon.ts b/packages/extension/src/background/icon.ts index 8eae76d0..cd3d410f 100644 --- a/packages/extension/src/background/icon.ts +++ b/packages/extension/src/background/icon.ts @@ -17,7 +17,7 @@ const ANIMATION_CONFIG = { stopFrame: 6, } as const; -let animationInterval: number | null = null; +let animationInterval: TTimer | null = null; const preRenderedFrames: Array { } }); -browser.runtime.onMessage.addListener((message, _sender, sendResponse) => { +browser.runtime.onMessage.addListener((message, _sender, _sendResponse) => { if (message.type === 'react-scan:is-focused') { debouncedUpdateIcon(message.data.state); } diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts index 5ae0a24f..988d5c24 100644 --- a/packages/extension/src/background/index.ts +++ b/packages/extension/src/background/index.ts @@ -26,7 +26,7 @@ const init = async (tab: browser.Tabs.Tab) => { }; // Listen for tab updates - only handle complete state -browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { +browser.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => { if (changeInfo.status === 'complete') { void init(tab); } diff --git a/packages/extension/src/content/index.ts b/packages/extension/src/content/index.ts index a13449b7..59cb0d89 100644 --- a/packages/extension/src/content/index.ts +++ b/packages/extension/src/content/index.ts @@ -44,14 +44,14 @@ chrome.runtime.onMessage.addListener(async (message: unknown, _sender, sendRespo return false; }); -window.addEventListener('DOMContentLoaded', (event) => { +window.addEventListener('DOMContentLoaded', () => { broadcast.onmessage = (type, data) => { if (type === 'react-scan:is-focused') { browser.runtime.sendMessage({ type: 'react-scan:is-focused', data: { - state: data.state - } + state: data.state, + }, }); } }; diff --git a/packages/extension/src/types/global.d.ts b/packages/extension/src/types/global.d.ts index 7a398e01..fa565bd6 100644 --- a/packages/extension/src/types/global.d.ts +++ b/packages/extension/src/types/global.d.ts @@ -1,19 +1,27 @@ -import * as reactScan from 'react-scan'; +import type * as reactScan from 'react-scan'; declare global { type BroadcastHandler = (type: BroadcastMessage['type'], data: Extract['data']) => void; interface Window { __REACT_DEVTOOLS_GLOBAL_HOOK__?: { - renderers: Map; + checkDCE: (fn: unknown) => void; supportsFiber: boolean; - checkDCE: () => void; - onCommitFiberRoot: (rendererID: number, root: unknown) => void; - onCommitFiberUnmount: () => void; - onScheduleFiberRoot: () => void; - inject: (renderer: unknown) => number; + supportsFlight: boolean; + renderers: Map; + hasUnsupportedRendererAttached: boolean; + onCommitFiberRoot: ( + rendererID: number, + root: FiberRoot, + // biome-ignore lint/suspicious/noConfusingVoidType: may or may not exist + priority: void | number, + ) => void; + onCommitFiberUnmount: (rendererID: number, fiber: Fiber) => void; + onPostCommitFiberRoot: (rendererID: number, root: FiberRoot) => void; + inject: (renderer: ReactRenderer) => number; + _instrumentationSource?: string; + _instrumentationIsActive?: boolean; }; - wrappedJSObject?: any; reactScan: typeof reactScan.setOptions; } @@ -27,5 +35,3 @@ declare global { var _reactScan: typeof reactScan; } - -export {}; diff --git a/packages/extension/src/utils/helpers.ts b/packages/extension/src/utils/helpers.ts index dc0467dc..7f0a3649 100644 --- a/packages/extension/src/utils/helpers.ts +++ b/packages/extension/src/utils/helpers.ts @@ -143,7 +143,7 @@ export const readLocalStorage = (storageKey: string): T | null => { } }; -export const saveLocalStorage = (storageKey: string, state: T): | void => { +export const saveLocalStorage = (storageKey: string, state: T): void => { if (typeof window === 'undefined') return; try { @@ -151,19 +151,47 @@ export const saveLocalStorage = (storageKey: string, state: T): | void => { } catch {} }; -export const debounce = any>( - func: T, - wait: number -): ((...args: Parameters) => void) => { - let timeout: number | null = null; +export const debounce = Promise>( + fn: T, + wait: number, + options: { leading?: boolean; trailing?: boolean } = {}, +) => { + let timeoutId: number | undefined; + let lastArg: boolean | null | undefined; + let isLeadingInvoked = false; + + const debounced = (enabled: boolean | null) => { + lastArg = enabled; + + if (options.leading && !isLeadingInvoked) { + isLeadingInvoked = true; + fn(enabled); + return; + } + + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + + if (options.trailing !== false) { + timeoutId = window.setTimeout(() => { + isLeadingInvoked = false; + timeoutId = undefined; + if (lastArg !== undefined) { + fn(lastArg); + } + }, wait); + } + }; - return (...args: Parameters): void => { - if (timeout !== null) { - clearTimeout(timeout); + debounced.cancel = () => { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + timeoutId = undefined; + isLeadingInvoked = false; + lastArg = undefined; } - timeout = setTimeout(() => { - func(...args); - timeout = null; - }, wait) as unknown as number; }; + + return debounced; }; diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json index 185ac29d..0377c6e2 100644 --- a/packages/extension/tsconfig.json +++ b/packages/extension/tsconfig.json @@ -22,12 +22,11 @@ ] }, "types": [ - "node", "chrome" - ], + ] }, "include": [ - "src", + "src" ], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts index cc199eed..33da1abd 100644 --- a/packages/extension/vite.config.ts +++ b/packages/extension/vite.config.ts @@ -1,4 +1,5 @@ -import { defineConfig, UserConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; +import { type UserConfig, defineConfig, loadEnv } from 'vite'; import webExtension, { readJsonFile } from 'vite-plugin-web-extension'; import tsconfigPaths from 'vite-tsconfig-paths'; @@ -9,32 +10,35 @@ const BROWSER_TYPES = { BRAVE: 'brave', } as const; +type BrowserType = (typeof BROWSER_TYPES)[keyof typeof BROWSER_TYPES]; + export default defineConfig(({ mode }): UserConfig => { const env = loadEnv(mode, process.cwd(), ''); - const browser = env.BROWSER || BROWSER_TYPES.CHROME; + const browser = (env.BROWSER || BROWSER_TYPES.CHROME) as BrowserType; const isBrave = browser === BROWSER_TYPES.BRAVE; // Validate Brave binary if (env.NODE_ENV === 'development' && isBrave && !env.BRAVE_BINARY) { + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error(` - ⚛️ React Scan - ============== - 🚫 Error: BRAVE_BINARY environment variable is missing + ⚛️ React Scan + ============== + 🚫 Error: BRAVE_BINARY environment variable is missing - This is required for Brave browser development. - Please check .env.example and set up your .env file with the correct path: + This is required for Brave browser development. + Please check .env.example and set up your .env file with the correct path: - 📍 For macOS: - BRAVE_BINARY="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" + 📍 For macOS: + BRAVE_BINARY="/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" - 📍 For Windows: - BRAVE_BINARY="C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" + 📍 For Windows: + BRAVE_BINARY="C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" - 📍 For Linux: - BRAVE_BINARY="/usr/bin/brave" - =============== -`); + 📍 For Linux: + BRAVE_BINARY="/usr/bin/brave" + =============== + `); process.exit(0); } @@ -44,7 +48,7 @@ export default defineConfig(({ mode }): UserConfig => { case BROWSER_TYPES.FIREFOX: return env.FIREFOX_BINARY; case BROWSER_TYPES.BRAVE: - return env.BRAVE_BINARY; + return env.BRAVE_BINARY || env.CHROME_BINARY; case BROWSER_TYPES.CHROME: return env.CHROME_BINARY; default: @@ -53,7 +57,7 @@ export default defineConfig(({ mode }): UserConfig => { }; // Generate manifest with package info - function generateManifest() { + const generateManifest = () => { const manifest = readJsonFile('src/manifest.json'); const pkg = readJsonFile('package.json'); @@ -63,7 +67,7 @@ export default defineConfig(({ mode }): UserConfig => { version: pkg.version, ...manifest, }; - } + }; // Vite configuration return { @@ -75,17 +79,19 @@ export default defineConfig(({ mode }): UserConfig => { minifyIdentifiers: false, }, plugins: [ + react(), tsconfigPaths(), webExtension({ manifest: generateManifest, // Use Chrome config for Brave - browser: isBrave ? BROWSER_TYPES.CHROME : browser, webExtConfig: { - browser: isBrave ? BROWSER_TYPES.CHROME : browser, - target: isBrave ? BROWSER_TYPES.CHROME : browser, + target: isBrave + ? 'chromium' + : browser === 'firefox' + ? 'firefox-desktop' + : 'chromium', chromiumBinary: getBrowserBinary(), firefoxBinary: env.FIREFOX_BINARY, - braveBinary: env.BRAVE_BINARY, startUrl: ['https://github.com/aidenybai/react-scan'], }, }), diff --git a/packages/scan/global.d.ts b/packages/scan/global.d.ts index 31f07ea5..032938f5 100644 --- a/packages/scan/global.d.ts +++ b/packages/scan/global.d.ts @@ -2,3 +2,8 @@ declare module '*.css' { const content: string; export default content; } + +declare module '*.astro' { + const Component: unknown; + export default Component; +} diff --git a/packages/scan/package.json b/packages/scan/package.json index d98277fd..d002b727 100644 --- a/packages/scan/package.json +++ b/packages/scan/package.json @@ -201,7 +201,6 @@ "dev:tsup": "NODE_ENV=development tsup --watch", "dev": "pnpm copy-astro && npm-run-all --parallel dev:css dev:tsup", "build:css": "npx tailwindcss -i ./src/web/assets/css/styles.tailwind.css -o ./src/web/assets/css/styles.css --minify", - "lint": "eslint 'src/**/*.{ts,tsx}' --fix", "pack": "npm version patch && pnpm build && npm pack", "pack:bump": "bun scripts/bump-version.js && nr pack && echo $(pwd)/react-scan-$(node -p \"require('./package.json').version\").tgz | pbcopy", "prettier": "prettier --config .prettierrc.mjs -w src", @@ -217,7 +216,7 @@ "@preact/signals": "^1.3.1", "@rollup/pluginutils": "^5.1.3", "@types/node": "^20.17.9", - "bippy": "^0.0.25", + "bippy": "^0.2.0", "esbuild": "^0.24.0", "estree-walker": "^3.0.3", "kleur": "^4.1.5", @@ -231,7 +230,6 @@ "@remix-run/react": "*", "@types/babel__core": "^7.20.5", "@types/react": "^18.0.0", - "@types/react-reconciler": "^0.28.8", "@types/react-router": "^5.1.0", "@vercel/style-guide": "^6.0.0", "clsx": "^2.1.1", @@ -242,7 +240,6 @@ "publint": "^0.2.12", "react": "*", "react-dom": "*", - "react-reconciler": "^0.29.2", "react-router": "^5.0.0", "react-router-dom": "^5.0.0 || ^6.0.0 || ^7.0.0", "tailwind-merge": "^2.5.5", diff --git a/packages/scan/scripts/bump-version.js b/packages/scan/scripts/bump-version.js index 1729f9d5..0c0f0e6f 100644 --- a/packages/scan/scripts/bump-version.js +++ b/packages/scan/scripts/bump-version.js @@ -1,6 +1,6 @@ -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; // Read the current version from scan package.json const scanPackagePath = path.join(__dirname, '../package.json'); @@ -8,14 +8,14 @@ const scanPackage = JSON.parse(fs.readFileSync(scanPackagePath, 'utf8')); // Bump patch version const version = scanPackage.version.split('.'); -version[2] = parseInt(version[2]) + 1; +version[2] = Number.parseInt(version[2]) + 1; const newVersion = version.join('.'); // Update the version in package.json scanPackage.version = newVersion; // Write back to package.json -fs.writeFileSync(scanPackagePath, JSON.stringify(scanPackage, null, 2) + '\n'); +fs.writeFileSync(scanPackagePath, `${JSON.stringify(scanPackage, null, 2)}\n`); // Get the tar file path const tarFileName = `react-scan-${newVersion}.tgz`; @@ -24,5 +24,7 @@ const tarFilePath = path.join(__dirname, '..', tarFileName); // Copy to clipboard execSync(`echo "${tarFilePath}" | pbcopy`); +// biome-ignore lint/suspicious/noConsole: Intended debug output console.log(`Bumped version to ${newVersion}`); -console.log(`Tar file path copied to clipboard: ${tarFilePath}`); \ No newline at end of file +// biome-ignore lint/suspicious/noConsole: Intended debug output +console.log(`Tar file path copied to clipboard: ${tarFilePath}`); diff --git a/packages/scan/src/auto.ts b/packages/scan/src/auto.ts index f9b439c1..b34a8304 100644 --- a/packages/scan/src/auto.ts +++ b/packages/scan/src/auto.ts @@ -1,9 +1,11 @@ -import 'bippy'; // implicit init RDT hook import { scan } from './index'; +import { init } from './install-hook'; // Initialize RDT hook + +init(); if (typeof window !== 'undefined') { scan({ dangerouslyForceRunInProduction: true }); window.reactScan = scan; } -export * from './index'; +export * from './core'; diff --git a/packages/scan/src/cli.mts b/packages/scan/src/cli.mts index 33134502..ec5619fc 100644 --- a/packages/scan/src/cli.mts +++ b/packages/scan/src/cli.mts @@ -1,30 +1,35 @@ import { spawn } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; -import mri from 'mri'; -import { intro, confirm, isCancel, cancel, spinner } from '@clack/prompts'; +import { cancel, confirm, intro, isCancel, spinner } from '@clack/prompts'; import { bgMagenta, dim, red } from 'kleur'; +import mri from 'mri'; import { + type Browser, + type BrowserContext, chromium, + devices, firefox, webkit, - devices, - type Browser, - type BrowserContext, } from 'playwright'; const truncateString = (str: string, maxLength: number) => { - str = str.replace('http://', '').replace('https://', '').replace('www.', ''); - if (str.endsWith('/')) { - str = str.slice(0, -1); + let result = str + .replace('http://', '') + .replace('https://', '') + .replace('www.', ''); + + if (result.endsWith('/')) { + result = result.slice(0, -1); } - if (str.length > maxLength) { + + if (result.length > maxLength) { const half = Math.floor(maxLength / 2); - const start = str.slice(0, half); - const end = str.slice(str.length - (maxLength - half)); + const start = result.slice(0, half); + const end = result.slice(result.length - (maxLength - half)); return `${start}…${end}`; } - return str; + return result; }; const inferValidURL = (maybeURL: string) => { @@ -74,9 +79,16 @@ const applyStealthScripts = async (context: BrowserContext) => { }); // Remove Playwright-specific properties - delete (window as any).__playwright; - delete (window as any).__pw_manual; - delete (window as any).__PW_inspect; + interface PlaywrightWindow extends Window { + __playwright?: unknown; + __pw_manual?: unknown; + __PW_inspect?: unknown; + } + + const win = window as PlaywrightWindow; + win.__playwright = undefined; + win.__pw_manual = undefined; + win.__PW_inspect = undefined; // Redefine the headless property Object.defineProperty(navigator, 'headless', { @@ -85,7 +97,7 @@ const applyStealthScripts = async (context: BrowserContext) => { // Override the permissions API const originalQuery = window.navigator.permissions.query; - window.navigator.permissions.query = (parameters: any) => + window.navigator.permissions.query = (parameters) => parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission, @@ -238,7 +250,7 @@ const init = async () => { const globalHook = globalThis.__REACT_SCAN__; if (!globalHook) return; let count = 0; - globalHook.ReactScanInternals.onRender = (fiber, renders) => { + globalHook.ReactScanInternals.onRender = (_fiber, renders) => { let localCount = 0; for (const render of renders) { localCount += render.count; @@ -248,7 +260,7 @@ const init = async () => { const reportData = globalHook.ReactScanInternals.Store.reportData; if (!Object.keys(reportData).length) return; - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.log('REACT_SCAN_REPORT', count); }); }; @@ -292,7 +304,7 @@ const init = async () => { interval = setInterval(() => { pollReport().catch(() => {}); }, 1000); - } catch (e) { + } catch { currentSpinner?.stop(red(`Error: ${truncatedURL}`)); } }; @@ -312,7 +324,7 @@ const init = async () => { } const reportDataString = text.replace('REACT_SCAN_REPORT', '').trim(); try { - count = parseInt(reportDataString, 10); + count = Number.parseInt(reportDataString, 10); } catch { return; } diff --git a/packages/scan/src/core/fast-serialize.test.ts b/packages/scan/src/core/fast-serialize.test.ts index c932e856..a7e6f346 100644 --- a/packages/scan/src/core/fast-serialize.test.ts +++ b/packages/scan/src/core/fast-serialize.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { fastSerialize } from '~core/instrumentation'; describe('fastSerialize', () => { @@ -18,7 +18,7 @@ describe('fastSerialize', () => { it('serializes numbers', () => { expect(fastSerialize(42)).toBe('42'); expect(fastSerialize(0)).toBe('0'); - expect(fastSerialize(NaN)).toBe('NaN'); + expect(fastSerialize(Number.NaN)).toBe('NaN'); }); it('serializes booleans', () => { @@ -27,9 +27,8 @@ describe('fastSerialize', () => { }); it('serializes functions', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const testFunc = (x: 2) => 3 - expect(fastSerialize(testFunc)).toBe('(x) => 3'); + const testFunc = (_x: 2) => 3; + expect(fastSerialize(testFunc)).toBe('(_x) => 3'); }); it('serializes arrays', () => { @@ -49,7 +48,6 @@ describe('fastSerialize', () => { }); it('serializes objects with custom constructors', () => { - // eslint-disable-next-line @typescript-eslint/no-extraneous-class class CustomClass {} const instance = new CustomClass(); expect(fastSerialize(instance)).toBe('CustomClass{…}'); diff --git a/packages/scan/src/core/index.ts b/packages/scan/src/core/index.ts index 23abb234..90f0e9bd 100644 --- a/packages/scan/src/core/index.ts +++ b/packages/scan/src/core/index.ts @@ -1,5 +1,6 @@ -import { signal, type Signal } from '@preact/signals'; +import { type Signal, signal } from '@preact/signals'; import { + type Fiber, detectReactBuildType, getDisplayName, getNearestHostFiber, @@ -10,26 +11,26 @@ import { isInstrumentationActive, traverseFiber, } from 'bippy'; -import type * as React from 'react'; -import type { Fiber } from 'react-reconciler'; +import type { ComponentType } from 'preact'; +import type { ReactNode } from 'preact/compat'; import { + type RenderData, aggregateChanges, aggregateRender, updateFiberRenderData, - type RenderData, } from 'src/core/utils'; import styles from '~web/assets/css/styles.css'; import { ICONS } from '~web/assets/svgs/svgs'; -import { type States } from '~web/components/inspector/utils'; +import type { States } from '~web/components/inspector/utils'; import { initReactScanOverlay } from '~web/overlay'; import { createToolbar } from '~web/toolbar'; import { playGeigerClickSound } from '~web/utils/geiger'; import { readLocalStorage, saveLocalStorage } from '~web/utils/helpers'; import { log, logIntro } from '~web/utils/log'; -import { flushOutlines, type Outline } from '~web/utils/outline'; -import { createInstrumentation, type Render } from './instrumentation'; +import { type Outline, flushOutlines } from '~web/utils/outline'; +import { type Render, createInstrumentation } from './instrumentation'; import type { InternalInteraction } from './monitor/types'; -import { type getSession } from './monitor/utils'; +import type { getSession } from './monitor/utils'; let rootContainer: HTMLDivElement | null = null; let shadowRoot: ShadowRoot | null = null; @@ -233,7 +234,7 @@ export type OutlineKey = `${string}-${string}`; export interface Internals { instrumentation: ReturnType | null; - componentAllowList: WeakMap, Options> | null; + componentAllowList: WeakMap, Options> | null; options: Signal; scheduledOutlines: Map; // we clear t,his nearly immediately, so no concern of mem leak on the fiber // outlines at the same coordinates always get merged together, so we pre-compute the merge ahead of time when aggregating in activeOutlines @@ -289,12 +290,18 @@ type LocalStorageOptions = Omit< | 'onPaintFinish' >; +function isOptionKey(key: string): key is keyof Options { + return key in ReactScanInternals.options.value; +} + const validateOptions = (options: Partial): Partial => { const errors: Array = []; const validOptions: Partial = {}; for (const key in options) { - const value = options[key as keyof Options]; + if (!isOptionKey(key)) continue; + + const value = options[key]; switch (key) { case 'enabled': case 'includeChildren': @@ -307,7 +314,7 @@ const validateOptions = (options: Partial): Partial => { if (typeof value !== 'boolean') { errors.push(`- ${key} must be a boolean. Got "${value}"`); } else { - (validOptions as any)[key] = value; + validOptions[key] = value; } break; case 'renderCountThreshold': @@ -315,7 +322,7 @@ const validateOptions = (options: Partial): Partial => { if (typeof value !== 'number' || value < 0) { errors.push(`- ${key} must be a non-negative number. Got "${value}"`); } else { - (validOptions as any)[key] = value; + validOptions[key] = value as number; } break; case 'animationSpeed': @@ -324,18 +331,39 @@ const validateOptions = (options: Partial): Partial => { `- Invalid animation speed "${value}". Using default "fast"`, ); } else { - (validOptions as any)[key] = value; + validOptions[key] = value as 'slow' | 'fast' | 'off'; } break; case 'onCommitStart': + if (typeof value !== 'function') { + errors.push(`- ${key} must be a function. Got "${value}"`); + } else { + validOptions.onCommitStart = value as () => void; + } + break; case 'onCommitFinish': + if (typeof value !== 'function') { + errors.push(`- ${key} must be a function. Got "${value}"`); + } else { + validOptions.onCommitFinish = value as () => void; + } + break; case 'onRender': + if (typeof value !== 'function') { + errors.push(`- ${key} must be a function. Got "${value}"`); + } else { + validOptions.onRender = value as ( + fiber: Fiber, + renders: Array, + ) => void; + } + break; case 'onPaintStart': case 'onPaintFinish': if (typeof value !== 'function') { errors.push(`- ${key} must be a function. Got "${value}"`); } else { - (validOptions as any)[key] = value; + validOptions[key] = value as (outlines: Array) => void; } break; case 'trackUnnecessaryRenders': { @@ -343,7 +371,6 @@ const validateOptions = (options: Partial): Partial => { typeof value === 'boolean' ? value : false; break; } - case 'smoothlyAnimateOutlines': { validOptions.smoothlyAnimateOutlines = typeof value === 'boolean' ? value : false; @@ -355,14 +382,14 @@ const validateOptions = (options: Partial): Partial => { } if (errors.length > 0) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.warn(`[React Scan] Invalid options:\n${errors.join('\n')}`); } return validOptions; }; -export const getReport = (type?: React.ComponentType) => { +export const getReport = (type?: ComponentType) => { if (type) { for (const reportData of Array.from(Store.legacyReportData.values())) { if (reportData.type === type) { @@ -426,8 +453,8 @@ export const reportRender = (fiber: Fiber, renders: Array) => { // More efficient null checks and Math.max const existingCount = Math.max( - (currentData && currentData.count) || 0, - (alternateData && alternateData.count) || 0, + currentData?.count ?? 0, + alternateData?.count ?? 0, ); // Create single shared object for both fibers @@ -436,7 +463,7 @@ export const reportRender = (fiber: Fiber, renders: Array) => { time: selfTime || 0, renders, displayName, - type: getType(fiber.type) || null, + type: (getType(fiber.type) as ComponentType | null) || null, }; // Store in both fibers @@ -465,7 +492,8 @@ export const reportRender = (fiber: Fiber, renders: Array) => { }; export const isValidFiber = (fiber: Fiber) => { - if (ignoredProps.has(fiber.memoizedProps)) { + const props = fiber.memoizedProps; + if (typeof props === 'object' && props !== null && ignoredProps.has(props)) { return false; } @@ -488,10 +516,12 @@ export const isValidFiber = (fiber: Fiber) => { return true; }; -let flushInterval: ReturnType; +let flushInterval: ReturnType | null = null; const startFlushOutlineInterval = () => { - clearInterval(flushInterval); - setInterval(() => { + if (flushInterval) { + clearInterval(flushInterval); + } + flushInterval = setInterval(() => { requestAnimationFrame(() => { flushOutlines(); }); @@ -510,8 +540,10 @@ const updateScheduledOutlines = (fiber: Fiber, renders: Array) => { continue; if (ReactScanInternals.scheduledOutlines.has(fiber)) { - const existingOutline = ReactScanInternals.scheduledOutlines.get(fiber)!; - aggregateRender(render, existingOutline.aggregatedRender); + const existingOutline = ReactScanInternals.scheduledOutlines.get(fiber); + if (existingOutline) { + aggregateRender(render, existingOutline.aggregatedRender); + } } else { ReactScanInternals.scheduledOutlines.set(fiber, { domNode: domFiber.stateNode, @@ -626,7 +658,7 @@ export const start = () => { ReactScanInternals.options.value.onCommitStart?.(); }, onError(error) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error('[React Scan] Error instrumenting:', error); }, isValidFiber, @@ -693,7 +725,7 @@ export const start = () => { if (!Store.monitor.value && !isUsedInBrowserExtension) { setTimeout(() => { if (isInstrumentationActive()) return; - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error( '[React Scan] Failed to load. Must import React Scan before React runs.', ); @@ -701,8 +733,8 @@ export const start = () => { } }; -export const withScan = ( - component: React.ComponentType, +export const withScan = ( + component: ComponentType, options: Options = {}, ) => { setOptions(options); @@ -712,12 +744,12 @@ export const withScan = ( return component; if (!componentAllowList) { ReactScanInternals.componentAllowList = new WeakMap< - React.ComponentType, + ComponentType, Options >(); } if (componentAllowList) { - componentAllowList.set(component, { ...options }); + componentAllowList.set(component as ComponentType, { ...options }); } start(); @@ -752,15 +784,13 @@ export const onRender = ( }; }; + export const ignoredProps = new WeakSet< - Exclude< - React.ReactNode, - undefined | null | string | number | boolean | bigint - > + Exclude >(); -export const ignoreScan = (node: React.ReactNode) => { - if (typeof node === 'object' && node) { +export const ignoreScan = (node: ReactNode) => { + if (typeof node === 'object' && node !== null) { ignoredProps.add(node); } }; diff --git a/packages/scan/src/core/instrumentation.ts b/packages/scan/src/core/instrumentation.ts index 7a5f2170..e1837496 100644 --- a/packages/scan/src/core/instrumentation.ts +++ b/packages/scan/src/core/instrumentation.ts @@ -1,5 +1,7 @@ -import { signal, type Signal } from '@preact/signals'; +import { type Signal, signal } from '@preact/signals'; import { + type Fiber, + type FiberRoot, createFiberVisitor, didFiberCommit, getDisplayName, @@ -13,7 +15,6 @@ import { traverseState, } from 'bippy'; import { isValidElement } from 'preact'; -import type { Fiber, FiberRoot } from 'react-reconciler'; import { isEqual } from '~core/utils'; import { getChangedPropsDetailed } from '~web/components/inspector/utils'; import { @@ -53,7 +54,7 @@ export const isElementVisible = (el: Element) => { return ( style.display !== 'none' && style.visibility !== 'hidden' && - (style as any).contentVisibility !== 'hidden' && + style.getPropertyValue('content-visibility') !== 'hidden' && style.opacity !== '0' ); }; @@ -81,7 +82,7 @@ export const isElementInViewport = ( return isVisible && rect.width && rect.height; }; -export const enum ChangeReason { +export enum ChangeReason { Props = 0b001, State = 0b010, Context = 0b100, @@ -90,7 +91,7 @@ export const enum ChangeReason { export interface RenderChange { type: ChangeReason; name: string; - value: any; + value: unknown; prevValue?: unknown; nextValue?: unknown; unstable?: boolean; @@ -139,7 +140,10 @@ export function fastSerialize(value: unknown, depth = 0): string { if (value === null) return 'null'; if (cache.has(value)) { - return cache.get(value)!; + const cached = cache.get(value); + if (cached !== undefined) { + return cached; + } } if (Array.isArray(value)) { @@ -163,7 +167,7 @@ export function fastSerialize(value: unknown, depth = 0): string { return str; } - const ctor = (value as any).constructor; + const ctor = value && typeof value === 'object' ? value.constructor : undefined; if (ctor && typeof ctor === 'function' && ctor.name) { const str = `${ctor.name}{…}`; cache.set(value, str); @@ -219,9 +223,10 @@ interface StateFiber { function getStateChangesTraversal( this: Array, - prevState: StateFiber, - nextState: StateFiber, + prevState: StateFiber | null | undefined, + nextState: StateFiber | null | undefined, ): void { + if (!prevState || !nextState) return; if (isEqual(prevState.memoizedState, nextState.memoizedState)) return; const change: RenderChange = { type: ChangeReason.State, @@ -247,26 +252,27 @@ interface ContextFiber { function getContextChangesTraversal( this: Array, - prevContext: ContextFiber, - nextContext: ContextFiber, + nextValue: ContextFiber | null | undefined, + prevValue: ContextFiber | null | undefined, ): void { - const prevValue = prevContext.memoizedValue; - const nextValue = nextContext.memoizedValue; + if (!nextValue || !prevValue) return; + const prevMemoizedValue = prevValue.memoizedValue; + const nextMemoizedValue = nextValue.memoizedValue; const change: RenderChange = { type: ChangeReason.Context, name: '', - value: nextValue, + value: nextMemoizedValue, unstable: false, }; this.push(change); - const prevValueString = fastSerialize(prevValue); - const nextValueString = fastSerialize(nextValue); + const prevValueString = fastSerialize(prevMemoizedValue); + const nextValueString = fastSerialize(nextMemoizedValue); if ( - unstableTypes.includes(typeof prevValue) && - unstableTypes.includes(typeof nextValue) && + unstableTypes.includes(typeof prevMemoizedValue) && + unstableTypes.includes(typeof nextMemoizedValue) && prevValueString === nextValueString ) { change.unstable = true; diff --git a/packages/scan/src/core/monitor/constants.ts b/packages/scan/src/core/monitor/constants.ts index b15d71de..8d61db1c 100644 --- a/packages/scan/src/core/monitor/constants.ts +++ b/packages/scan/src/core/monitor/constants.ts @@ -8,9 +8,6 @@ * @see https://github.com/localvoid/ivi/blob/bd5bbe8c6b39a7be1051c16ea0a07b3df9a178bd/packages/ivi/src/client/core.ts#L13 */ -/* eslint-disable prefer-const */ -/* eslint-disable import/no-mutable-exports */ - /** * Do not destructure exports or import React from "react" here. * From empirical ad-hoc testing, this breaks in certain scenarios. @@ -22,15 +19,19 @@ import * as React from 'react'; * * @see https://nextjs.org/docs/messages/react-client-hook-in-server-component */ -export let isRSC = !React.useRef; -export let isSSR = typeof window === 'undefined' || isRSC; +export const isRSC = () => !React.useRef; +export const isSSR = () => typeof window === 'undefined' || isRSC(); + +interface WindowWithCypress extends Window { + Cypress?: unknown; +} -export let isTest = +export const isTest = (typeof window !== 'undefined' && /** * @see https://docs.cypress.io/faq/questions/using-cypress-faq#Is-there-any-way-to-detect-if-my-app-is-running-under-Cypress */ - ((window as any).Cypress || + ((window as WindowWithCypress).Cypress || /** * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/webdriver */ @@ -41,16 +42,16 @@ export let isTest = // @ts-expect-error jest is a global in test typeof jest !== 'undefined'; -export let VERSION = null!; // todo -export let PAYLOAD_VERSION = null!; // todo +export const VERSION = null; // todo +export const PAYLOAD_VERSION = null; // todo -export let MAX_QUEUE_SIZE = 300; -export let FLUSH_TIMEOUT = isTest +export const MAX_QUEUE_SIZE = 300; +export const FLUSH_TIMEOUT = isTest ? 100 // Make sure there is no data loss in tests : process.env.NODE_ENV === 'production' ? 5000 : 1000; -export let SESSION_EXPIRE_TIMEOUT = 300000; // 5 minutes -export let GZIP_MIN_LEN = 1000; -export let GZIP_MAX_LEN = 60000; // 1 minute -export let MAX_PENDING_REQUESTS = 15; +export const SESSION_EXPIRE_TIMEOUT = 300000; // 5 minutes +export const GZIP_MIN_LEN = 1000; +export const GZIP_MAX_LEN = 60000; // 1 minute +export const MAX_PENDING_REQUESTS = 15; diff --git a/packages/scan/src/core/monitor/index.ts b/packages/scan/src/core/monitor/index.ts index 1a42968d..3c0cabe4 100644 --- a/packages/scan/src/core/monitor/index.ts +++ b/packages/scan/src/core/monitor/index.ts @@ -1,19 +1,23 @@ 'use client'; -import { getDisplayName, getTimings, isCompositeFiber } from 'bippy'; -import { type Fiber } from 'react-reconciler'; +import { + type Fiber, + getDisplayName, + getTimings, + isCompositeFiber, +} from 'bippy'; import { useEffect } from 'react'; import { type MonitoringOptions, ReactScanInternals, - setOptions, Store, + setOptions, } from '..'; -import { createInstrumentation, type Render } from '../instrumentation'; +import { type Render, createInstrumentation } from '../instrumentation'; import { updateFiberRenderData } from '../utils'; -import { initPerformanceMonitoring } from './performance'; -import { getSession } from './utils'; import { flush } from './network'; import { computeRoute } from './params/utils'; +import { initPerformanceMonitoring } from './performance'; +import { getSession } from './utils'; // max retries before the set of components do not get reported (avoid memory leaks of the set of fibers stored on the component aggregation) const MAX_RETRIES_BEFORE_COMPONENT_GC = 7; diff --git a/packages/scan/src/core/monitor/network.ts b/packages/scan/src/core/monitor/network.ts index 25e029fd..bea744a7 100644 --- a/packages/scan/src/core/monitor/network.ts +++ b/packages/scan/src/core/monitor/network.ts @@ -1,12 +1,12 @@ -import { Store } from '../..'; -import { GZIP_MIN_LEN, GZIP_MAX_LEN, MAX_PENDING_REQUESTS } from './constants'; -import { getSession } from './utils'; +import { Store } from '..'; +import { GZIP_MAX_LEN, GZIP_MIN_LEN, MAX_PENDING_REQUESTS } from './constants'; import type { - Interaction, + Component, IngestRequest, + Interaction, InternalInteraction, - Component, } from './types'; +import { getSession } from './utils'; const INTERACTION_TIME_TILL_COMPLETED = 4000; @@ -35,7 +35,7 @@ export const flush = async (): Promise => { const timeSinceStart = now - interaction.performanceEntry.startTime; // these interactions were retried enough and should be discarded to avoid mem leak if (timeSinceStart > 30000) { - continue; + // Skip this iteration } else if (timeSinceStart <= INTERACTION_TIME_TILL_COMPLETED) { pendingInteractions.push(interaction); } else { @@ -43,8 +43,9 @@ export const flush = async (): Promise => { } } - // nothing to flush - if (!completedInteractions.length) return; + if (!completedInteractions.length) + // nothing to flush + return; // idempotent const session = await getSession({ @@ -157,6 +158,7 @@ export const flush = async (): Promise => { ...session, url: window.location.toString(), route: monitor.route, // this might be inaccurate but used to caculate which paths all the unique sessions are coming from without having to join on the interactions table (expensive) + wifi: session.wifi ?? '', }, }; @@ -197,8 +199,14 @@ export const compress = async (payload: string): Promise => { * * @see https://gist.github.com/aidenybai/473689493f2d5d01bbc52e2da5950b45#file-palette-dev-browser-dist-palette-dev-mjs-L365 */ +interface RequestHeaders { + 'Content-Type': string; + 'Content-Encoding'?: string; + 'x-api-key'?: string; +} + export const transport = async ( - url: string, + initialUrl: string, payload: IngestRequest, ): Promise<{ ok: boolean }> => { const fail = { ok: false }; @@ -210,11 +218,12 @@ export const transport = async ( shouldCompress && supportsCompression ? await compress(json) : json; if (!navigator.onLine) return fail; - const headers: any = { + const headerValues: RequestHeaders = { 'Content-Type': CONTENT_TYPE, 'Content-Encoding': shouldCompress ? 'gzip' : undefined, - 'x-api-key': Store.monitor.value?.apiKey, + 'x-api-key': Store.monitor.value?.apiKey ?? undefined, }; + let url = initialUrl; if (shouldCompress) url += '?z=1'; const size = typeof body === 'string' ? body.length : body.byteLength; @@ -245,6 +254,6 @@ export const transport = async ( MAX_PENDING_REQUESTS > (Store.monitor.value?.pendingRequests ?? 0), priority: 'low', // mode: 'no-cors', - headers, + headers: headerValues as unknown as HeadersInit, }); }; diff --git a/packages/scan/src/core/monitor/params/utils.ts b/packages/scan/src/core/monitor/params/utils.ts index b3c5f4e5..399624da 100644 --- a/packages/scan/src/core/monitor/params/utils.ts +++ b/packages/scan/src/core/monitor/params/utils.ts @@ -35,7 +35,7 @@ function computeRouteWithFormatter( } } return result; - } catch (e) { + } catch { return pathname; } } diff --git a/packages/scan/src/core/monitor/performance.ts b/packages/scan/src/core/monitor/performance.ts index 75bd1b19..bcead927 100644 --- a/packages/scan/src/core/monitor/performance.ts +++ b/packages/scan/src/core/monitor/performance.ts @@ -1,7 +1,6 @@ -import { getDisplayName } from 'bippy'; -import { type Fiber } from 'react-reconciler'; +import { type Fiber, getDisplayName } from 'bippy'; import { getCompositeComponentFromElement } from '~web/components/inspector/utils'; -import { Store } from '../..'; +import { Store } from '..'; import type { PerformanceInteraction, PerformanceInteractionEntry, @@ -90,15 +89,16 @@ const isMinified = (name: string): boolean => { }; export const getInteractionPath = ( - fiber: Fiber | null, + initialFiber: Fiber | null, filters: PathFilters = DEFAULT_FILTERS, ): Array => { - if (!fiber) return []; + if (!initialFiber) return []; - const currentName = getDisplayName(fiber.type); + const currentName = getDisplayName(initialFiber.type); if (!currentName) return []; const stack = new Array(); + let fiber = initialFiber; while (fiber.return) { const name = getCleanComponentName(fiber.type); if (name && !isMinified(name) && shouldIncludeInPath(name, filters)) { @@ -115,9 +115,13 @@ export const getInteractionPath = ( let currentMouseOver: Element; -const getCleanComponentName = ( - component: any /** fiber.type is any */, -): string => { +interface FiberType { + displayName?: string; + name?: string; + [key: string]: unknown; +} + +const getCleanComponentName = (component: FiberType): string => { const name = getDisplayName(component); if (!name) return ''; @@ -128,11 +132,11 @@ const getCleanComponentName = ( }; // For future use, normalization of paths happens on server side now using path property of interaction -const _normalizePath = (path: Array): string => { - const cleaned = path.filter(Boolean); - const deduped = cleaned.filter((name, i) => name !== cleaned[i - 1]); - return deduped.join('.'); -}; +// const _normalizePath = (path: Array): string => { +// const cleaned = path.filter(Boolean); +// const deduped = cleaned.filter((name, i) => name !== cleaned[i - 1]); +// return deduped.join('.'); +// }; const handleMouseover = (event: Event) => { if (!(event.target instanceof Element)) return; @@ -151,7 +155,7 @@ const getFirstNamedAncestorCompositeFiber = (element: Element) => { if (!fiber) { continue; } - if (fiber.type && getDisplayName(fiber.type)) { + if (getDisplayName(fiber?.type)) { parentCompositeFiber = fiber; } } diff --git a/packages/scan/src/core/monitor/types.ts b/packages/scan/src/core/monitor/types.ts index 95beb7e4..9ae875f2 100644 --- a/packages/scan/src/core/monitor/types.ts +++ b/packages/scan/src/core/monitor/types.ts @@ -1,4 +1,4 @@ -import { type Fiber } from 'react-reconciler'; +import type { Fiber } from 'bippy'; export enum Device { DESKTOP = 0, diff --git a/packages/scan/src/core/monitor/utils.ts b/packages/scan/src/core/monitor/utils.ts index 749345e0..a42ae4c8 100644 --- a/packages/scan/src/core/monitor/utils.ts +++ b/packages/scan/src/core/monitor/utils.ts @@ -2,6 +2,12 @@ import { onIdle } from '~web/utils/helpers'; import { isSSR } from './constants'; import { Device, type Session } from './types'; +interface NetworkInformation { + connection?: { + effectiveType?: string; + }; +} + const getDeviceType = () => { const userAgent = navigator.userAgent; @@ -11,7 +17,8 @@ const getDeviceType = () => { ) ) { return Device.MOBILE; - } else if (/iPad|Tablet/i.test(userAgent)) { + } + if (/iPad|Tablet/i.test(userAgent)) { return Device.TABLET; } return Device.DESKTOP; @@ -20,7 +27,7 @@ const getDeviceType = () => { /** * Measure layout time */ -export const doubleRAF = (callback: (...args: Array) => void) => { +export const doubleRAF = (callback: (...args: unknown[]) => void) => { return requestAnimationFrame(() => { requestAnimationFrame(callback); }); @@ -41,10 +48,10 @@ export const generateId = () => { * @see https://deviceandbrowserinfo.com/learning_zone/articles/webgl_renderer_values */ const getGpuRenderer = () => { - // Prevent WEBGL_debug_renderer_info deprecation warnings in firefox - if (!('chrome' in window)) return ''; + if (!('chrome' in window)) return ''; // Prevent WEBGL_debug_renderer_info deprecation warnings in firefox const gl = document .createElement('canvas') + // Get the specs for the fastest GPU available. This helps provide a better // picture of the device's capabilities. .getContext('webgl', { powerPreference: 'high-performance' }); @@ -68,7 +75,7 @@ export const getSession = async ({ commit?: string | null; branch?: string | null; }) => { - if (isSSR) return null; + if (isSSR()) return null; if (cachedSession) { return cachedSession; } @@ -81,8 +88,8 @@ export const getSession = async ({ * * @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType */ - const connection = (navigator as any).connection; - const wifi = (connection && connection.effectiveType) || null; + const connection = (navigator as NetworkInformation).connection; + const wifi = connection?.effectiveType ?? null; /** * Number of CPU threads * @@ -110,7 +117,7 @@ export const getSession = async ({ url, route: null, device: getDeviceType(), - wifi, + wifi: wifi ?? '', cpu, mem, gpu: await gpuRendererPromise, diff --git a/packages/scan/src/core/utils.ts b/packages/scan/src/core/utils.ts index 7f1ceb3d..8bd32137 100644 --- a/packages/scan/src/core/utils.ts +++ b/packages/scan/src/core/utils.ts @@ -1,7 +1,7 @@ -import { getType } from 'bippy'; -import { type Fiber } from 'react-reconciler'; +import { type Fiber, getType } from 'bippy'; +import type { ComponentType } from 'preact'; import { ReactScanInternals } from '~core/index'; -import { type AggregatedRender } from '~web/utils/outline'; +import type { AggregatedRender } from '~web/utils/outline'; import type { AggregatedChange, Render, RenderChange } from './instrumentation'; export const aggregateChanges = ( @@ -75,7 +75,7 @@ function getComponentGroupNames(group: ComponentData[]): string { const max = Math.min(4, len); for (let i = 1; i < max; i++) { - result += ', ' + group[i].name; + result += `, ${group[i].name}`; } return result; @@ -105,30 +105,30 @@ export const getLabelText = ( ) => { let labelText = ''; - // TODO(Alexis): perhaps simplify this block up to the sorted line - const componentsByCount = new Map>(); + const componentsByCount = new Map< + number, + Array<{ name: string; forget: boolean; time: number }> + >(); - for (const { - forget, - time, - aggregatedCount, - name, - } of groupedAggregatedRenders) { + for (const aggregatedRender of groupedAggregatedRenders) { + const { forget, time, aggregatedCount, name } = aggregatedRender; if (!componentsByCount.has(aggregatedCount)) { componentsByCount.set(aggregatedCount, []); } - componentsByCount - .get(aggregatedCount)! - .push({ name, forget, time: time ?? 0 }); + const components = componentsByCount.get(aggregatedCount); + if (components) { + components.push({ name, forget, time: time ?? 0 }); + } } const sortedCounts = Array.from(componentsByCount.keys()).sort(descending); const parts: Array = []; let cumulativeTime = 0; - for (const count of sortedCounts) { - const componentGroup = componentsByCount.get(count)!; + const componentGroup = componentsByCount.get(count); + if (!componentGroup) continue; + let text = getComponentGroupNames(componentGroup); const totalTime = getComponentGroupTotalTime(componentGroup); const hasForget = componentGroupHasForget(componentGroup); @@ -140,11 +140,11 @@ export const getLabelText = ( } if (count > 1) { - text += ' ×' + count; + text += ` × ${count}`; } if (hasForget) { - text = '✨' + text; + text = `✨${text}`; } parts.push(text); @@ -155,11 +155,11 @@ export const getLabelText = ( if (!labelText.length) return null; if (labelText.length > 40) { - labelText = labelText.slice(0, 40) + '…'; + labelText = `${labelText.slice(0, 40)}…`; } if (cumulativeTime >= 0.01) { - labelText += ' (' + cumulativeTime.toFixed(2) + 'ms)'; + labelText += ` (${Number(cumulativeTime.toFixed(2))}ms)`; } return labelText; @@ -187,11 +187,10 @@ export interface RenderData { time: number; renders: Array; displayName: string | null; - type: React.ComponentType | null; + type: ComponentType | null; changes?: Array; } export function isEqual(a: unknown, b: unknown): boolean { - // eslint-disable-next-line no-self-compare - return a === b || (a !== a && b !== b); + return a === b || (Number.isNaN(a) && Number.isNaN(b)); } diff --git a/packages/scan/src/index.ts b/packages/scan/src/index.ts index 44916ab8..f69723ba 100644 --- a/packages/scan/src/index.ts +++ b/packages/scan/src/index.ts @@ -1,3 +1,5 @@ -import 'bippy'; // implicit init RDT hook +import { init } from './install-hook'; // Initialize RDT hook + +init(); export * from './core/index'; diff --git a/packages/scan/src/install-hook.ts b/packages/scan/src/install-hook.ts index 9a9c8113..386ab55a 100644 --- a/packages/scan/src/install-hook.ts +++ b/packages/scan/src/install-hook.ts @@ -1,3 +1,10 @@ -import 'bippy'; +import { installRDTHook } from 'bippy'; -export {}; +// Initialize React DevTools hook +const init = () => { + installRDTHook(); +}; + +init(); + +export { init }; diff --git a/packages/scan/src/react-component-name/__tests__/utils.ts b/packages/scan/src/react-component-name/__tests__/utils.ts index 9e560e77..48eefa8a 100644 --- a/packages/scan/src/react-component-name/__tests__/utils.ts +++ b/packages/scan/src/react-component-name/__tests__/utils.ts @@ -1,14 +1,18 @@ import { reactComponentNamePlugin } from ".."; +type TransformFn = ( + code: string, + id: string, +) => Promise<{ code: string } | string | null>; + export const transform = async (code: string) => { - const plugin = reactComponentNamePlugin.vite({}) as any; - const transformFn: (...params: Array) => any = plugin.transform; + const plugin = reactComponentNamePlugin.vite({}) as { transform: TransformFn }; + const transformFn = plugin.transform; if (!transformFn) return code; const result = await transformFn.call( { getCombinedSourcemap: () => null, - // eslint-disable-next-line no-console error: console.error, }, code, diff --git a/packages/scan/src/react-component-name/astro.ts b/packages/scan/src/react-component-name/astro.ts index 8eef8c0f..7117e7ed 100644 --- a/packages/scan/src/react-component-name/astro.ts +++ b/packages/scan/src/react-component-name/astro.ts @@ -1,9 +1,10 @@ +import type { Options } from '.'; import vite from './vite'; -import { type Options } from '.'; export default (options: Options = {}) => ({ name: 'react-component-name', hooks: { + // biome-ignore lint/suspicious/noExplicitAny: should be { config: AstroConfig } 'astro:config:setup': (astro: any) => { astro.config.vite.plugins ||= []; astro.config.vite.plugins.push(vite(options)); diff --git a/packages/scan/src/react-component-name/index.ts b/packages/scan/src/react-component-name/index.ts index d2683b41..0e0b0cdd 100644 --- a/packages/scan/src/react-component-name/index.ts +++ b/packages/scan/src/react-component-name/index.ts @@ -1,9 +1,8 @@ - -import { createFilter } from '@rollup/pluginutils'; -import { createUnplugin } from 'unplugin'; import { transformAsync } from '@babel/core'; -import type { PluginObj } from '@babel/core'; +import type { NodePath, PluginObj } from '@babel/core'; import * as t from '@babel/types'; +import { createFilter } from '@rollup/pluginutils'; +import { createUnplugin } from 'unplugin'; export interface Options { include?: Array; @@ -20,7 +19,7 @@ const createBabelPlugin = (): PluginObj => { ); } - function isReactComponent(path: any): boolean { + function isReactComponent(path: NodePath | { node: t.Node }): boolean { if (!path?.node) return false; // Arrow functions and function declarations @@ -84,7 +83,7 @@ const createBabelPlugin = (): PluginObj => { if (t.isCallExpression(callee)) { return path.node.arguments.some( (arg: t.Node) => - (t.isIdentifier(arg) && (/^[A-Z]/.exec(arg.name))) ?? + (t.isIdentifier(arg) && /^[A-Z]/.exec(arg.name)) ?? isReactComponent({ node: arg }), ); } @@ -129,7 +128,7 @@ const createBabelPlugin = (): PluginObj => { path.traverse({ 'ClassDeclaration|FunctionDeclaration|VariableDeclarator'(path) { let componentName: string | undefined; - let componentPath: any; + let componentPath: NodePath | { node: t.Node } | null = null; if (t.isClassDeclaration(path.node) && path.node.id?.name) { componentName = path.node.id.name; @@ -145,13 +144,15 @@ const createBabelPlugin = (): PluginObj => { t.isIdentifier(path.node.id) ) { componentName = path.node.id.name; - componentPath = path.get('init'); + const init = path.get('init'); + componentPath = Array.isArray(init) ? init[0] : init; } if ( componentName && isComponentName(componentName) && !hasDisplayNameAssignment.has(componentName) && + componentPath && isReactComponent(componentPath) ) { const displayNameAssignment = t.tryStatement( @@ -223,7 +224,7 @@ export const reactComponentNamePlugin = createUnplugin( return result ? { code: result.code ?? '', map: result.map } : null; } catch (error) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error('Error processing file:', id, error); return null; } diff --git a/packages/scan/src/types.d.ts b/packages/scan/src/types.d.ts deleted file mode 100644 index f07779b3..00000000 --- a/packages/scan/src/types.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable no-var */ -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -type ReactScanInternals = (typeof import('./core/index'))['ReactScanInternals']; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports -type Scan = (typeof import('./index'))['scan']; - -interface ReactRenderer { - findFiberByHostInstance: (instance: Element) => Fiber | null; - version: string; - bundleType: number; - rendererPackageName: string; - overrideHookState?: (fiber: Fiber, id: string, path: Array, value: any) => void; - overrideProps?: (fiber: Fiber, path: Array, value: any) => void; - scheduleUpdate?: (fiber: Fiber) => void; -} - -interface DevToolsHook { - renderers: Map; -} - -declare global { - var __REACT_DEVTOOLS_GLOBAL_HOOK__: DevToolsHook; - var __REACT_SCAN__: { - ReactScanInternals: ReactScanInternals; - }; - var reactScan: Scan; - var scheduler: { - postTask: (cb: any, options: { priority: string }) => void; - }; - - type TTimer = NodeJS.Timeout; - - interface Window { - __REACT_DEVTOOLS_GLOBAL_HOOK__: DevToolsHook; - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - interface globalThis { - __REACT_DEVTOOLS_GLOBAL_HOOK__: DevToolsHook; - __REACT_SCAN__: { - ReactScanInternals: ReactScanInternals; - }; - reactScan: Scan; - scheduler: { - postTask: (cb: any, options: { priority: string }) => void; - }; - } -} - - -declare module '*.css' { - const content: string; - export default content; -} - -interface StoreType { - wasDetailsOpen?: boolean; -} - - -export {}; diff --git a/packages/scan/src/types.ts b/packages/scan/src/types.ts new file mode 100644 index 00000000..bccc2bb3 --- /dev/null +++ b/packages/scan/src/types.ts @@ -0,0 +1,50 @@ +import type { Fiber, FiberRoot, ReactRenderer } from 'bippy'; + +type ReactScanInternals = typeof import('./core/index')['ReactScanInternals']; +type Scan = typeof import('./index')['scan']; + +export interface ExtendedReactRenderer extends ReactRenderer { + overrideHookState?: ( + fiber: Fiber, + id: string, + path: Array, + value: unknown, + ) => void; + overrideProps?: (fiber: Fiber, path: Array, value: unknown) => void; +} + +declare global { + var __REACT_SCAN__: { + ReactScanInternals: ReactScanInternals; + }; + var reactScan: Scan; + var scheduler: { + postTask: (cb: unknown, options: { priority: string }) => void; + }; + + type TTimer = NodeJS.Timeout; + + interface Window { + isReactScanExtension?: boolean; + reactScan: Scan; + + __REACT_DEVTOOLS_GLOBAL_HOOK__?: { + checkDCE: (fn: unknown) => void; + supportsFiber: boolean; + supportsFlight: boolean; + renderers: Map; + hasUnsupportedRendererAttached: boolean; + onCommitFiberRoot: ( + rendererID: number, + root: FiberRoot, + // biome-ignore lint/suspicious/noConfusingVoidType: may or may not exist + priority: void | number, + ) => void; + onCommitFiberUnmount: (rendererID: number, fiber: Fiber) => void; + onPostCommitFiberRoot: (rendererID: number, root: FiberRoot) => void; + inject: (renderer: ExtendedReactRenderer) => number; + _instrumentationSource?: string; + _instrumentationIsActive?: boolean; + }; + } +} diff --git a/packages/scan/src/web/assets/css/styles.tailwind.css b/packages/scan/src/web/assets/css/styles.tailwind.css index 6d6bf4f3..b27568ae 100644 --- a/packages/scan/src/web/assets/css/styles.tailwind.css +++ b/packages/scan/src/web/assets/css/styles.tailwind.css @@ -91,16 +91,16 @@ svg { @apply z-[2147483678]; @apply animate-fade-in animation-duration-300 animation-delay-300; @apply shadow-[0_4px_12px_rgba(0,0,0,0.2)]; +} - button { - &:hover { - background: rgba(255, 255, 255, 0.1); - } +.button { + &:hover { + background: rgba(255, 255, 255, 0.1); + } - &:active { - background: rgba(255, 255, 255, 0.15); - } - } + &:active { + background: rgba(255, 255, 255, 0.15); + } } .resize-line-wrapper { diff --git a/packages/scan/src/web/components/icon/index.tsx b/packages/scan/src/web/components/icon/index.tsx index 0476444b..39e1cc8c 100644 --- a/packages/scan/src/web/components/icon/index.tsx +++ b/packages/scan/src/web/components/icon/index.tsx @@ -1,5 +1,5 @@ import type { JSX } from 'preact'; -import { forwardRef, type ForwardedRef } from 'preact/compat'; +import { type ForwardedRef, forwardRef } from 'preact/compat'; export interface SVGIconProps { size?: number | Array; @@ -11,42 +11,38 @@ export interface SVGIconProps { style?: JSX.CSSProperties; } -export const Icon = forwardRef( - ( - { - size = 15, - name, - fill = 'currentColor', - stroke = 'currentColor', - className, - externalURL = '', - style, - }: SVGIconProps, - ref: ForwardedRef, - ) => { - const width = Array.isArray(size) ? size[0] : size; - const height = Array.isArray(size) ? size[1] || size[0] : size; +export const Icon = forwardRef(({ + size = 15, + name, + fill = 'currentColor', + stroke = 'currentColor', + className, + externalURL = '', + style, +}: SVGIconProps, ref: ForwardedRef) => { + const width = Array.isArray(size) ? size[0] : size; + const height = Array.isArray(size) ? size[1] || size[0] : size; - const path = `${externalURL}#${name}`; + const path = `${externalURL}#${name}`; - return ( - - - - ); - }, -); + return ( + + {name} + + + ); +}); diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index 0dcd5397..ccd89907 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -1,5 +1,5 @@ import { signal } from '@preact/signals'; -import { getDisplayName } from 'bippy'; +import { type Fiber, getDisplayName } from 'bippy'; import { Component } from 'preact'; import { useCallback, @@ -8,7 +8,6 @@ import { useRef, useState, } from 'preact/hooks'; -import type { Fiber } from 'react-reconciler'; import { Store } from '~core/index'; import { isEqual } from '~core/utils'; import { CopyToClipboard } from '~web/components/copy-to-clipboard'; @@ -197,22 +196,21 @@ const isEditableValue = (value: unknown, parentPath?: string): boolean => { } } - switch (typeof value) { - case 'string': - case 'number': - case 'boolean': - case 'bigint': + switch (value.constructor) { + case Date: + case RegExp: + case Error: return true; - case 'object': - if ( - value instanceof Date || - value instanceof RegExp || - value instanceof Error - ) { - return true; - } default: - return false; + switch (typeof value) { + case 'string': + case 'number': + case 'boolean': + case 'bigint': + return true; + default: + return false; + } } }; @@ -555,7 +553,7 @@ const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { initialValue = formatInitialValue(value); } } catch (error) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.warn(sanitizeErrorMessage(String(error))); initialValue = String(value); } @@ -867,7 +865,7 @@ const PropertyElement = ({ const currentState = getCurrentState(fiber); if (!currentState || !(baseStateKey in currentState)) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.warn(sanitizeErrorMessage('Invalid state key')); return; } diff --git a/packages/scan/src/web/components/inspector/overlay/index.tsx b/packages/scan/src/web/components/inspector/overlay/index.tsx index 8904e0fa..1e0a7084 100644 --- a/packages/scan/src/web/components/inspector/overlay/index.tsx +++ b/packages/scan/src/web/components/inspector/overlay/index.tsx @@ -1,11 +1,10 @@ -import { getDisplayName } from 'bippy'; +import { type Fiber, getDisplayName } from 'bippy'; import { useEffect, useRef } from 'preact/hooks'; -import type { Fiber } from 'react-reconciler'; import { ReactScanInternals, Store } from '~core/index'; import { + type States, findComponentDOMNode, getCompositeComponentFromElement, - type States, } from '~web/components/inspector/utils'; import { cn, throttle } from '~web/utils/helpers'; import { lerp } from '~web/utils/lerp'; @@ -288,7 +287,9 @@ export const ScanOverlay = () => { }; const unsubscribeAll = () => { - refCleanupMap.current.forEach((cleanup) => cleanup?.()); + for (const cleanup of refCleanupMap.current.values()) { + cleanup?.(); + } }; const cleanupCanvas = (canvas: HTMLCanvasElement) => { @@ -364,11 +365,11 @@ export const ScanOverlay = () => { startFadeOut(); }; - const handleMouseMove = throttle((e: MouseEvent) => { + const handleMouseMove = throttle((e?: MouseEvent) => { const state = Store.inspectState.peek(); if (state.kind !== 'inspecting' || !refEventCatcher.current) return; - const element = document.elementFromPoint(e.clientX, e.clientY); + const element = document.elementFromPoint(e?.clientX ?? 0, e?.clientY ?? 0); clearTimeout(refTimeout.current); if (element && element !== refCanvas.current) { @@ -590,6 +591,7 @@ export const ScanOverlay = () => { } }; + // biome-ignore lint/correctness/useExhaustiveDependencies: no deps useEffect(() => { const canvas = refCanvas.current; if (!canvas) return; diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index a2a4ece5..0f2a505f 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -1,6 +1,4 @@ -import { FunctionComponentTag } from 'bippy'; -import { type ComponentState } from 'react'; -import { type Fiber } from 'react-reconciler'; +import { type Fiber, FunctionComponentTag, type MemoizedState } from 'bippy'; import { isEqual } from '~core/utils'; interface ContextDependency { @@ -38,21 +36,32 @@ const ensureRecord = ( value: unknown, seen = new WeakSet(), ): Record => { - if (value == null) { + if (value === null || value === undefined) { return {}; } - switch (typeof value) { - case 'object': - if (value instanceof Element) { - return { - type: 'Element', - tagName: value.tagName.toLowerCase(), - }; - } - if (value instanceof Promise || 'then' in value) { - return { type: 'promise' }; - } - if (seen.has(value)) { + + switch (true) { + case value instanceof Element: + return { + type: 'Element', + tagName: (value as Element).tagName.toLowerCase(), + }; + + case typeof value === 'function': + return { + type: 'function', + name: (value as { name?: string }).name || 'anonymous', + }; + + case Boolean( + value && + (value instanceof Promise || + (typeof value === 'object' && 'then' in value)), + ): + return { type: 'promise' }; + + case typeof value === 'object': { + if (seen.has(value as object)) { return { type: 'circular' }; } @@ -62,14 +71,14 @@ const ensureRecord = ( return { type: 'array', length: value.length, items: safeArray }; } - seen.add(value); + seen.add(value as object); const result: Record = {}; try { - const keys = Object.keys(value); + const keys = Object.keys(value as object); for (const key of keys) { try { - const val = (value as any)[key]; + const val = (value as Record)[key]; result[key] = ensureRecord(val, seen); } catch { result[key] = { @@ -82,8 +91,8 @@ const ensureRecord = ( } catch { return { type: 'object' }; } - case 'function': - return { type: 'function', name: value.name || 'anonymous' }; + } + default: return { value }; } @@ -119,8 +128,7 @@ export const isDirectComponent = (fiber: Fiber): boolean => { if (!fiber || !fiber.type) return false; const isFunctionalComponent = typeof fiber.type === 'function'; - const isClassComponent = - fiber.type.prototype && fiber.type.prototype.isReactComponent; + const isClassComponent = fiber.type?.prototype?.isReactComponent ?? false; if (!(isFunctionalComponent || isClassComponent)) return false; @@ -133,7 +141,9 @@ export const isDirectComponent = (fiber: Fiber): boolean => { if (memoizedState.queue) { return true; } - memoizedState = memoizedState.next; + const nextState = memoizedState.next; + if (!nextState) break; + memoizedState = nextState; } return false; @@ -144,7 +154,7 @@ export const getCurrentState = (fiber: Fiber | null) => { try { if (fiber.tag === FunctionComponentTag && isDirectComponent(fiber)) { - return getCurrentFiberState(fiber); + return getCurrentFiberState(fiber) ?? {}; } } catch { // Silently fail @@ -190,7 +200,18 @@ export const getChangedState = (fiber: Fiber): Set => { return changes; }; -const getCurrentFiberState = (fiber: Fiber): ComponentState | null => { +const getStateValue = (memoizedState: MemoizedState): unknown => { + if (!memoizedState) return undefined; + + const queue = memoizedState.queue; + if (queue) { + return queue.lastRenderedState; + } + + return memoizedState.memoizedState; +}; + +const getCurrentFiberState = (fiber: Fiber): Record | null => { if (fiber.tag !== FunctionComponentTag || !isDirectComponent(fiber)) { return null; } @@ -205,7 +226,7 @@ const getCurrentFiberState = (fiber: Fiber): ComponentState | null => { if (!memoizedState) return null; - const currentState: ComponentState = {}; + const currentState: Record = {}; const stateNames = getStateNames(fiber); let index = 0; @@ -219,32 +240,14 @@ const getCurrentFiberState = (fiber: Fiber): ComponentState | null => { } index++; } - memoizedState = memoizedState.next; + const nextState = memoizedState.next; + if (!nextState) break; + memoizedState = nextState; } return currentState; }; -const getStateValue = (memoizedState: any): any => { - let value = memoizedState.memoizedState; - - if (memoizedState.queue?.pending) { - const pending = memoizedState.queue.pending; - let update = pending.next; - do { - if (update?.payload) { - value = - typeof update.payload === 'function' - ? update.payload(value) - : update.payload; - } - update = update.next; - } while (update !== pending.next); - } - - return value; -}; - export const getPropsOrder = (fiber: Fiber): Array => { const componentSource = fiber.type?.toString?.() || ''; const match = componentSource.match(PROPS_ORDER_REGEX); @@ -257,10 +260,9 @@ export const getPropsOrder = (fiber: Fiber): Array => { }; export const getCurrentProps = (fiber: Fiber): Record => { - const currentIsNewer = - fiber && fiber.alternate - ? (fiber.actualStartTime ?? 0) > (fiber.alternate?.actualStartTime ?? 0) - : true; + const currentIsNewer = fiber?.alternate + ? (fiber.actualStartTime ?? 0) > (fiber.alternate?.actualStartTime ?? 0) + : true; const baseProps = currentIsNewer ? fiber.memoizedProps || fiber.pendingProps @@ -292,8 +294,10 @@ export const getChangedProps = (fiber: Fiber): Set => { if (!isEqual(currentValue, previousValue)) { changes.add(key); - const count = (propsChangeCounts.get(key) ?? 0) + 1; - propsChangeCounts.set(key, count); + if (typeof currentValue !== 'function') { + const count = (propsChangeCounts.get(key) ?? 0) + 1; + propsChangeCounts.set(key, count); + } } } @@ -418,7 +422,7 @@ export const getChangedContext = (fiber: Fiber): Set => { searchFiber = searchFiber.return; } - if (providerFiber && providerFiber.alternate) { + if (providerFiber?.alternate) { const currentProviderValue = providerFiber.memoizedProps?.value; const alternateValue = providerFiber.alternate.memoizedProps?.value; diff --git a/packages/scan/src/web/components/inspector/utils.ts b/packages/scan/src/web/components/inspector/utils.ts index e2b30ac7..b8b29e93 100644 --- a/packages/scan/src/web/components/inspector/utils.ts +++ b/packages/scan/src/web/components/inspector/utils.ts @@ -1,12 +1,13 @@ import { + type Fiber, getDisplayName, isCompositeFiber, isHostFiber, traverseFiber, } from 'bippy'; -import { type Fiber } from 'react-reconciler'; import { ReactScanInternals } from '~core/index'; import { isEqual } from '~core/utils'; +import type { ExtendedReactRenderer } from '../../../types'; export type States = | { @@ -38,34 +39,15 @@ interface ReactInternalProps { [key: string]: Fiber; } -interface ReactRenderer { - findFiberByHostInstance: (instance: Element) => Fiber | null; - version: string; - bundleType: number; - rendererPackageName: string; - overrideHookState?: ( - fiber: Fiber, - id: string, - path: Array, - value: any, - ) => void; - overrideProps?: (fiber: Fiber, path: Array, value: any) => void; - scheduleUpdate?: (fiber: Fiber) => void; -} - -interface DevToolsHook { - renderers: Map; -} - export const getFiberFromElement = (element: Element): Fiber | null => { if ('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window) { - const { renderers } = window.__REACT_DEVTOOLS_GLOBAL_HOOK__ as DevToolsHook; - if (!renderers) return null; - for (const [, renderer] of Array.from(renderers)) { + const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; + if (!hook.renderers) return null; + for (const [, renderer] of Array.from(hook.renderers)) { try { - const fiber = renderer.findFiberByHostInstance(element); + const fiber = renderer.findFiberByHostInstance?.(element); if (fiber) return fiber; - } catch (e) { + } catch { // If React is mid-render, references to previous nodes may disappear } } @@ -126,7 +108,7 @@ export const getNearestFiberFromElement = ( const res = getParentCompositeFiber(fiber); return res ? res[0] : null; - } catch (error) { + } catch { return null; } }; @@ -231,65 +213,85 @@ export const getChangedPropsDetailed = (fiber: Fiber): Array => { return changes; }; +type OverrideHookState = ( + fiber: Fiber, + id: string, + path: Array, + value: unknown, +) => void; + +type OverrideProps = ( + fiber: Fiber, + path: Array, + value: unknown, +) => void; + interface OverrideMethods { - overrideProps: - | ((fiber: Fiber, path: Array, value: unknown) => void) - | null; - overrideHookState: - | ((fiber: Fiber, id: string, path: Array, value: unknown) => void) - | null; + overrideProps: OverrideProps | null; + overrideHookState: OverrideHookState | null; } +const isRecord = (value: unknown): value is Record => { + return value !== null && typeof value === 'object'; +}; + export const getOverrideMethods = (): OverrideMethods => { let overrideProps = null; let overrideHookState = null; if ('__REACT_DEVTOOLS_GLOBAL_HOOK__' in window) { - const { renderers } = window.__REACT_DEVTOOLS_GLOBAL_HOOK__ as DevToolsHook; + const { renderers } = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (renderers) { for (const [_, renderer] of Array.from(renderers)) { try { + const devToolsRenderer = renderer as ExtendedReactRenderer; + if (overrideHookState) { const prevOverrideHookState = overrideHookState; overrideHookState = ( fiber: Fiber, id: string, - path: Array, - value: any, + path: Array, + value: unknown, ) => { // Find the hook let current = fiber.memoizedState; - for (let i = 0; i < parseInt(id); i++) { + for (let i = 0; i < Number(id); i++) { + if (!current?.next) break; current = current.next; } - if (current && current.queue) { + if (current?.queue) { // Update through React's queue mechanism - const dispatch = current.queue.dispatch; - if (dispatch) { + const queue = current.queue; + if (isRecord(queue) && 'dispatch' in queue) { + const dispatch = queue.dispatch as (value: unknown) => void; dispatch(value); return; } } - // Fallback to direct override if queue dispatch isn't available prevOverrideHookState(fiber, id, path, value); - renderer.overrideHookState?.(fiber, id, path, value); + devToolsRenderer.overrideHookState?.(fiber, id, path, value); }; - } else if (renderer.overrideHookState) { - overrideHookState = renderer.overrideHookState.bind(renderer); + } else if (devToolsRenderer.overrideHookState) { + overrideHookState = devToolsRenderer.overrideHookState; } if (overrideProps) { const prevOverrideProps = overrideProps; - overrideProps = (fiber: Fiber, path: Array, value: any) => { + overrideProps = ( + fiber: Fiber, + path: Array, + value: unknown, + ) => { prevOverrideProps(fiber, path, value); - renderer.overrideProps?.(fiber, path, value); + devToolsRenderer.overrideProps?.(fiber, path, value); }; - } else if (renderer.overrideProps) { - overrideProps = renderer.overrideProps.bind(renderer); + } else if (devToolsRenderer.overrideProps) { + overrideProps = devToolsRenderer.overrideProps; } - } catch (e) { + } catch { /**/ } } @@ -300,28 +302,28 @@ export const getOverrideMethods = (): OverrideMethods => { }; const nonVisualTags = new Set([ - 'HTML', - 'META', - 'SCRIPT', - 'LINK', - 'STYLE', - 'HEAD', - 'TITLE', - 'NOSCRIPT', - 'BASE', - 'TEMPLATE', - 'IFRAME', - 'EMBED', - 'OBJECT', - 'PARAM', - 'SOURCE', - 'TRACK', - 'AREA', - 'PORTAL', - 'SLOT', - 'XML', - 'DOCTYPE', - 'COMMENT' + 'html', + 'meta', + 'script', + 'link', + 'style', + 'head', + 'title', + 'noscript', + 'base', + 'template', + 'iframe', + 'embed', + 'object', + 'param', + 'source', + 'track', + 'area', + 'portal', + 'slot', + 'xml', + 'doctype', + 'comment', ]); export const findComponentDOMNode = ( fiber: Fiber, @@ -329,10 +331,7 @@ export const findComponentDOMNode = ( ): HTMLElement | null => { if (fiber.stateNode && 'nodeType' in fiber.stateNode) { const element = fiber.stateNode as HTMLElement; - if ( - excludeNonVisualTags && - nonVisualTags.has(element.tagName) - ) { + if (excludeNonVisualTags && nonVisualTags.has(element.tagName.toLowerCase())) { return null; } return element; @@ -375,20 +374,20 @@ export const getInspectableElements = ( if (inspectable) { const { parentCompositeFiber } = getCompositeComponentFromElement(inspectable); + + if (!parentCompositeFiber) return; + result.push({ element: inspectable, depth, - name: - (parentCompositeFiber!.type && - getDisplayName(parentCompositeFiber!.type)) ?? - 'Unknown', + name: getDisplayName(parentCompositeFiber.type) ?? 'Unknown', }); } // Traverse children first (depth-first) - Array.from(element.children).forEach((child) => { + for (const child of Array.from(element.children)) { traverse(child as HTMLElement, inspectable ? depth + 1 : depth); - }); + } }; traverse(root); diff --git a/packages/scan/src/web/components/widget/header.tsx b/packages/scan/src/web/components/widget/header.tsx index 56addc46..9dd250f6 100644 --- a/packages/scan/src/web/components/widget/header.tsx +++ b/packages/scan/src/web/components/widget/header.tsx @@ -58,6 +58,7 @@ export const BtnReplay = () => { return ( + )) + } ); diff --git a/packages/scan/src/web/overlay.ts b/packages/scan/src/web/overlay.ts index 4981812b..5f6ae246 100644 --- a/packages/scan/src/web/overlay.ts +++ b/packages/scan/src/web/overlay.ts @@ -80,7 +80,6 @@ export const initReactScanOverlay = () => { transfer: [offscreen], }, ) - // eslint-disable-next-line no-console .catch(console.error); updateCanvasSize(); diff --git a/packages/scan/src/web/state.ts b/packages/scan/src/web/state.ts index e556ab7a..7ceb8e8d 100644 --- a/packages/scan/src/web/state.ts +++ b/packages/scan/src/web/state.ts @@ -1,8 +1,8 @@ import { signal } from '@preact/signals'; -import { - type Corner, - type WidgetConfig, - type WidgetSettings, +import type { + Corner, + WidgetConfig, + WidgetSettings, } from './components/widget/types'; import { LOCALSTORAGE_KEY, MIN_SIZE, SAFE_AREA } from './constants'; import { readLocalStorage, saveLocalStorage } from './utils/helpers'; diff --git a/packages/scan/src/web/toolbar.tsx b/packages/scan/src/web/toolbar.tsx index 03f89f7a..0d6873c0 100644 --- a/packages/scan/src/web/toolbar.tsx +++ b/packages/scan/src/web/toolbar.tsx @@ -9,7 +9,7 @@ export const createToolbar = (root: ShadowRoot): HTMLElement => { const originalRemove = container.remove.bind(container); - container.remove = function () { + container.remove = () => { if (container.hasChildNodes()) { // Double render(null) is needed to fully unmount Preact components. // The first call initiates unmounting, while the second ensures diff --git a/packages/scan/src/web/utils/helpers.ts b/packages/scan/src/web/utils/helpers.ts index 38ab7c63..75b891e5 100644 --- a/packages/scan/src/web/utils/helpers.ts +++ b/packages/scan/src/web/utils/helpers.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from 'clsx'; +import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export const cn = (...inputs: Array): string => { @@ -20,35 +20,35 @@ export const onIdle = (callback: () => void) => { return setTimeout(callback, 0); }; -export const throttle = ) => any>( - callback: T, +export const throttle = ( + callback: (e?: E) => void, delay: number, -) => { +): ((e?: E) => void) => { let lastCall = 0; - return (...args: Parameters) => { + return (e?: E) => { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; - return callback(...args); + return callback(e); } + return undefined; }; }; - -export const debounce = ) => any>( +export const debounce = Promise>( fn: T, wait: number, options: { leading?: boolean; trailing?: boolean } = {}, ) => { let timeoutId: number | undefined; - let lastArgs: Parameters | undefined; + let lastArg: boolean | null | undefined; let isLeadingInvoked = false; - const debounced = (...args: Parameters) => { - lastArgs = args; + const debounced = (enabled: boolean | null) => { + lastArg = enabled; if (options.leading && !isLeadingInvoked) { isLeadingInvoked = true; - fn(...args); + fn(enabled); return; } @@ -60,7 +60,9 @@ export const debounce = ) => any>( timeoutId = window.setTimeout(() => { isLeadingInvoked = false; timeoutId = undefined; - fn(...lastArgs!); + if (lastArg !== undefined) { + fn(lastArg); + } }, wait); } }; @@ -70,7 +72,7 @@ export const debounce = ) => any>( clearTimeout(timeoutId); timeoutId = undefined; isLeadingInvoked = false; - lastArgs = undefined; + lastArg = undefined; } }; diff --git a/packages/scan/src/web/utils/log.ts b/packages/scan/src/web/utils/log.ts index d639fc75..e3787c1c 100644 --- a/packages/scan/src/web/utils/log.ts +++ b/packages/scan/src/web/utils/log.ts @@ -32,8 +32,8 @@ export const log = (renders: Array) => { ]); if (!labelText) continue; - let prevChangedProps: Record | null = null; - let nextChangedProps: Record | null = null; + let prevChangedProps: Record | null = null; + let nextChangedProps: Record | null = null; if (render.changes) { for (let i = 0, len = render.changes.length; i < len; i++) { @@ -67,28 +67,28 @@ export const log = (renders: Array) => { logMap.set(labelText, changeLog); } for (const [name, changeLog] of Array.from(logMap.entries())) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.group( `%c${name}`, 'background: hsla(0,0%,70%,.3); border-radius:3px; padding: 0 2px;', ); for (const { type, prev, next, unstable } of changeLog) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.log(`${type}:`, unstable ? '⚠️' : '', prev, '!==', next); } - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.groupEnd(); } }; export const logIntro = () => { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.log( - `%c[·] %cReact Scan`, + '%c[·] %cReact Scan', 'font-weight:bold;color:#7a68e8;font-size:20px;', 'font-weight:bold;font-size:14px;', ); - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.log( 'Try React Scan Monitoring to target performance issues in production: https://react-scan.com/monitoring', ); diff --git a/packages/scan/src/web/utils/outline-worker.ts b/packages/scan/src/web/utils/outline-worker.ts index 71049ac7..77a155cb 100644 --- a/packages/scan/src/web/utils/outline-worker.ts +++ b/packages/scan/src/web/utils/outline-worker.ts @@ -45,7 +45,7 @@ function setupOutlineWorker(): (action: OutlineWorkerAction) => Promise { 'Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace'; let ctx: OffscreenCanvasRenderingContext2D | undefined; - const enum Reason { + enum Reason { Commit = 0b001, Unstable = 0b010, Unnecessary = 0b100, diff --git a/packages/scan/src/web/utils/outline.ts b/packages/scan/src/web/utils/outline.ts index 2e72caab..ba4a2be1 100644 --- a/packages/scan/src/web/utils/outline.ts +++ b/packages/scan/src/web/utils/outline.ts @@ -1,13 +1,13 @@ -import { type Fiber } from 'react-reconciler'; -import { ReactScanInternals, type OutlineKey } from '~core/index'; -import { type AggregatedChange } from '~core/instrumentation'; +import type { Fiber } from 'bippy'; +import { type OutlineKey, ReactScanInternals } from '~core/index'; +import type { AggregatedChange } from '~core/instrumentation'; import { getLabelText, joinAggregations } from '~core/utils'; import { lerp } from '~web/utils/lerp'; import { throttle } from './helpers'; import { LRUMap } from './lru'; -import { outlineWorker, type DrawingQueue } from './outline-worker'; +import { type DrawingQueue, outlineWorker } from './outline-worker'; -const enum Reason { +enum Reason { Commit = 0b001, Unstable = 0b010, Unnecessary = 0b100, @@ -141,14 +141,12 @@ export const fadeOutOutline = () => { const invariantActiveOutline = activeOutline as { [K in keyof Outline]: NonNullable; }; - let frame; + let frame: number | null = null; for (const aggregatedRender of invariantActiveOutline.groupedAggregatedRender.values()) { - aggregatedRender.frame! += 1; - - frame = frame - ? Math.max(aggregatedRender.frame!, frame) - : aggregatedRender.frame!; + const newFrame = (aggregatedRender.frame ?? 0) + 1; + aggregatedRender.frame = newFrame; + frame = frame ? Math.max(newFrame, frame) : newFrame; } if (!frame) { @@ -179,9 +177,6 @@ export const fadeOutOutline = () => { // don't re-create to avoid gc time phases.clear(); - let unstable = false; - let isUnnecessary = false; - for (const render of invariantActiveOutline.groupedAggregatedRender.values()) { if (render.unnecessary) { reasons |= Reason.Unnecessary; @@ -272,7 +267,7 @@ export const fadeOutOutline = () => { fiber, aggregatedRender, ] of invariantActiveOutline.groupedAggregatedRender) { - if (aggregatedRender.frame! >= totalFrames) { + if ((aggregatedRender.frame ?? 0) >= totalFrames) { invariantActiveOutline.groupedAggregatedRender.delete(fiber); } } @@ -330,7 +325,7 @@ export interface Outline { estimatedTextWidth: number | null; // todo: estimated is stupid just make it the actual } -export const enum RenderPhase { +export enum RenderPhase { Mount = 0b001, Update = 0b010, Unmount = 0b100, @@ -509,8 +504,8 @@ const activateOutlines = async () => { value.frame = 45; // todo: make this max frame, not hardcoded // for interpolation reference equality - if (existingOutline) { - existingOutline.current = value.computedCurrent!; + if (existingOutline && value.computedCurrent) { + existingOutline.current = value.computedCurrent; } } } @@ -563,14 +558,17 @@ function ascendingTransformedOutlineLabel( function getTransformedOutlineLabels( labels: Array, ): Array { - let array: Array = []; + const array: Array = []; for (let i = 0, len = labels.length, label: OutlineLabel; i < len; i++) { label = labels[i]; - array.push({ - original: label, - rect: applyLabelTransform(label.activeOutline.current!, label.textWidth), - }); + const current = label.activeOutline.current; + if (current) { + array.push({ + original: label, + rect: applyLabelTransform(current, label.textWidth), + }); + } } array.sort(ascendingTransformedOutlineLabel); @@ -581,7 +579,7 @@ function getTransformedOutlineLabels( function getMergedOutlineLabels( labels: Array, ): Array { - let array: Array = []; + const array: Array = []; for (let i = 0, len = labels.length; i < len; i++) { array.push(toMergedLabel(labels[i])); @@ -641,12 +639,17 @@ function toMergedLabel( label: OutlineLabel, rectOverride?: DOMRect, ): MergedOutlineLabel { + const current = label.activeOutline.current; + const groupedAggregatedRender = label.activeOutline.groupedAggregatedRender; const rect = rectOverride ?? - applyLabelTransform(label.activeOutline.current!, label.textWidth); - const groupedArray = Array.from( - label.activeOutline.groupedAggregatedRender!.values(), - ); + (current + ? applyLabelTransform(current, label.textWidth) + : new DOMRect(0, 0, 0, 0)); + const groupedArray = groupedAggregatedRender + ? Array.from(groupedAggregatedRender.values()) + : []; + return { alpha: label.alpha, color: label.color, @@ -768,8 +771,9 @@ function getMeasuringContext(): MeasuringContext { } export const measureTextCached = (text: string): TextMetrics => { - if (textMeasurementCache.has(text)) { - return textMeasurementCache.get(text)!; + const cached = textMeasurementCache.get(text); + if (cached) { + return cached; } const ctx = getMeasuringContext(); ctx.font = `11px ${MONO_FONT}`; diff --git a/packages/scan/src/web/utils/preact/constant.ts b/packages/scan/src/web/utils/preact/constant.ts index e57c4939..2b8ec617 100644 --- a/packages/scan/src/web/utils/preact/constant.ts +++ b/packages/scan/src/web/utils/preact/constant.ts @@ -1,17 +1,22 @@ -import { createElement, type Component, type FunctionComponent } from 'preact'; +import { + type Attributes, + type Component, + type FunctionComponent, + createElement, +} from 'preact'; function CONSTANT_UPDATE() { return false; } -export function constant

(Component: FunctionComponent

) { +export function constant

( + Component: FunctionComponent

, +) { function Memoed(this: Component

, props: P) { this.shouldComponentUpdate = CONSTANT_UPDATE; - return createElement(Component, props as any); // Preact has a broken type declaration + return createElement

(Component, props); } - Memoed.displayName = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - `Memo(${Component.displayName || Component.name})`; + Memoed.displayName = `Memo(${Component.displayName || Component.name})`; Memoed.prototype.isReactComponent = true; Memoed._forwarded = true; return Memoed; diff --git a/packages/scan/tsconfig.json b/packages/scan/tsconfig.json index 32bbf81c..b689a064 100644 --- a/packages/scan/tsconfig.json +++ b/packages/scan/tsconfig.json @@ -23,7 +23,7 @@ } }, "include": [ - "src/**/*", + "src", "global.d.ts" ], "exclude": [ diff --git a/packages/scan/tsup.config.ts b/packages/scan/tsup.config.ts index cdd93641..8298cfc3 100644 --- a/packages/scan/tsup.config.ts +++ b/packages/scan/tsup.config.ts @@ -1,32 +1,25 @@ -import fsPromise from 'node:fs/promises'; import * as fs from 'node:fs'; +import fsPromise from 'node:fs/promises'; import path from 'node:path'; -import { defineConfig } from 'tsup'; import { TsconfigPathsPlugin } from '@esbuild-plugins/tsconfig-paths'; import { init, parse } from 'es-module-lexer'; +import { defineConfig } from 'tsup'; const DIST_PATH = './dist'; -if (!fs.existsSync(DIST_PATH)) { - fs.mkdirSync(DIST_PATH, { recursive: true }); -} - const addDirectivesToChunkFiles = async (readPath: string): Promise => { try { const files = await fsPromise.readdir(readPath); for (const file of files) { if (file.endsWith('.mjs') || file.endsWith('.js')) { const filePath = path.join(readPath, file); - const data = await fsPromise.readFile(filePath, 'utf8'); - const updatedContent = `'use client';\n${data}`; - await fsPromise.writeFile(filePath, updatedContent, 'utf8'); } } } catch (err) { - // eslint-disable-next-line no-console -- We need to log the error + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error('Error:', err); } }; @@ -53,6 +46,11 @@ const banner = `/** void (async () => { await init; + if (fs.existsSync(DIST_PATH)) { + fs.rmSync(DIST_PATH, { recursive: true }); + } + fs.mkdirSync(DIST_PATH, { recursive: true }); + const code = fs.readFileSync('./src/core/index.ts', 'utf8'); const [_, allExports] = parse(code); const names: Array = []; @@ -77,10 +75,7 @@ void (async () => { for (const ext of ['js', 'mjs', 'global.js']) { fs.writeFileSync(`./dist/rsc-shim.${ext}`, script); } - for (const ext of ['d.mts', 'd.ts']) { - fs.writeFileSync(`./dist/rsc-shim.${ext}`, `export {}`); - } - }, 500); // for some reason it clears the file if we don't wait + }, 500); })(); export default defineConfig([ @@ -106,7 +101,6 @@ export default defineConfig([ external: [ 'react', 'react-dom', - 'react-reconciler', 'next', 'next/navigation', 'react-router', @@ -133,7 +127,7 @@ export default defineConfig([ }, outDir: DIST_PATH, splitting: false, - clean: true, + clean: false, sourcemap: false, format: ['cjs', 'esm'], target: 'esnext', @@ -163,7 +157,6 @@ export default defineConfig([ external: [ 'react', 'react-dom', - 'react-reconciler', 'next', 'next/navigation', 'react-router', @@ -188,7 +181,7 @@ export default defineConfig([ js: banner, }, splitting: false, - clean: true, + clean: false, sourcemap: false, format: ['cjs'], target: 'esnext', @@ -213,7 +206,7 @@ export default defineConfig([ outDir: `${DIST_PATH}/react-component-name`, splitting: false, sourcemap: false, - clean: true, + clean: false, format: ['cjs', 'esm'], target: 'esnext', external: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 416066ca..11e4dbea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,15 +18,9 @@ importers: '@types/node': specifier: ^22.10.2 version: 22.10.2 - '@typescript-eslint/eslint-plugin': - specifier: ^6.0.0 - version: 6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5) - '@typescript-eslint/parser': - specifier: ^6.0.0 - version: 6.0.0(eslint@8.57.1)(typescript@5.4.5) '@vercel/style-guide': specifier: ^6.0.0 - version: 6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.4.5)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0)) + version: 6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.7.3)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -36,18 +30,6 @@ importers: chalk: specifier: ^5.3.0 version: 5.3.0 - eslint: - specifier: ^8.57.1 - version: 8.57.1 - eslint-import-resolver-typescript: - specifier: ^3.7.0 - version: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-jsonc: - specifier: ^2.18.2 - version: 2.18.2(eslint@8.57.1) - eslint-plugin-tailwindcss: - specifier: ^3.17.5 - version: 3.17.5(tailwindcss@3.4.17) postcss: specifier: ^8.4.49 version: 8.4.49 @@ -55,17 +37,17 @@ importers: specifier: ^3.4.17 version: 3.4.17 typescript: - specifier: 5.4.5 - version: 5.4.5 + specifier: latest + version: 5.7.3 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.4.5)(vite@5.4.3(@types/node@22.10.2)(terser@5.36.0)) + version: 5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1)) examples/sierpinski: dependencies: '@vercel/analytics': specifier: ^1.4.0 - version: 1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1))(react@19.0.0-rc.1) + version: 1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1))(react@19.0.0-rc.1) babel-plugin-react-compiler: specifier: 19.0.0-beta-a7bf2bd-20241110 version: 19.0.0-beta-a7bf2bd-20241110 @@ -86,14 +68,23 @@ importers: version: 0.8.7(rollup@4.28.0)(vite@5.4.3(@types/node@22.10.2)(terser@5.36.0)) devDependencies: '@vitejs/plugin-react': - specifier: ^4.3.1 - version: 4.3.1(vite@5.4.3(@types/node@22.10.2)(terser@5.36.0)) + specifier: ^4.3.4 + version: 4.3.4(vite@5.4.3(@types/node@22.10.2)(terser@5.36.0)) vite: specifier: ^5.4.3 version: 5.4.3(@types/node@22.10.2)(terser@5.36.0) packages/extension: dependencies: + '@pivanov/utils': + specifier: ^0.0.1 + version: 0.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) react-scan: specifier: workspace:* version: link:../scan @@ -101,9 +92,6 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: - '@pivanov/utils': - specifier: ^0.0.1 - version: 0.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/chrome': specifier: ^0.0.281 version: 0.0.281 @@ -116,6 +104,9 @@ importers: '@types/webextension-polyfill': specifier: ^0.10.0 version: 0.10.0 + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.3.4(vite@6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1)) bestzip: specifier: ^2.2.1 version: 2.2.1 @@ -123,11 +114,14 @@ importers: specifier: ^7.0.3 version: 7.0.3 vite: - specifier: ^5.4.3 - version: 5.4.3(@types/node@22.10.2)(terser@5.36.0) + specifier: ^6.0.7 + version: 6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1) vite-plugin-web-extension: - specifier: ^4.3.1 - version: 4.3.1(@types/node@22.10.2)(terser@5.36.0) + specifier: ^4.4.3 + version: 4.4.3(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1)) webextension-polyfill: specifier: ^0.10.0 version: 0.10.0 @@ -136,7 +130,7 @@ importers: dependencies: '@vercel/analytics': specifier: ^1.4.0 - version: 1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) bippy: specifier: ^0.0.21 version: 0.0.21 @@ -190,8 +184,8 @@ importers: specifier: ^20.17.9 version: 20.17.10 bippy: - specifier: ^0.0.25 - version: 0.0.25 + specifier: ^0.2.0 + version: 0.2.0 esbuild: specifier: ^0.24.0 version: 0.24.0 @@ -220,25 +214,22 @@ importers: devDependencies: '@esbuild-plugins/tsconfig-paths': specifier: ^0.1.2 - version: 0.1.2(esbuild@0.24.0)(typescript@5.6.3) + version: 0.1.2(esbuild@0.24.0)(typescript@5.7.3) '@remix-run/react': specifier: '*' - version: 2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.6.3) + version: 2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3) '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 '@types/react': specifier: ^18.0.0 version: 18.3.12 - '@types/react-reconciler': - specifier: ^0.28.8 - version: 0.28.8 '@types/react-router': specifier: ^5.1.0 version: 5.1.20 '@vercel/style-guide': specifier: ^6.0.0 - version: 6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.6.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0)) + version: 6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.7.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -263,9 +254,6 @@ importers: react-dom: specifier: '*' version: 18.2.0(react@18.2.0) - react-reconciler: - specifier: ^0.29.2 - version: 0.29.2(react@18.2.0) react-router: specifier: ^5.0.0 version: 5.3.4(react@18.2.0) @@ -280,7 +268,7 @@ importers: version: 5.36.0 tsup: specifier: ^8.0.0 - version: 8.2.4(jiti@1.21.6)(postcss@8.4.49)(tsx@4.0.0)(typescript@5.6.3)(yaml@2.6.1) + version: 8.2.4(jiti@1.21.6)(postcss@8.4.49)(tsx@4.0.0)(typescript@5.7.3)(yaml@2.6.1) vitest: specifier: ^1.0.0 version: 1.0.0(@types/node@20.17.10)(terser@5.36.0) @@ -289,7 +277,7 @@ importers: dependencies: '@vercel/analytics': specifier: ^1.4.1 - version: 1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + version: 1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@vercel/speed-insights': specifier: ^1.1.0 version: 1.1.0(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) @@ -304,7 +292,7 @@ importers: version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-scan: specifier: ^0.0.48 - version: 0.0.48(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react-router-dom@6.28.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-router@6.28.0(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.28.0) + version: 0.0.48(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react-router-dom@6.28.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-router@6.28.0(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.28.0) zod: specifier: ^3.23.8 version: 3.23.8 @@ -320,7 +308,7 @@ importers: version: 8.57.1 eslint-config-next: specifier: 15.0.3 - version: 15.0.3(eslint@8.57.1)(typescript@5.6.3) + version: 15.0.3(eslint@8.57.1)(typescript@5.7.3) packages: @@ -524,6 +512,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.24.2': + resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -548,6 +542,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.24.2': + resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -572,6 +572,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.24.2': + resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -596,6 +602,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.24.2': + resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -620,6 +632,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.24.2': + resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -644,6 +662,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.24.2': + resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -668,6 +692,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.24.2': + resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -692,6 +722,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.2': + resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -716,6 +752,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.24.2': + resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -740,6 +782,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.24.2': + resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -764,6 +812,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.24.2': + resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -788,6 +842,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.24.2': + resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -812,6 +872,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.24.2': + resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -836,6 +902,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.24.2': + resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -860,6 +932,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.24.2': + resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -884,6 +962,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.24.2': + resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -908,6 +992,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.24.2': + resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.24.2': + resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -932,6 +1028,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.2': + resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.23.1': resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} @@ -944,6 +1046,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.24.2': + resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -968,6 +1076,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.2': + resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -992,6 +1106,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.24.2': + resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -1016,6 +1136,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.24.2': + resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -1040,6 +1166,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.24.2': + resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -1064,6 +1196,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.24.2': + resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1551,9 +1689,6 @@ packages: '@types/react-dom@18.0.9': resolution: {integrity: sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==} - '@types/react-reconciler@0.28.8': - resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} - '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} @@ -1785,6 +1920,12 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitest/expect@1.0.0': resolution: {integrity: sha512-EbqHCSzQhAY8Su/uLsMCDXkC26LyqQO54kAqJy/DubBqwpRre1iMzvDMPWx+YPfNIN3w7/ydKaJWjH6qRoz0fA==} @@ -1986,8 +2127,8 @@ packages: bippy@0.0.21: resolution: {integrity: sha512-E8WYEeiI6kxbxCS5k1un0crgMg3E9td4HrZNndBSJjoGs8/WZl31EyhKmOwVpuCCXNv1EymFueNHEWFgpknozw==} - bippy@0.0.25: - resolution: {integrity: sha512-+rvlmS7vbv704MjmpMLaSNKezGkc7xux7/DbhTp61RFQZAYwH8V0pbxGYiDWxA9a+7RxNFhHtsSIu9uoB+eK0Q==} + bippy@0.2.0: + resolution: {integrity: sha512-rK6cZxWDvJ9tTmp3pCboCju/YmF1LGvxkIKOfGznP8XhM1bUMjNQCyhWVI4ycbZ5dW8SfBc6+ZxyQ5NbHaGGcA==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2491,6 +2632,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.24.2: + resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2507,12 +2653,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-compat-utils@0.6.4: - resolution: {integrity: sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - eslint-config-next@15.0.3: resolution: {integrity: sha512-IGP2DdQQrgjcr4mwFPve4DrCqo7CVVez1WoYY47XwKSrYO4hC0Dlb+iJA60i0YfICOzgNADIb8r28BpQ5Zs0wg==} peerDependencies: @@ -2563,17 +2703,6 @@ packages: eslint-plugin-import-x: optional: true - eslint-json-compat-utils@0.2.1: - resolution: {integrity: sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==} - engines: {node: '>=12'} - peerDependencies: - '@eslint/json': '*' - eslint: '*' - jsonc-eslint-parser: ^2.4.0 - peerDependenciesMeta: - '@eslint/json': - optional: true - eslint-module-utils@2.12.0: resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} engines: {node: '>=4'} @@ -2624,12 +2753,6 @@ packages: jest: optional: true - eslint-plugin-jsonc@2.18.2: - resolution: {integrity: sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '>=6.0.0' - eslint-plugin-jsx-a11y@6.10.2: resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} engines: {node: '>=4.0'} @@ -2664,12 +2787,6 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-tailwindcss@3.17.5: - resolution: {integrity: sha512-8Mi7p7dm+mO1dHgRHHFdPu4RDTBk69Cn4P0B40vRQR+MrguUpwmKwhZy1kqYe3Km8/4nb+cyrCF+5SodOEmaow==} - engines: {node: '>=18.12.0'} - peerDependencies: - tailwindcss: ^3.4.0 - eslint-plugin-testing-library@6.5.0: resolution: {integrity: sha512-Ls5TUfLm5/snocMAOlofSOJxNN0aKqwTlco7CrNtMjkTdQlkpSMaeTCDHCuXfzrI97xcx2rSCNeKeJjtpkNC1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} @@ -3386,10 +3503,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonc-eslint-parser@2.4.0: - resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -4070,12 +4183,6 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-reconciler@0.29.2: - resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^18.3.1 - react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -4556,10 +4663,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.6.2: - resolution: {integrity: sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==} - engines: {node: '>=12.20'} - synckit@0.9.2: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4757,13 +4860,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} - engines: {node: '>=14.17'} - hasBin: true - - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} hasBin: true @@ -4839,8 +4937,8 @@ packages: '@nuxt/kit': optional: true - vite-plugin-web-extension@4.3.1: - resolution: {integrity: sha512-yG/07Rzk70SxLUQIfZMbNm0472gbFPkoPCAJvcGhyblTrg0FPSKfkQJ4yug0//IxoYCaTv5WIcNJCuVntUz4rQ==} + vite-plugin-web-extension@4.4.3: + resolution: {integrity: sha512-xOQR4o5bfxnZDlVxDYoK/aZO9Tt92CItaGybrKC41rl218Of5fsLDQDYR95rQd2wg8DnT8R9CEheQ++lmP+Euw==} engines: {node: '>=16'} vite-tsconfig-paths@5.1.4: @@ -4882,6 +4980,46 @@ packages: terser: optional: true + vite@6.0.7: + resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@1.0.0: resolution: {integrity: sha512-jpablj5+ifiFHV3QGOxPews3uxBuu6rQUzTaQYtEd6ocBpdQBil6AvmmGRQ3Rn0WPgyzb+Ni+JekfMyng+qYng==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4911,6 +5049,9 @@ packages: resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} engines: {node: '>=10.13.0'} + web-ext-option-types@8.3.1: + resolution: {integrity: sha512-mKG1fplVXMKYaEeSs35v/x9YIx7FJJDCBQNoLoMvUXeFck0rNC2qnHsYaRnVXXd1XL7o/hz+5+T7YqpTVyEK3w==} + web-ext-run@0.2.2: resolution: {integrity: sha512-GD59q5/1wYQJXTHrljMZaBa3cCz+Jj3FMDLYgKyAa34TPcHSuMaGqp7TcLJ66PCe43C3hmbEAZd8QCpAB34eiw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5258,13 +5399,13 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild-plugins/tsconfig-paths@0.1.2(esbuild@0.24.0)(typescript@5.6.3)': + '@esbuild-plugins/tsconfig-paths@0.1.2(esbuild@0.24.0)(typescript@5.7.3)': dependencies: debug: 4.3.7 esbuild: 0.24.0 find-up: 5.0.0 strip-json-comments: 3.1.1 - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -5277,6 +5418,9 @@ snapshots: '@esbuild/aix-ppc64@0.24.0': optional: true + '@esbuild/aix-ppc64@0.24.2': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true @@ -5289,6 +5433,9 @@ snapshots: '@esbuild/android-arm64@0.24.0': optional: true + '@esbuild/android-arm64@0.24.2': + optional: true + '@esbuild/android-arm@0.18.20': optional: true @@ -5301,6 +5448,9 @@ snapshots: '@esbuild/android-arm@0.24.0': optional: true + '@esbuild/android-arm@0.24.2': + optional: true + '@esbuild/android-x64@0.18.20': optional: true @@ -5313,6 +5463,9 @@ snapshots: '@esbuild/android-x64@0.24.0': optional: true + '@esbuild/android-x64@0.24.2': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true @@ -5325,6 +5478,9 @@ snapshots: '@esbuild/darwin-arm64@0.24.0': optional: true + '@esbuild/darwin-arm64@0.24.2': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true @@ -5337,6 +5493,9 @@ snapshots: '@esbuild/darwin-x64@0.24.0': optional: true + '@esbuild/darwin-x64@0.24.2': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true @@ -5349,6 +5508,9 @@ snapshots: '@esbuild/freebsd-arm64@0.24.0': optional: true + '@esbuild/freebsd-arm64@0.24.2': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true @@ -5361,6 +5523,9 @@ snapshots: '@esbuild/freebsd-x64@0.24.0': optional: true + '@esbuild/freebsd-x64@0.24.2': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true @@ -5373,6 +5538,9 @@ snapshots: '@esbuild/linux-arm64@0.24.0': optional: true + '@esbuild/linux-arm64@0.24.2': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true @@ -5385,6 +5553,9 @@ snapshots: '@esbuild/linux-arm@0.24.0': optional: true + '@esbuild/linux-arm@0.24.2': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true @@ -5397,6 +5568,9 @@ snapshots: '@esbuild/linux-ia32@0.24.0': optional: true + '@esbuild/linux-ia32@0.24.2': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true @@ -5409,6 +5583,9 @@ snapshots: '@esbuild/linux-loong64@0.24.0': optional: true + '@esbuild/linux-loong64@0.24.2': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true @@ -5421,6 +5598,9 @@ snapshots: '@esbuild/linux-mips64el@0.24.0': optional: true + '@esbuild/linux-mips64el@0.24.2': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true @@ -5433,6 +5613,9 @@ snapshots: '@esbuild/linux-ppc64@0.24.0': optional: true + '@esbuild/linux-ppc64@0.24.2': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true @@ -5445,6 +5628,9 @@ snapshots: '@esbuild/linux-riscv64@0.24.0': optional: true + '@esbuild/linux-riscv64@0.24.2': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true @@ -5457,6 +5643,9 @@ snapshots: '@esbuild/linux-s390x@0.24.0': optional: true + '@esbuild/linux-s390x@0.24.2': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true @@ -5469,6 +5658,12 @@ snapshots: '@esbuild/linux-x64@0.24.0': optional: true + '@esbuild/linux-x64@0.24.2': + optional: true + + '@esbuild/netbsd-arm64@0.24.2': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true @@ -5481,12 +5676,18 @@ snapshots: '@esbuild/netbsd-x64@0.24.0': optional: true + '@esbuild/netbsd-x64@0.24.2': + optional: true + '@esbuild/openbsd-arm64@0.23.1': optional: true '@esbuild/openbsd-arm64@0.24.0': optional: true + '@esbuild/openbsd-arm64@0.24.2': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true @@ -5499,6 +5700,9 @@ snapshots: '@esbuild/openbsd-x64@0.24.0': optional: true + '@esbuild/openbsd-x64@0.24.2': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true @@ -5511,6 +5715,9 @@ snapshots: '@esbuild/sunos-x64@0.24.0': optional: true + '@esbuild/sunos-x64@0.24.2': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true @@ -5523,6 +5730,9 @@ snapshots: '@esbuild/win32-arm64@0.24.0': optional: true + '@esbuild/win32-arm64@0.24.2': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true @@ -5535,6 +5745,9 @@ snapshots: '@esbuild/win32-ia32@0.24.0': optional: true + '@esbuild/win32-ia32@0.24.2': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true @@ -5547,6 +5760,9 @@ snapshots: '@esbuild/win32-x64@0.24.0': optional: true + '@esbuild/win32-x64@0.24.2': + optional: true + '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -5749,10 +5965,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} - '@pivanov/utils@0.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@pivanov/utils@0.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) '@pkgjs/parseargs@0.11.0': optional: true @@ -5780,60 +5996,60 @@ snapshots: '@preact/signals-core': 1.8.0 preact: 10.25.1 - '@remix-run/react@2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.6.3)': + '@remix-run/react@2.15.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3)': dependencies: '@remix-run/router': 1.21.0 - '@remix-run/server-runtime': 2.15.0(typescript@5.6.3) + '@remix-run/server-runtime': 2.15.0(typescript@5.7.3) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-router: 6.28.0(react@18.2.0) react-router-dom: 6.28.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 - '@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.3)': + '@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3)': dependencies: '@remix-run/router': 1.21.0 - '@remix-run/server-runtime': 2.15.0(typescript@5.6.3) + '@remix-run/server-runtime': 2.15.0(typescript@5.7.3) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) react-router: 6.28.0(react@19.0.0) react-router-dom: 6.28.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 optional: true - '@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3)': + '@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3)': dependencies: '@remix-run/router': 1.21.0 - '@remix-run/server-runtime': 2.15.0(typescript@5.6.3) + '@remix-run/server-runtime': 2.15.0(typescript@5.7.3) react: 19.0.0-rc-66855b96-20241106 react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-router: 6.28.0(react@19.0.0-rc-66855b96-20241106) react-router-dom: 6.28.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 optional: true - '@remix-run/react@2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.6.3)': + '@remix-run/react@2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.7.3)': dependencies: '@remix-run/router': 1.21.0 - '@remix-run/server-runtime': 2.15.0(typescript@5.6.3) + '@remix-run/server-runtime': 2.15.0(typescript@5.7.3) react: 19.0.0-rc.1 react-dom: 19.0.0-rc.1(react@19.0.0-rc.1) react-router: 6.28.0(react@19.0.0-rc.1) react-router-dom: 6.28.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 optional: true '@remix-run/router@1.21.0': {} - '@remix-run/server-runtime@2.15.0(typescript@5.6.3)': + '@remix-run/server-runtime@2.15.0(typescript@5.7.3)': dependencies: '@remix-run/router': 1.21.0 '@types/cookie': 0.6.0 @@ -5843,7 +6059,7 @@ snapshots: source-map: 0.7.4 turbo-stream: 2.4.0 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 '@rollup/pluginutils@5.1.3(rollup@4.28.0)': dependencies: @@ -5989,10 +6205,6 @@ snapshots: dependencies: '@types/react': 18.3.12 - '@types/react-reconciler@0.28.8': - dependencies: - '@types/react': 18.3.12 - '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 @@ -6007,13 +6219,13 @@ snapshots: '@types/webextension-polyfill@0.10.0': {} - '@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/scope-manager': 6.0.0 - '@typescript-eslint/type-utils': 6.0.0(eslint@8.57.1)(typescript@5.4.5) - '@typescript-eslint/utils': 6.0.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/type-utils': 6.0.0(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/utils': 6.0.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/visitor-keys': 6.0.0 debug: 4.3.7 eslint: 8.57.1 @@ -6023,119 +6235,53 @@ snapshots: natural-compare: 1.4.0 natural-compare-lite: 1.4.0 semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.4.5) + ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 6.0.0 - '@typescript-eslint/type-utils': 6.0.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/utils': 6.0.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 6.0.0 - debug: 4.3.7 - eslint: 8.57.1 - grapheme-splitter: 1.0.4 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - natural-compare-lite: 1.4.0 - semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.4.5) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.7.3) '@typescript-eslint/visitor-keys': 7.18.0 eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5)': - dependencies: - '@typescript-eslint/scope-manager': 6.0.0 - '@typescript-eslint/types': 6.0.0 - '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 6.0.0 - debug: 4.3.7 - eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 6.0.0 '@typescript-eslint/types': 6.0.0 - '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 6.0.0 debug: 4.3.7 eslint: 8.57.1 optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5)': + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.3.7 eslint: 8.57.1 optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7 - eslint: 8.57.1 - optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -6154,51 +6300,27 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/type-utils@6.0.0(eslint@8.57.1)(typescript@5.4.5)': - dependencies: - '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.5) - '@typescript-eslint/utils': 6.0.0(eslint@8.57.1)(typescript@5.4.5) - debug: 4.3.7 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@6.0.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.6.3) - '@typescript-eslint/utils': 6.0.0(eslint@8.57.1)(typescript@5.6.3) - debug: 4.3.7 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.4.5)': + '@typescript-eslint/type-utils@6.0.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.7.3) + '@typescript-eslint/utils': 6.0.0(eslint@8.57.1)(typescript@5.7.3) debug: 4.3.7 eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.4.5) + ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.6.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.7.3) debug: 4.3.7 eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.6.3) + ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -6208,7 +6330,7 @@ snapshots: '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5)': + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 @@ -6216,41 +6338,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.4.5) + tsutils: 3.21.0(typescript@5.7.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@5.62.0(typescript@5.6.3)': - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.7 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.6.3 - tsutils: 3.21.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@6.0.0(typescript@5.4.5)': - dependencies: - '@typescript-eslint/types': 6.0.0 - '@typescript-eslint/visitor-keys': 6.0.0 - debug: 4.3.7 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.4.5) - optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@6.0.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@6.0.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 6.0.0 '@typescript-eslint/visitor-keys': 6.0.0 @@ -6258,28 +6352,13 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.4.5)': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.4.5) + ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 @@ -6288,20 +6367,20 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.4.3(typescript@5.6.3) + ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.4.5)': + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.7.3) eslint: 8.57.1 eslint-scope: 5.1.1 semver: 7.6.3 @@ -6309,29 +6388,14 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - eslint: 8.57.1 - eslint-scope: 5.1.1 - semver: 7.6.3 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/utils@6.0.0(eslint@8.57.1)(typescript@5.4.5)': + '@typescript-eslint/utils@6.0.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) '@types/json-schema': 7.0.15 '@types/semver': 7.5.8 '@typescript-eslint/scope-manager': 6.0.0 '@typescript-eslint/types': 6.0.0 - '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.7.3) eslint: 8.57.1 eslint-scope: 5.1.1 semver: 7.6.3 @@ -6339,38 +6403,12 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.0.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 6.0.0 - '@typescript-eslint/types': 6.0.0 - '@typescript-eslint/typescript-estree': 6.0.0(typescript@5.6.3) - eslint: 8.57.1 - eslint-scope: 5.1.1 - semver: 7.6.3 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.4.5)': + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.6.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color @@ -6393,21 +6431,21 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vercel/analytics@1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@vercel/analytics@1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': optionalDependencies: - '@remix-run/react': 2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.6.3) + '@remix-run/react': 2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) next: 15.0.3(@babel/core@7.26.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 - '@vercel/analytics@1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + '@vercel/analytics@1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': optionalDependencies: - '@remix-run/react': 2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) + '@remix-run/react': 2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3) next: 15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react: 19.0.0-rc-66855b96-20241106 - '@vercel/analytics@1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1))(react@19.0.0-rc.1)': + '@vercel/analytics@1.4.1(@remix-run/react@2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1))(react@19.0.0-rc.1)': optionalDependencies: - '@remix-run/react': 2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.6.3) + '@remix-run/react': 2.15.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)(typescript@5.7.3) next: 15.0.3(@babel/core@7.26.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1) react: 19.0.0-rc.1 @@ -6421,32 +6459,32 @@ snapshots: next: 15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react: 19.0.0-rc-66855b96-20241106 - '@vercel/style-guide@6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.4.5)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0))': + '@vercel/style-guide@6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.7.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@8.57.1) '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5) - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.7.3) eslint-config-prettier: 9.1.0(eslint@8.57.1) eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) - eslint-plugin-playwright: 1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1) + eslint-plugin-playwright: 1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) - eslint-plugin-testing-library: 6.5.0(eslint@8.57.1)(typescript@5.4.5) + eslint-plugin-testing-library: 6.5.0(eslint@8.57.1)(typescript@5.7.3) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-unicorn: 51.0.1(eslint@8.57.1) - eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0)) + eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0)) prettier-plugin-packagejson: 2.5.6(prettier@3.3.3) optionalDependencies: eslint: 8.57.1 prettier: 3.3.3 - typescript: 5.4.5 + typescript: 5.7.3 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -6454,32 +6492,32 @@ snapshots: - supports-color - vitest - '@vercel/style-guide@6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.6.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0))': + '@vercel/style-guide@6.0.0(eslint@8.57.1)(prettier@3.3.3)(typescript@5.7.3)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0))': dependencies: '@babel/core': 7.26.0 '@babel/eslint-parser': 7.25.9(@babel/core@7.26.0)(eslint@8.57.1) '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.7.3) eslint-config-prettier: 9.1.0(eslint@8.57.1) eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.31.0) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) - eslint-plugin-playwright: 1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1) + eslint-plugin-playwright: 1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) - eslint-plugin-testing-library: 6.5.0(eslint@8.57.1)(typescript@5.6.3) + eslint-plugin-testing-library: 6.5.0(eslint@8.57.1)(typescript@5.7.3) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-unicorn: 51.0.1(eslint@8.57.1) - eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0)) + eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0)) prettier-plugin-packagejson: 2.5.6(prettier@3.3.3) optionalDependencies: eslint: 8.57.1 prettier: 3.3.3 - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -6498,6 +6536,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@4.3.4(vite@5.4.3(@types/node@22.10.2)(terser@5.36.0))': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.4.3(@types/node@22.10.2)(terser@5.36.0) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-react@4.3.4(vite@6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1))': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@1.0.0': dependencies: '@vitest/spy': 1.0.0 @@ -6744,7 +6804,7 @@ snapshots: bippy@0.0.21: {} - bippy@0.0.25: {} + bippy@0.2.0: {} bl@4.1.0: dependencies: @@ -7405,6 +7465,34 @@ snapshots: '@esbuild/win32-ia32': 0.24.0 '@esbuild/win32-x64': 0.24.0 + esbuild@0.24.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + escalade@3.2.0: {} escape-goat@4.0.0: {} @@ -7413,26 +7501,21 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.6.4(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - semver: 7.6.3 - - eslint-config-next@15.0.3(eslint@8.57.1)(typescript@5.6.3): + eslint-config-next@15.0.3(eslint@8.57.1)(typescript@5.7.3): dependencies: '@next/eslint-plugin-next': 15.0.3 '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0(eslint@8.57.1) optionalDependencies: - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - eslint-import-resolver-webpack - eslint-plugin-import-x @@ -7444,7 +7527,7 @@ snapshots: eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.31.0): dependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) eslint-import-resolver-node@0.3.9: dependencies: @@ -7454,19 +7537,19 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.3.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -7485,43 +7568,26 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-json-compat-utils@0.2.1(eslint@8.57.1)(jsonc-eslint-parser@2.4.0): - dependencies: - eslint: 8.57.1 - esquery: 1.6.0 - jsonc-eslint-parser: 2.4.0 - - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.6.3) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) @@ -7534,7 +7600,7 @@ snapshots: eslint: 8.57.1 ignore: 5.3.2 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7545,7 +7611,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7557,13 +7623,13 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7574,7 +7640,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7586,75 +7652,22 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.0.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.15.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - string.prototype.trimend: 1.0.8 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5): - dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.4.5) - eslint: 8.57.1 - optionalDependencies: - '@typescript-eslint/eslint-plugin': 6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5) - transitivePeerDependencies: - - supports-color - - typescript - - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3): dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 optionalDependencies: - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-jsonc@2.18.2(eslint@8.57.1): - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) - eslint: 8.57.1 - eslint-compat-utils: 0.6.4(eslint@8.57.1) - eslint-json-compat-utils: 0.2.1(eslint@8.57.1)(jsonc-eslint-parser@2.4.0) - espree: 9.6.1 - graphemer: 1.4.0 - jsonc-eslint-parser: 2.4.0 - natural-compare: 1.4.0 - synckit: 0.6.2 - transitivePeerDependencies: - - '@eslint/json' - eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): dependencies: aria-query: 5.3.2 @@ -7674,12 +7687,12 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.1 - eslint-plugin-playwright@1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1): + eslint-plugin-playwright@1.8.3(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1): dependencies: eslint: 8.57.1 globals: 13.24.0 optionalDependencies: - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): dependencies: @@ -7711,23 +7724,9 @@ snapshots: string.prototype.matchall: 4.0.11 string.prototype.repeat: 1.0.0 - eslint-plugin-tailwindcss@3.17.5(tailwindcss@3.4.17): - dependencies: - fast-glob: 3.3.2 - postcss: 8.4.49 - tailwindcss: 3.4.17 - - eslint-plugin-testing-library@6.5.0(eslint@8.57.1)(typescript@5.4.5): - dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.4.5) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - eslint-plugin-testing-library@6.5.0(eslint@8.57.1)(typescript@5.6.3): + eslint-plugin-testing-library@6.5.0(eslint@8.57.1)(typescript@5.7.3): dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 transitivePeerDependencies: - supports-color @@ -7760,24 +7759,24 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0)): + eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0)): dependencies: - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.4.5) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 optionalDependencies: - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.4.5))(eslint@8.57.1)(typescript@5.4.5) - vitest: 1.0.0(@types/node@22.10.2)(terser@5.36.0) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + vitest: 1.0.0(@types/node@20.17.10)(terser@5.36.0) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)(vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0)): + eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3)(vitest@1.0.0(@types/node@22.10.2)(terser@5.36.0)): dependencies: - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.7.3) eslint: 8.57.1 optionalDependencies: - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3) - vitest: 1.0.0(@types/node@20.17.10)(terser@5.36.0) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.7.3))(eslint@8.57.1)(typescript@5.7.3) + vitest: 1.0.0(@types/node@22.10.2)(terser@5.36.0) transitivePeerDependencies: - supports-color - typescript @@ -8479,13 +8478,6 @@ snapshots: json5@2.2.3: {} - jsonc-eslint-parser@2.4.0: - dependencies: - acorn: 8.14.0 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - semver: 7.6.3 - jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -9215,12 +9207,6 @@ snapshots: react-is@18.3.1: {} - react-reconciler@0.29.2(react@18.2.0): - dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.2 - react-refresh@0.14.2: {} react-router-dom@6.28.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): @@ -9298,7 +9284,7 @@ snapshots: mri: 1.2.0 playwright: 1.49.0 - react-scan@0.0.48(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react-router-dom@6.28.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-router@6.28.0(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.28.0): + react-scan@0.0.48(@remix-run/react@2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3))(next@15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react-router-dom@6.28.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-router@6.28.0(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.28.0): dependencies: '@babel/core': 7.26.0 '@babel/generator': 7.26.2 @@ -9319,7 +9305,7 @@ snapshots: react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) tsx: 4.0.0 optionalDependencies: - '@remix-run/react': 2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) + '@remix-run/react': 2.15.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.7.3) next: 15.0.3(@babel/core@7.26.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-router: 6.28.0(react@19.0.0-rc-66855b96-20241106) react-router-dom: 6.28.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) @@ -9849,10 +9835,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.6.2: - dependencies: - tslib: 2.8.1 - synckit@0.9.2: dependencies: '@pkgr/core': 0.1.1 @@ -9945,19 +9927,15 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@1.4.3(typescript@5.4.5): - dependencies: - typescript: 5.4.5 - - ts-api-utils@1.4.3(typescript@5.6.3): + ts-api-utils@1.4.3(typescript@5.7.3): dependencies: - typescript: 5.6.3 + typescript: 5.7.3 ts-interface-checker@0.1.13: {} - tsconfck@3.1.4(typescript@5.4.5): + tsconfck@3.1.4(typescript@5.7.3): optionalDependencies: - typescript: 5.4.5 + typescript: 5.7.3 tsconfig-paths@3.15.0: dependencies: @@ -9970,7 +9948,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.2.4(jiti@1.21.6)(postcss@8.4.49)(tsx@4.0.0)(typescript@5.6.3)(yaml@2.6.1): + tsup@8.2.4(jiti@1.21.6)(postcss@8.4.49)(tsx@4.0.0)(typescript@5.7.3)(yaml@2.6.1): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) cac: 6.7.14 @@ -9990,22 +9968,17 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: postcss: 8.4.49 - typescript: 5.6.3 + typescript: 5.7.3 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - tsutils@3.21.0(typescript@5.4.5): + tsutils@3.21.0(typescript@5.7.3): dependencies: tslib: 1.14.1 - typescript: 5.4.5 - - tsutils@3.21.0(typescript@5.6.3): - dependencies: - tslib: 1.14.1 - typescript: 5.6.3 + typescript: 5.7.3 tsx@4.0.0: dependencies: @@ -10076,9 +10049,7 @@ snapshots: typedarray@0.0.6: {} - typescript@5.4.5: {} - - typescript@5.6.3: {} + typescript@5.7.3: {} ufo@1.5.4: {} @@ -10200,7 +10171,7 @@ snapshots: - rollup - supports-color - vite-plugin-web-extension@4.3.1(@types/node@22.10.2)(terser@5.36.0): + vite-plugin-web-extension@4.4.3(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0): dependencies: ajv: 8.17.1 async-lock: 1.4.1 @@ -10210,13 +10181,15 @@ snapshots: lodash.uniq: 4.5.0 lodash.uniqby: 4.7.0 md5: 2.3.0 - vite: 5.4.3(@types/node@22.10.2)(terser@5.36.0) + vite: 6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1) + web-ext-option-types: 8.3.1 web-ext-run: 0.2.2 webextension-polyfill: 0.10.0 yaml: 2.6.1 transitivePeerDependencies: - '@types/node' - bufferutil + - jiti - less - lightningcss - sass @@ -10225,15 +10198,16 @@ snapshots: - sugarss - supports-color - terser + - tsx - utf-8-validate - vite-tsconfig-paths@5.1.4(typescript@5.4.5)(vite@5.4.3(@types/node@22.10.2)(terser@5.36.0)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1)): dependencies: debug: 4.3.7 globrex: 0.1.2 - tsconfck: 3.1.4(typescript@5.4.5) + tsconfck: 3.1.4(typescript@5.7.3) optionalDependencies: - vite: 5.4.3(@types/node@22.10.2)(terser@5.36.0) + vite: 6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -10258,6 +10232,18 @@ snapshots: fsevents: 2.3.3 terser: 5.36.0 + vite@6.0.7(@types/node@22.10.2)(jiti@1.21.6)(terser@5.36.0)(yaml@2.6.1): + dependencies: + esbuild: 0.24.2 + postcss: 8.4.49 + rollup: 4.28.0 + optionalDependencies: + '@types/node': 22.10.2 + fsevents: 2.3.3 + jiti: 1.21.6 + terser: 5.36.0 + yaml: 2.6.1 + vitest@1.0.0(@types/node@20.17.10)(terser@5.36.0): dependencies: '@vitest/expect': 1.0.0 @@ -10334,6 +10320,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + web-ext-option-types@8.3.1: {} + web-ext-run@0.2.2: dependencies: '@babel/runtime': 7.24.7 diff --git a/scripts/bump-version.js b/scripts/bump-version.js index 53eca096..6e2294b2 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'node:fs'; +import path from 'node:path'; // Read the current version from scan package.json const scanPackagePath = path.join(__dirname, '../packages/scan/package.json'); @@ -7,13 +7,14 @@ const scanPackage = JSON.parse(fs.readFileSync(scanPackagePath, 'utf8')); // Bump patch version const version = scanPackage.version.split('.'); -version[2] = parseInt(version[2]) + 1; +version[2] = Number.parseInt(version[2]) + 1; const newVersion = version.join('.'); // Update the version in package.json scanPackage.version = newVersion; // Write back to package.json -fs.writeFileSync(scanPackagePath, JSON.stringify(scanPackage, null, 2) + '\n'); +fs.writeFileSync(scanPackagePath, `${JSON.stringify(scanPackage, null, 2)}\n`); -console.log(`Bumped version to ${newVersion}`); \ No newline at end of file +// biome-ignore lint/suspicious/noConsole: Intended debug output +console.log(`Bumped version to ${newVersion}`); diff --git a/scripts/version-warning.mjs b/scripts/version-warning.mjs index 5c26e1a7..3a396800 100755 --- a/scripts/version-warning.mjs +++ b/scripts/version-warning.mjs @@ -1,8 +1,8 @@ import { readFileSync, readdirSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import chalk from 'chalk'; import boxen from 'boxen'; +import chalk from 'chalk'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -14,17 +14,17 @@ const styles = { arrow: chalk.hex('#C4B5FD'), version: chalk.hex('#C4B5FD'), border: '#503C9B', - dim: chalk.hex('#A1A1AA') + dim: chalk.hex('#A1A1AA'), }; const MESSAGES = { workspace: { header: '📦 Workspace Packages', - text: 'Make sure to bump versions if publishing' + text: 'Make sure to bump versions if publishing', }, package: { - text: 'Make sure to bump version if publishing' - } + text: 'Make sure to bump version if publishing', + }, }; function getWorkspacePackages() { @@ -32,8 +32,9 @@ function getWorkspacePackages() { const packages = {}; try { - const dirs = readdirSync(packagesDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory()); + const dirs = readdirSync(packagesDir, { withFileTypes: true }).filter( + (dirent) => dirent.isDirectory(), + ); for (const dir of dirs) { const pkgPath = resolve(packagesDir, dir.name, 'package.json'); @@ -43,14 +44,14 @@ function getWorkspacePackages() { packages[pkg.name] = pkg.version; } } catch (err) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error(`Error reading ${dir.name}/package.json:`, err); } } return packages; } catch (err) { - // eslint-disable-next-line no-console + // biome-ignore lint/suspicious/noConsole: Intended debug output console.error('Error reading packages directory:', err); process.exit(1); } @@ -60,7 +61,8 @@ function getPackageInfo() { const cwd = process.cwd(); const isWorkspacePackage = cwd.includes('packages/'); const isRootDir = cwd === resolve(__dirname, '..'); - const isDirectPackageBuild = isWorkspacePackage && !process.env.WORKSPACE_BUILD; + const isDirectPackageBuild = + isWorkspacePackage && !process.env.WORKSPACE_BUILD; if (isDirectPackageBuild) { const pkgPath = resolve(cwd, 'package.json'); @@ -71,7 +73,7 @@ function getPackageInfo() { if (isRootDir) { return { name: 'Workspace Packages', - versions: getWorkspacePackages() + versions: getWorkspacePackages(), }; } @@ -81,23 +83,27 @@ function getPackageInfo() { const pkgInfo = getPackageInfo(); const message = pkgInfo.versions - ? `${styles.text(MESSAGES.workspace.text)}\n\n${styles.header(MESSAGES.workspace.header)}\n${Object.entries(pkgInfo.versions).sort(([a], [b]) => a.localeCompare(b)) - .map(([pkg, version], index, array) => { - const prevPkg = index > 0 ? array[index - 1][0] : ''; - const needsSpace = prevPkg.startsWith('@') && pkg === 'react-scan'; - return `${needsSpace ? '\n' : ''}${styles.dim(pkg.padEnd(32))}${styles.version(`v${version}`)}`; - }) - .join('\n') - }` + ? `${styles.text(MESSAGES.workspace.text)}\n\n${styles.header(MESSAGES.workspace.header)}\n${Object.entries( + pkgInfo.versions, + ) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([pkg, version], index, array) => { + const prevPkg = index > 0 ? array[index - 1][0] : ''; + const needsSpace = prevPkg.startsWith('@') && pkg === 'react-scan'; + return `${needsSpace ? '\n' : ''}${styles.dim(pkg.padEnd(32))}${styles.version(`v${version}`)}`; + }) + .join('\n')}` : `${styles.text(MESSAGES.package.text)}\n\n${styles.dim(pkgInfo.name.padEnd(32))}${styles.version(`v${pkgInfo.version}`)}`; -// eslint-disable-next-line no-console -console.log(boxen(message, { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: styles.border, - title: chalk.hex(styles.title)(`⚡ Version Check`), - titleAlignment: 'left', - float: 'left' -})); +// biome-ignore lint/suspicious/noConsole: Intended debug output +console.log( + boxen(message, { + padding: 1, + margin: 1, + borderStyle: 'round', + borderColor: styles.border, + title: chalk.hex(styles.title)('⚡ Version Check'), + titleAlignment: 'left', + float: 'left', + }), +); diff --git a/scripts/workspace.mjs b/scripts/workspace.mjs index aa9be473..6330583f 100644 --- a/scripts/workspace.mjs +++ b/scripts/workspace.mjs @@ -1,63 +1,62 @@ -/* eslint-disable no-console */ import { execSync, spawn } from 'node:child_process'; const runCommand = (command, filters = []) => { const filterArgs = filters.map((filter) => `--filter ${filter}`).join(' '); - execSync(`WORKSPACE_BUILD=1 pnpm ${filterArgs} ${command}`, { stdio: 'inherit' }); + execSync(`WORKSPACE_BUILD=1 pnpm ${filterArgs} ${command}`, { + stdio: 'inherit', + }); }; const buildAll = () => { runCommand('build', ['react-scan']); runCommand('build', ['./packages/*', '!react-scan']); -} +}; const devAll = () => { // Start scan build with pipe to capture output const scanProcess = spawn('pnpm', ['--filter', 'react-scan', 'dev'], { - stdio: ['inherit', 'pipe', 'inherit'], // Pipe stdout, inherit others + stdio: 'inherit', // Inherit all streams to preserve colors shell: true, - env: { ...process.env, WORKSPACE_BUILD: '1' } + env: { ...process.env, WORKSPACE_BUILD: '1' }, }); - // Forward stdout while watching for build success - let isFirstBuild = true; - scanProcess.stdout?.on('data', (data) => { - process.stdout.write(data); // Forward output - - if (isFirstBuild && data.toString().includes('⚡️ Build success')) { - isFirstBuild = false; - - // Start other processes after initial build - const otherProcess = spawn('pnpm', [ + // Start other processes after a delay to ensure scan builds first + setTimeout(() => { + // Start other processes after initial build + const otherProcess = spawn( + 'pnpm', + [ '--filter', '"./packages/*"', '--filter', '"!react-scan"', '--parallel', - 'dev' - ], { + 'dev', + ], + { stdio: 'inherit', - shell: true - }); - - // Handle Ctrl+C for both processes - process.on('SIGINT', () => { - scanProcess.kill('SIGINT'); - otherProcess.kill('SIGINT'); - process.exit(0); - }); - } - }); -} + shell: true, + }, + ); + + // Handle Ctrl+C for both processes + process.on('SIGINT', () => { + scanProcess.kill('SIGINT'); + otherProcess.kill('SIGINT'); + process.exit(0); + }); + }, 1000); // Wait 1 second before starting other processes +}; const packAll = () => { runCommand('pack', ['react-scan']); runCommand('--parallel pack', ['./packages/*', '!react-scan']); -} +}; // Parse command-line arguments const args = process.argv.slice(2); if (args.includes('build')) buildAll(); else if (args.includes('dev')) devAll(); else if (args.includes('pack')) packAll(); +// biome-ignore lint/suspicious/noConsole: Intended debug output else console.error('Invalid command. Use: node workspace.mjs [build|dev|pack]'); From a2f47a7be8c2cd53a35feba7a2da5c39fd4b17c8 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Thu, 9 Jan 2025 09:00:30 +0200 Subject: [PATCH 02/15] bump extension with react-scan@0.0.55 --- CHROME_EXTENSION_GUIDE.md | 9 ++++++--- packages/extension/README.md | 6 +++--- packages/extension/package.json | 2 +- packages/kitchen-sink/src/index.jsx | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHROME_EXTENSION_GUIDE.md b/CHROME_EXTENSION_GUIDE.md index 77510d73..e8a2e26a 100644 --- a/CHROME_EXTENSION_GUIDE.md +++ b/CHROME_EXTENSION_GUIDE.md @@ -5,7 +5,7 @@ ## Chrome -1. Download the [`chrome-react-scanner-extension-v1.0.1.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. +1. Download the [`chrome-react-scanner-extension-v1.0.2.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. 2. Unzip the file. 3. Open Chrome and navigate to `chrome://extensions/`. 4. Enable "Developer mode" if it is not already enabled. @@ -13,7 +13,7 @@ ## Firefox -1. Download the [`firefox-react-scanner-extension-v1.0.1.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. +1. Download the [`firefox-react-scanner-extension-v1.0.2.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. 2. Unzip the file. 3. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`. 4. Click "Load Temporary Add-on..." @@ -21,7 +21,10 @@ ## Brave -1. Download the [`brave-react-scanner-extension-v1.0.1.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. +1. Download the [`brave-react-scanner-extension-v1.0.2.zip`](https://github.com/aidenybai/react-scan/tree/main/packages/extension/build) file. 2. Unzip the file. 3. Open Brave and navigate to `brave://extensions`. 4. Click "Load unpacked" and select the unzipped folder (or drag the folder into the page). + +> [!NOTE] +> The React Scan browser extension currently uses `react-scan@0.0.55` diff --git a/packages/extension/README.md b/packages/extension/README.md index 228121bf..435c8372 100644 --- a/packages/extension/README.md +++ b/packages/extension/README.md @@ -64,8 +64,8 @@ pnpm pack:all ``` This will create: -- `chrome-react-scanner-extension-v1.0.1.zip` -- `firefox-react-scanner-extension-v1.0.1.zip` -- `brave-react-scanner-extension-v1.0.1.zip` +- `chrome-react-scanner-extension-v1.0.2.zip` +- `firefox-react-scanner-extension-v1.0.2.zip` +- `brave-react-scanner-extension-v1.0.2.zip` in the `build` directory. diff --git a/packages/extension/package.json b/packages/extension/package.json index 3f5eb7ca..b59262a2 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@react-scan/extension", - "version": "1.0.0", + "version": "1.0.2", "private": true, "type": "module", "scripts": { diff --git a/packages/kitchen-sink/src/index.jsx b/packages/kitchen-sink/src/index.jsx index 97d8ac40..86dfe7b6 100644 --- a/packages/kitchen-sink/src/index.jsx +++ b/packages/kitchen-sink/src/index.jsx @@ -1,6 +1,6 @@ import React, { createContext, useState } from 'react'; import ReactDOMClient from 'react-dom/client'; -import { scan } from 'react-scan/dist/index.mjs'; // force production build +import { scan } from 'react-scan'; // force production build import './styles.css'; From 1ff083b5a7121e6e204f288d76764235ca0d2f4e Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Thu, 9 Jan 2025 09:07:05 +0200 Subject: [PATCH 03/15] improve build-extension workflow --- .github/workflows/build-extension.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-extension.yml b/.github/workflows/build-extension.yml index e4590812..d409fb11 100644 --- a/.github/workflows/build-extension.yml +++ b/.github/workflows/build-extension.yml @@ -42,6 +42,7 @@ jobs: run: | pnpm build cd packages/extension + rm -rf build pnpm pack:all - name: Commit changes From d0a06c863569690d4dcfa702765a6302a3ca7fa3 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Thu, 9 Jan 2025 09:31:14 +0200 Subject: [PATCH 04/15] improve extension --- packages/extension/public/rules.json | 20 ----------- packages/extension/src/inject/index.ts | 18 +++++----- packages/extension/src/manifest.json | 46 +++++------------------- packages/extension/src/types/messages.ts | 4 +-- packages/extension/src/utils/helpers.ts | 2 +- 5 files changed, 22 insertions(+), 68 deletions(-) delete mode 100644 packages/extension/public/rules.json diff --git a/packages/extension/public/rules.json b/packages/extension/public/rules.json deleted file mode 100644 index b139047c..00000000 --- a/packages/extension/public/rules.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "id": 1, - "priority": 1, - "description": "Remove CSP headers to allow injecting scripts", - "action": { - "type": "modifyHeaders", - "responseHeaders": [ - { - "header": "content-security-policy", - "operation": "remove" - } - ] - }, - "condition": { - "urlFilter": "*", - "resourceTypes": ["main_frame", "script"] - } - } -] diff --git a/packages/extension/src/inject/index.ts b/packages/extension/src/inject/index.ts index 77214a1a..73f3aae8 100644 --- a/packages/extension/src/inject/index.ts +++ b/packages/extension/src/inject/index.ts @@ -1,10 +1,9 @@ +import { sleep, storageGetItem, storageSetItem } from '@pivanov/utils'; import { broadcast, canLoadReactScan, hasReactFiber } from '../utils/helpers'; -import { sleep, storageGetItem, storageSetItem } from '@pivanov/utils' import { createReactNotAvailableUI, toggleReactIsNotAvailable } from './react-is-not-available'; window.addEventListener('DOMContentLoaded', async () => { - if (!canLoadReactScan) { return; } @@ -16,28 +15,31 @@ window.addEventListener('DOMContentLoaded', async () => { if (!isReactAvailable) { _reactScan.setOptions({ enabled: false, - showToolbar: false + showToolbar: false, }); createReactNotAvailableUI(); } - const isDefaultEnabled = await storageGetItem('react-scan', 'enabled'); + const isDefaultEnabled = await storageGetItem( + 'react-scan', + 'enabled', + ); _reactScan.setOptions({ enabled: !!isDefaultEnabled, - showToolbar: !!isDefaultEnabled + showToolbar: !!isDefaultEnabled, }); broadcast.onmessage = async (type, data) => { if (type === 'react-scan:toggle-state') { broadcast.postMessage('react-scan:react-version', { - version: isReactAvailable + version: isReactAvailable, }); if (isReactAvailable) { const state = data?.state; _reactScan.setOptions({ enabled: state, - showToolbar: state + showToolbar: state, }); void storageSetItem('react-scan', 'enabled', state); } else { @@ -48,7 +50,7 @@ window.addEventListener('DOMContentLoaded', async () => { _reactScan.ReactScanInternals.Store.inspectState.subscribe((state) => { broadcast.postMessage('react-scan:is-focused', { - state: state.kind === 'focused' + state: state.kind === 'focused', }); }); }); diff --git a/packages/extension/src/manifest.json b/packages/extension/src/manifest.json index e0a2e749..ee35bd8d 100644 --- a/packages/extension/src/manifest.json +++ b/packages/extension/src/manifest.json @@ -37,9 +37,7 @@ }, "background": { "{{chrome}}.service_worker": "src/background/index.ts", - "{{firefox}}.scripts": [ - "src/background/index.ts" - ] + "{{firefox}}.scripts": ["src/background/index.ts"] }, "{{chrome}}.permissions": [ "activeTab", @@ -57,58 +55,32 @@ "webRequestBlocking", "" ], - "{{chrome}}.host_permissions": [ - "" - ], - "{{chrome}}.declarative_net_request": { - "rule_resources": [ - { - "id": "react_scan_csp_rules", - "enabled": false, - "path": "rules.json" - } - ] - }, + "{{chrome}}.host_permissions": [""], "{{firefox}}.content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "content_scripts": [ { "matches": [""], - "js": [ - "src/inject/react-scan.ts" - ], + "js": ["src/inject/react-scan.ts"], "run_at": "document_start", "world": "MAIN" }, { - "matches": [ - "" - ], - "js": [ - "src/inject/index.ts" - ], + "matches": [""], + "js": ["src/inject/index.ts"], "run_at": "document_start", "world": "MAIN" }, { "matches": [""], - "js": [ - "src/content/index.ts" - ], + "js": ["src/content/index.ts"], "run_at": "document_start" } ], "{{chrome}}.web_accessible_resources": [ { - "resources": [ - "icon/*" - ], - "matches": [ - "" - ] + "resources": ["icon/*"], + "matches": [""] } ], - "{{firefox}}.web_accessible_resources": [ - "src/content.js", - "icon/*" - ] + "{{firefox}}.web_accessible_resources": ["src/content.js", "icon/*"] } diff --git a/packages/extension/src/types/messages.ts b/packages/extension/src/types/messages.ts index 09310f3c..6851f7b1 100644 --- a/packages/extension/src/types/messages.ts +++ b/packages/extension/src/types/messages.ts @@ -6,9 +6,9 @@ export const BroadcastSchema = z.object({ 'react-scan:is-running', 'react-scan:toggle-state', 'react-scan:react-version', - 'react-scan:is-focused' + 'react-scan:is-focused', ]), - data: z.any().optional() + data: z.any().optional(), }); export type BroadcastMessage = z.infer; diff --git a/packages/extension/src/utils/helpers.ts b/packages/extension/src/utils/helpers.ts index 7f0a3649..fb65640c 100644 --- a/packages/extension/src/utils/helpers.ts +++ b/packages/extension/src/utils/helpers.ts @@ -174,7 +174,7 @@ export const debounce = Promise>( } if (options.trailing !== false) { - timeoutId = window.setTimeout(() => { + timeoutId = setTimeout(() => { isLeadingInvoked = false; timeoutId = undefined; if (lastArg !== undefined) { From a4c410003b9631c145ee7cc718e21558f68fd144 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Thu, 9 Jan 2025 10:42:53 +0200 Subject: [PATCH 05/15] fix getChangedState issue and create getCompositeFiberFromElement for overlay --- .../src/web/components/inspector/index.tsx | 18 +++++++++---- .../web/components/inspector/overlay/utils.ts | 9 ++++++- .../src/web/components/inspector/utils.ts | 25 +++++++++++++++++++ 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index ccd89907..201622f5 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -28,7 +28,7 @@ import { getStateNames, resetStateTracking, } from './overlay/utils'; -import { getCompositeComponentFromElement, getOverrideMethods } from './utils'; +import { getCompositeFiberFromElement, getOverrideMethods } from './utils'; interface InspectorState { fiber: Fiber | null; @@ -1207,9 +1207,13 @@ export const Inspector = constant(() => { }; const unSubState = Store.inspectState.subscribe((state) => { - if (state.kind !== 'focused' || !state.focusedDomElement) return; + if (state.kind !== 'focused' || !state.focusedDomElement) { + clearTimeout(debounceTimer); + cancelAnimationFrame(rafId); + return; + }; - const { parentCompositeFiber } = getCompositeComponentFromElement( + const { parentCompositeFiber } = getCompositeFiberFromElement( state.focusedDomElement, ); if (!parentCompositeFiber) return; @@ -1221,11 +1225,15 @@ export const Inspector = constant(() => { if (isProcessing) return; const inspectState = Store.inspectState.value; - if (inspectState.kind !== 'focused') return; + if (inspectState.kind !== 'focused') { + clearTimeout(debounceTimer); + cancelAnimationFrame(rafId); + return; + } const element = inspectState.focusedDomElement; const { parentCompositeFiber } = - getCompositeComponentFromElement(element); + getCompositeFiberFromElement(element); if (parentCompositeFiber && lastInspectedFiber) { processFiberUpdate(parentCompositeFiber); diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index 0f2a505f..2bd8773f 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -200,7 +200,14 @@ export const getChangedState = (fiber: Fiber): Set => { return changes; }; -const getStateValue = (memoizedState: MemoizedState): unknown => { +interface ExtendedMemoizedState extends MemoizedState { + queue?: { + lastRenderedState: unknown; + } | null; + element?: unknown; +} + +const getStateValue = (memoizedState: ExtendedMemoizedState): unknown => { if (!memoizedState) return undefined; const queue = memoizedState.queue; diff --git a/packages/scan/src/web/components/inspector/utils.ts b/packages/scan/src/web/components/inspector/utils.ts index b8b29e93..4a7b01af 100644 --- a/packages/scan/src/web/components/inspector/utils.ts +++ b/packages/scan/src/web/components/inspector/utils.ts @@ -184,6 +184,31 @@ export const getCompositeComponentFromElement = (element: Element) => { }; }; +export const getCompositeFiberFromElement = (element: Element) => { + const associatedFiber = getNearestFiberFromElement(element); + + if (!associatedFiber) return {}; + const currentAssociatedFiber = isCurrentTree(associatedFiber) + ? associatedFiber + : (associatedFiber.alternate ?? associatedFiber); + const stateNode = getFirstStateNode(currentAssociatedFiber); + if (!stateNode) return {}; + + const anotherRes = getParentCompositeFiber(currentAssociatedFiber); + if (!anotherRes) { + return {}; + } + let [parentCompositeFiber] = anotherRes; + parentCompositeFiber = + (isCurrentTree(parentCompositeFiber) + ? parentCompositeFiber + : parentCompositeFiber.alternate) ?? parentCompositeFiber; + + return { + parentCompositeFiber, + }; +}; + interface PropChange { name: string; value: unknown; From 1a2c07790bbba48911b642def80fe5dfdc694f74 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Fri, 10 Jan 2025 04:10:55 +0200 Subject: [PATCH 06/15] resolve comments + fix flushes --- packages/scan/src/core/monitor/network.ts | 14 +++++++------- packages/scan/src/core/monitor/performance.ts | 2 +- packages/scan/src/core/monitor/utils.ts | 7 +++++-- .../scan/src/web/components/inspector/index.tsx | 4 +--- packages/scan/src/web/utils/outline-worker.ts | 3 ++- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/scan/src/core/monitor/network.ts b/packages/scan/src/core/monitor/network.ts index bea744a7..fa484fe2 100644 --- a/packages/scan/src/core/monitor/network.ts +++ b/packages/scan/src/core/monitor/network.ts @@ -33,13 +33,13 @@ export const flush = async (): Promise => { for (let i = 0; i < interactions.length; i++) { const interaction = interactions[i]; const timeSinceStart = now - interaction.performanceEntry.startTime; - // these interactions were retried enough and should be discarded to avoid mem leak - if (timeSinceStart > 30000) { - // Skip this iteration - } else if (timeSinceStart <= INTERACTION_TIME_TILL_COMPLETED) { - pendingInteractions.push(interaction); - } else { - completedInteractions.push(interaction); + if (timeSinceStart <= 30000) { + // Skip interactions older than 30 seconds to prevent memory leaks + if (timeSinceStart <= INTERACTION_TIME_TILL_COMPLETED) { + pendingInteractions.push(interaction); + } else { + completedInteractions.push(interaction); + } } } diff --git a/packages/scan/src/core/monitor/performance.ts b/packages/scan/src/core/monitor/performance.ts index bcead927..58bba144 100644 --- a/packages/scan/src/core/monitor/performance.ts +++ b/packages/scan/src/core/monitor/performance.ts @@ -155,7 +155,7 @@ const getFirstNamedAncestorCompositeFiber = (element: Element) => { if (!fiber) { continue; } - if (getDisplayName(fiber?.type)) { + if (fiber.type && getDisplayName(fiber.type)) { parentCompositeFiber = fiber; } } diff --git a/packages/scan/src/core/monitor/utils.ts b/packages/scan/src/core/monitor/utils.ts index a42ae4c8..4cc1e091 100644 --- a/packages/scan/src/core/monitor/utils.ts +++ b/packages/scan/src/core/monitor/utils.ts @@ -8,6 +8,10 @@ interface NetworkInformation { }; } +interface ExtendedNavigator extends Navigator { + deviceMemory?: number; +} + const getDeviceType = () => { const userAgent = navigator.userAgent; @@ -103,8 +107,7 @@ export const getSession = async ({ * * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory */ - // @ts-expect-error - deviceMemory is still experimental - const mem = navigator.deviceMemory; // GiB ram + const mem = (navigator as ExtendedNavigator).deviceMemory ?? 0; const gpuRendererPromise = new Promise((resolve) => { onIdle(() => { diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index 201622f5..1077b8de 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -762,13 +762,11 @@ const PropertyElement = ({ useEffect(() => { lastRendered.set(currentPath, value); - const isSameComponentType = lastInspectedFiber?.type === fiber?.type; const isFirstRender = !lastRendered.has(currentPath); const shouldFlash = isChanged && refElement.current && prevValue !== undefined && - !isSameComponentType && !isFirstRender; if (shouldFlash && refElement.current) { @@ -780,7 +778,7 @@ const PropertyElement = ({ flashManager.cleanup(refElement.current); } }; - }, [value, isChanged, currentPath, prevValue, fiber?.type]); + }, [value, isChanged, currentPath, prevValue]); const shouldShowWarning = useMemo(() => { const shouldShowChange = diff --git a/packages/scan/src/web/utils/outline-worker.ts b/packages/scan/src/web/utils/outline-worker.ts index 77a155cb..740d8bd2 100644 --- a/packages/scan/src/web/utils/outline-worker.ts +++ b/packages/scan/src/web/utils/outline-worker.ts @@ -45,7 +45,8 @@ function setupOutlineWorker(): (action: OutlineWorkerAction) => Promise { 'Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace'; let ctx: OffscreenCanvasRenderingContext2D | undefined; - enum Reason { + // biome-ignore lint/suspicious/noConstEnum: TS enums are bloated + const enum Reason { Commit = 0b001, Unstable = 0b010, Unnecessary = 0b100, From 1fa8d27b24200d82f9869aed4f14e460251d8d10 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Fri, 10 Jan 2025 12:38:36 +0200 Subject: [PATCH 07/15] resolve comments --- packages/scan/src/core/monitor/performance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/scan/src/core/monitor/performance.ts b/packages/scan/src/core/monitor/performance.ts index 58bba144..27803d35 100644 --- a/packages/scan/src/core/monitor/performance.ts +++ b/packages/scan/src/core/monitor/performance.ts @@ -155,7 +155,7 @@ const getFirstNamedAncestorCompositeFiber = (element: Element) => { if (!fiber) { continue; } - if (fiber.type && getDisplayName(fiber.type)) { + if (getDisplayName(fiber.type)) { parentCompositeFiber = fiber; } } From c22872b3526b526df5ad02cd1123eff4d88657ab Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Fri, 10 Jan 2025 15:18:39 +0200 Subject: [PATCH 08/15] resolve comments --- biome.json | 3 +- packages/scan/src/core/index.ts | 2 +- packages/scan/src/core/instrumentation.ts | 5 +- packages/scan/src/core/utils.ts | 3 +- .../web/components/inspector/overlay/utils.ts | 52 +++++++++---------- .../src/web/components/inspector/utils.ts | 45 ++++++++-------- 6 files changed, 55 insertions(+), 55 deletions(-) diff --git a/biome.json b/biome.json index 4bc92391..dd696cec 100644 --- a/biome.json +++ b/biome.json @@ -64,7 +64,8 @@ "enabled": true, "indentStyle": "space", "indentWidth": 2, - "lineWidth": 80 + "lineWidth": 80, + "lineEnding": "lf" }, "javascript": { "formatter": { diff --git a/packages/scan/src/core/index.ts b/packages/scan/src/core/index.ts index 90f0e9bd..63c7bd0b 100644 --- a/packages/scan/src/core/index.ts +++ b/packages/scan/src/core/index.ts @@ -733,7 +733,7 @@ export const start = () => { } }; -export const withScan = ( +export const withScan = ( component: ComponentType, options: Options = {}, ) => { diff --git a/packages/scan/src/core/instrumentation.ts b/packages/scan/src/core/instrumentation.ts index e1837496..fc7a5fcc 100644 --- a/packages/scan/src/core/instrumentation.ts +++ b/packages/scan/src/core/instrumentation.ts @@ -54,7 +54,7 @@ export const isElementVisible = (el: Element) => { return ( style.display !== 'none' && style.visibility !== 'hidden' && - style.getPropertyValue('content-visibility') !== 'hidden' && + style.contentVisibility !== 'hidden' && style.opacity !== '0' ); }; @@ -82,7 +82,8 @@ export const isElementInViewport = ( return isVisible && rect.width && rect.height; }; -export enum ChangeReason { +// biome-ignore lint/suspicious/noConstEnum: Using const enum for better performance since it's inlined at compile time and removed from the JS output +export const enum ChangeReason { Props = 0b001, State = 0b010, Context = 0b100, diff --git a/packages/scan/src/core/utils.ts b/packages/scan/src/core/utils.ts index 8bd32137..1f2fdf42 100644 --- a/packages/scan/src/core/utils.ts +++ b/packages/scan/src/core/utils.ts @@ -192,5 +192,6 @@ export interface RenderData { } export function isEqual(a: unknown, b: unknown): boolean { - return a === b || (Number.isNaN(a) && Number.isNaN(b)); + // biome-ignore lint/suspicious/noSelfCompare: reliable way to detect NaN values in JavaScript + return a === b || (a !== a && b !== b); } diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index 2bd8773f..684099c9 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -36,32 +36,21 @@ const ensureRecord = ( value: unknown, seen = new WeakSet(), ): Record => { - if (value === null || value === undefined) { + if (value == null) { return {}; } - - switch (true) { - case value instanceof Element: - return { - type: 'Element', - tagName: (value as Element).tagName.toLowerCase(), - }; - - case typeof value === 'function': - return { - type: 'function', - name: (value as { name?: string }).name || 'anonymous', - }; - - case Boolean( - value && - (value instanceof Promise || - (typeof value === 'object' && 'then' in value)), - ): - return { type: 'promise' }; - - case typeof value === 'object': { - if (seen.has(value as object)) { + switch (typeof value) { + case 'object': + if (value instanceof Element) { + return { + type: 'Element', + tagName: value.tagName.toLowerCase(), + }; + } + if (value instanceof Promise || 'then' in value) { + return { type: 'promise' }; + } + if (seen.has(value)) { return { type: 'circular' }; } @@ -71,11 +60,18 @@ const ensureRecord = ( return { type: 'array', length: value.length, items: safeArray }; } - seen.add(value as object); + seen.add(value); + /* + * biome-ignore lint/correctness/noSwitchDeclarations: + * Performance optimization: + * - Early type-based branching via typeof before expensive instanceof checks + * - Single allocation of result object + * - Avoids redundant checks in switch-case fallthrough + */ const result: Record = {}; try { - const keys = Object.keys(value as object); + const keys = Object.keys(value); for (const key of keys) { try { const val = (value as Record)[key]; @@ -91,8 +87,8 @@ const ensureRecord = ( } catch { return { type: 'object' }; } - } - + case 'function': + return { type: 'function', name: value.name || 'anonymous' }; default: return { value }; } diff --git a/packages/scan/src/web/components/inspector/utils.ts b/packages/scan/src/web/components/inspector/utils.ts index 4a7b01af..5eed8d1c 100644 --- a/packages/scan/src/web/components/inspector/utils.ts +++ b/packages/scan/src/web/components/inspector/utils.ts @@ -327,29 +327,30 @@ export const getOverrideMethods = (): OverrideMethods => { }; const nonVisualTags = new Set([ - 'html', - 'meta', - 'script', - 'link', - 'style', - 'head', - 'title', - 'noscript', - 'base', - 'template', - 'iframe', - 'embed', - 'object', - 'param', - 'source', - 'track', - 'area', - 'portal', - 'slot', - 'xml', - 'doctype', - 'comment', + 'HTML', + 'HEAD', + 'META', + 'TITLE', + 'BASE', + 'SCRIPT', + 'SCRIPT', + 'STYLE', + 'LINK', + 'NOSCRIPT', + 'SOURCE', + 'TRACK', + 'EMBED', + 'OBJECT', + 'PARAM', + 'TEMPLATE', + 'PORTAL', + 'SLOT', + 'AREA', + 'XML', + 'DOCTYPE', + 'COMMENT', ]); + export const findComponentDOMNode = ( fiber: Fiber, excludeNonVisualTags = true, From 6d1c358ff6a4c085715f60078b1c0c125287565a Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Fri, 10 Jan 2025 16:59:55 +0200 Subject: [PATCH 09/15] improvements and small bug fixes --- .../src/web/components/inspector/index.tsx | 196 ++++++++++-------- .../web/components/widget/toolbar/search.tsx | 17 +- 2 files changed, 116 insertions(+), 97 deletions(-) diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index 1077b8de..5fdbf4e3 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -100,13 +100,19 @@ interface EditableValueProps { type IterableEntry = [key: string | number, value: unknown]; -const EXPANDED_PATHS = new Set(); -const lastRendered = new Map(); -let lastInspectedFiber: Fiber | null = null; - const THROTTLE_MS = 16; const DEBOUNCE_MS = 150; +const globalInspectorState = { + lastRendered: new Map(), + expandedPaths: new Set(), + cleanup: () => { + globalInspectorState.lastRendered.clear(); + globalInspectorState.expandedPaths.clear(); + flashManager.cleanupAll(); + } +}; + const inspectorState = signal({ fiber: null, changes: { @@ -185,7 +191,7 @@ const isEditableValue = (value: unknown, parentPath?: string): boolean => { let currentPath = ''; for (const part of parts) { currentPath = currentPath ? `${currentPath}.${part}` : part; - const obj = lastRendered.get(currentPath); + const obj = globalInspectorState.lastRendered.get(currentPath); if ( obj instanceof DataView || obj instanceof ArrayBuffer || @@ -533,8 +539,10 @@ const formatInitialValue = (value: unknown): string => { }; const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { - const inputRef = useRef(null); - const [editValue, setEditValue] = useState(() => { + const refInput = useRef(null); + const [editValue, setEditValue] = useState(''); + + useEffect(() => { let initialValue = ''; try { if (value instanceof Date) { @@ -552,21 +560,19 @@ const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { } else { initialValue = formatInitialValue(value); } - } catch (error) { - // biome-ignore lint/suspicious/noConsole: Intended debug output - console.warn(sanitizeErrorMessage(String(error))); + } catch { initialValue = String(value); } - return sanitizeString(initialValue); - }); + setEditValue(sanitizeString(initialValue)); + }, [value]); useEffect(() => { - inputRef.current?.focus(); + refInput.current?.focus(); if (typeof value === 'string') { - inputRef.current?.setSelectionRange(1, inputRef.current.value.length - 1); + refInput.current?.setSelectionRange(1, refInput.current.value.length - 1); } else { - inputRef.current?.select(); + refInput.current?.select(); } }, [value]); @@ -605,7 +611,7 @@ const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { return ( { const { fiber } = inspectorState.value; - const refElement = useRef(null); const currentPath = getPath( @@ -690,12 +695,45 @@ const PropertyElement = ({ parentPath ?? '', name, ); - const [isExpanded, setIsExpanded] = useState(EXPANDED_PATHS.has(currentPath)); + const [isExpanded, setIsExpanded] = useState(globalInspectorState.expandedPaths.has(currentPath)); const [isEditing, setIsEditing] = useState(false); - const prevValue = lastRendered.get(currentPath); + const prevValue = globalInspectorState.lastRendered.get(currentPath); const isChanged = prevValue !== undefined && !isEqual(prevValue, value); + useEffect(() => { + globalInspectorState.lastRendered.set(currentPath, value); + + const isFirstRender = !globalInspectorState.lastRendered.has(currentPath); + const shouldFlash = + isChanged && + refElement.current && + prevValue !== undefined && + !isFirstRender; + + if (shouldFlash && refElement.current) { + flashManager.create(refElement.current); + } + + return () => { + if (refElement.current) { + flashManager.cleanup(refElement.current); + } + }; + }, [value, isChanged, currentPath, prevValue]); + + const handleToggleExpand = useCallback(() => { + setIsExpanded((prevState: boolean) => { + const newIsExpanded = !prevState; + if (newIsExpanded) { + globalInspectorState.expandedPaths.add(currentPath); + } else { + globalInspectorState.expandedPaths.delete(currentPath); + } + return newIsExpanded; + }); + }, [currentPath]); + const renderNestedProperties = useCallback( (obj: InspectableValue) => { let entries: Array; @@ -759,31 +797,10 @@ const PropertyElement = ({ ); }, [section, overrideProps, overrideHookState, allowEditing, name]); - useEffect(() => { - lastRendered.set(currentPath, value); - - const isFirstRender = !lastRendered.has(currentPath); - const shouldFlash = - isChanged && - refElement.current && - prevValue !== undefined && - !isFirstRender; - - if (shouldFlash && refElement.current) { - flashManager.create(refElement.current); - } - - return () => { - if (refElement.current) { - flashManager.cleanup(refElement.current); - } - }; - }, [value, isChanged, currentPath, prevValue]); - const shouldShowWarning = useMemo(() => { const shouldShowChange = - !lastRendered.has(currentPath) || - !isEqual(lastRendered.get(currentPath), value); + !globalInspectorState.lastRendered.has(currentPath) || + !isEqual(globalInspectorState.lastRendered.get(currentPath), value); const isBadRender = level === 0 && @@ -797,18 +814,6 @@ const PropertyElement = ({ const clipboardText = useMemo(() => formatForClipboard(value), [value]); - const handleToggleExpand = useCallback(() => { - setIsExpanded((state) => { - const newIsExpanded = !state; - if (newIsExpanded) { - EXPANDED_PATHS.add(currentPath); - } else { - EXPANDED_PATHS.delete(currentPath); - } - return newIsExpanded; - }); - }, [currentPath]); - const handleEdit = useCallback(() => { if (canEdit) { setIsEditing(true); @@ -908,8 +913,7 @@ const PropertyElement = ({ {isExpandable(value) && ( - )} +

+ { + isExpandable(value) && ( + + ) + } +
- {shouldShowWarning && ( - - )} + { + isBadRender && + !changedKeys.has(`${name}:memoized`) && + !changedKeys.has(`${name}:unmemoized`) && ( + + ) + } + { + changedKeys.has(`${name}:memoized`) + ? ( + + ) + : ( + changedKeys.has(`${name}:unmemoized`) && ( + + ) + ) + }
{name}:
- {isEditing && isEditableValue(value, parentPath) ? ( - setIsEditing(false)} - /> - ) : ( - - )} + { + isEditing && isEditableValue(value, parentPath) + ? ( + setIsEditing(false)} + /> + ) + : ( + + ) + } <>{ClipboardIcon}}
- {isExpandable(value) && isExpanded && ( -
- {renderNestedProperties(value)} -
- )} +
+ { + isExpandable(value) && ( +
+ {renderNestedProperties(value)} +
+ ) + } +
); @@ -1018,48 +1084,91 @@ const PropertySection = ({ title, section }: PropertySectionProps) => { }; const WhatChanged = constant(() => { + const refPrevFiber = useRef(null); const [isExpanded, setIsExpanded] = useState(Store.wasDetailsOpen.value); const [shouldShow, setShouldShow] = useState(false); + const { changes, fiber } = inspectorState.value; - const refTimer = useRef(); - const refPrevFiber = useRef(null); - const hasChanges = - changes.state.size > 0 || - changes.props.size > 0 || - changes.context.size > 0; + const renderSection = useCallback(( + sectionName: 'state' | 'props' | 'context', + items: Set, + getCount: (key: string) => number, + ) => { + const elements = Array.from(items).reduce((acc, key) => { + if (sectionName === 'props') { + const isUnmemoized = key.endsWith(':unmemoized'); + if (isUnmemoized) { + acc.push( +
  • +
    + {key.split(':')[0]}{' '} + +
    +
  • , + ); + } + } - useEffect(() => { - const cleanup = () => { - clearTimeout(refTimer.current); - setShouldShow(false); + const count = getCount(key); + if (count > 0) { + const displayKey = + sectionName === 'context' ? key.replace(/^context\./, '') : key; + acc.push( +
  • + {displayKey} ×{count} +
  • , + ); + } + + return acc; + }, []); + + if (!elements.length) return null; + + return ( + <> +
    + {sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}: +
    +
      {elements}
    + + ); + }, []); + + const stateSection = useMemo( + () => renderSection('state', changes.state, getStateChangeCount), + [changes.state, renderSection], + ); + + const propsSection = useMemo( + () => renderSection('props', changes.props, getPropsChangeCount), + [changes.props, renderSection], + ); + + const contextSection = useMemo( + () => renderSection('context', changes.context, getContextChangeCount), + [changes.context, renderSection], + ); + + const { hasChanges, sections } = useMemo(() => { + return { + hasChanges: !!(stateSection || propsSection || contextSection), + sections: [stateSection, propsSection, contextSection], }; + }, [stateSection, propsSection, contextSection]); - if (fiber && refPrevFiber.current && fiber.type !== refPrevFiber.current.type) { - cleanup(); + useEffect(() => { + if (!refPrevFiber.current || refPrevFiber.current.type !== fiber?.type) { refPrevFiber.current = fiber; + setShouldShow(false); return; } refPrevFiber.current = fiber; - - if (!hasChanges) { - cleanup(); - return; - } - - clearTimeout(refTimer.current); - refTimer.current = setTimeout(() => { - setShouldShow(true); - }, 32); - - return cleanup; + setShouldShow(hasChanges); }, [hasChanges, fiber]); - if (!hasChanges || !shouldShow) { - return null; - } - const handleToggle = useCallback(() => { setIsExpanded((state) => { Store.wasDetailsOpen.value = !state; @@ -1068,188 +1177,163 @@ const WhatChanged = constant(() => { }, []); return ( - + { + shouldShow && refPrevFiber.current?.type === fiber?.type && ( +
    e.key === 'Enter' && handleToggle()} + className={cn( + 'flex flex-col', + 'px-1 py-2', + 'text-left text-white', + 'bg-yellow-600', + 'overflow-hidden', + 'opacity-0', + 'transition-all duration-300 delay-300', + { + 'opacity-100 delay-0': shouldShow, + }, + )} + > +
    +
    + + + + What changed? +
    +
    +
    +
    {sections}
    +
    +
    + ) + } + ); }); export const Inspector = constant(() => { const refLastInspectedFiber = useRef(null); + const isSettingsOpen = signalIsSettingsOpen.value; + useEffect(() => { - let rafId: ReturnType; - let debounceTimer: ReturnType; - let lastUpdateTime = 0; let isProcessing = false; - let pendingFiber: Fiber | null = null; - let lastInspectedElement: Element | null = null; - - const updateInspectorState = (fiber: Fiber, element: Element) => { - const isNewComponent = !refLastInspectedFiber.current || - refLastInspectedFiber.current.type !== fiber.type; - const isNewElement = element !== lastInspectedElement; - - if (isNewComponent || isNewElement) { - resetStateTracking(); - lastInspectedElement = element; - globalInspectorState.cleanup(); - } + const pendingUpdates = new Set(); + + const updateInspectorState = (fiber: Fiber) => { + refLastInspectedFiber.current = fiber; inspectorState.value = { fiber, changes: { - props: isNewElement ? new Set() : getChangedProps(fiber), - state: isNewElement ? new Set() : getChangedState(fiber), - context: isNewElement ? new Set() : getChangedContext(fiber), + props: getChangedProps(fiber), + state: getChangedState(fiber), + context: getChangedContext(fiber), }, current: { state: getCurrentState(fiber), props: getCurrentProps(fiber), context: getCurrentContext(fiber), - }, + } }; - - refLastInspectedFiber.current = fiber; }; - const processFiberUpdate = (fiber: Fiber, element: Element) => { - const now = Date.now(); - const timeSinceLastUpdate = now - lastUpdateTime; - - clearTimeout(debounceTimer); - cancelAnimationFrame(rafId); - - if (timeSinceLastUpdate < THROTTLE_MS) { - pendingFiber = fiber; - debounceTimer = setTimeout(() => { - rafId = requestAnimationFrame(() => { - if (pendingFiber) { - isProcessing = true; - updateInspectorState(pendingFiber, element); - isProcessing = false; - pendingFiber = null; - lastUpdateTime = Date.now(); - } - }); - }, DEBOUNCE_MS); + const processNextUpdate = () => { + if (pendingUpdates.size === 0) { + isProcessing = false; return; } - rafId = requestAnimationFrame(() => { + const nextFiber = Array.from(pendingUpdates)[0]; + pendingUpdates.delete(nextFiber); + + try { + updateInspectorState(nextFiber); + } finally { + if (pendingUpdates.size > 0) { + queueMicrotask(processNextUpdate); + } else { + isProcessing = false; + } + } + }; + + const processFiberUpdate = (fiber: Fiber) => { + pendingUpdates.add(fiber); + + if (!isProcessing) { isProcessing = true; - updateInspectorState(fiber, element); - isProcessing = false; - lastUpdateTime = now; - }); + queueMicrotask(processNextUpdate); + } }; const unSubState = Store.inspectState.subscribe((state) => { if (state.kind !== 'focused' || !state.focusedDomElement) { - clearTimeout(debounceTimer); - cancelAnimationFrame(rafId); + pendingUpdates.clear(); return; } + if (state.kind === 'focused') { + signalIsSettingsOpen.value = false; + } + const { parentCompositeFiber } = getCompositeFiberFromElement( state.focusedDomElement, ); if (!parentCompositeFiber) return; - processFiberUpdate(parentCompositeFiber, state.focusedDomElement); + pendingUpdates.clear(); + globalInspectorState.cleanup(); + refLastInspectedFiber.current = parentCompositeFiber; + + getChangedProps(parentCompositeFiber); + getChangedState(parentCompositeFiber); + getChangedContext(parentCompositeFiber); + + inspectorState.value = { + fiber: parentCompositeFiber, + changes: { + props: new Set(), + state: new Set(), + context: new Set(), + }, + current: { + state: getCurrentState(parentCompositeFiber), + props: getCurrentProps(parentCompositeFiber), + context: getCurrentContext(parentCompositeFiber), + } + }; + }); const unSubReport = Store.lastReportTime.subscribe(() => { - if (isProcessing) return; - const inspectState = Store.inspectState.value; if (inspectState.kind !== 'focused') { - clearTimeout(debounceTimer); - cancelAnimationFrame(rafId); + pendingUpdates.clear(); return; } @@ -1263,25 +1347,33 @@ export const Inspector = constant(() => { return; } - if (parentCompositeFiber && refLastInspectedFiber.current) { - processFiberUpdate(parentCompositeFiber, element); + if (parentCompositeFiber.type === refLastInspectedFiber.current?.type) { + processFiberUpdate(parentCompositeFiber); } }); return () => { unSubState(); unSubReport(); - clearTimeout(debounceTimer); - cancelAnimationFrame(rafId); - pendingFiber = null; - lastInspectedElement = null; + pendingUpdates.clear(); globalInspectorState.cleanup(); + resetStateTracking(); }; }, []); return ( -
    +
    @@ -1290,6 +1382,7 @@ export const Inspector = constant(() => { ); }); + export const replayComponent = async (fiber: Fiber): Promise => { try { const { overrideProps, overrideHookState } = getOverrideMethods(); diff --git a/packages/scan/src/web/components/inspector/overlay/index.tsx b/packages/scan/src/web/components/inspector/overlay/index.tsx index 1cd93bdb..7676bad3 100644 --- a/packages/scan/src/web/components/inspector/overlay/index.tsx +++ b/packages/scan/src/web/components/inspector/overlay/index.tsx @@ -7,6 +7,7 @@ import { getCompositeComponentFromElement, nonVisualTags, } from '~web/components/inspector/utils'; +import { signalIsSettingsOpen } from '~web/state'; import { cn, throttle } from '~web/utils/helpers'; import { lerp } from '~web/utils/lerp'; @@ -326,8 +327,9 @@ export const ScanOverlay = () => { !refCanvas.current || e.propertyName !== 'opacity' || !refIsFadingOut.current - ) + ) { return; + } refCanvas.current.removeEventListener( 'transitionend', handleTransitionEnd, @@ -335,7 +337,6 @@ export const ScanOverlay = () => { cleanupCanvas(refCanvas.current); onComplete?.(); }; - const existingListener = refCleanupMap.current.get('fade-out'); if (existingListener) { existingListener(); @@ -352,21 +353,31 @@ export const ScanOverlay = () => { refIsFadingOut.current = true; refCanvas.current.classList.remove('fade-in'); - refCanvas.current.classList.add('fade-out'); + requestAnimationFrame(() => { + refCanvas.current?.classList.add('fade-out'); + }); }; const startFadeIn = () => { if (!refCanvas.current) return; - refCanvas.current.classList.remove('fade-out'); - refCanvas.current.classList.add('fade-in'); refIsFadingOut.current = false; + refCanvas.current.classList.remove('fade-out'); + requestAnimationFrame(() => { + refCanvas.current?.classList.add('fade-in'); + }); }; const handleHoverableElement = (componentElement: HTMLElement) => { if (componentElement === refLastHoveredElement.current) return; - startFadeIn(); refLastHoveredElement.current = componentElement; + + if (nonVisualTags.has(componentElement.tagName)) { + startFadeOut(); + } else { + startFadeIn(); + } + Store.inspectState.value = { kind: 'inspecting', hoveredDomElement: componentElement, @@ -509,6 +520,7 @@ export const ScanOverlay = () => { } case 'inspecting': { startFadeOut(() => { + signalIsSettingsOpen.value = false; Store.inspectState.value = { kind: 'inspect-off', }; @@ -539,6 +551,7 @@ export const ScanOverlay = () => { switch (state.kind) { case 'inspect-off': + startFadeOut(); return; case 'inspecting': diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index 684099c9..63fe9fe2 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -148,12 +148,8 @@ export const isDirectComponent = (fiber: Fiber): boolean => { export const getCurrentState = (fiber: Fiber | null) => { if (!fiber) return {}; - try { - if (fiber.tag === FunctionComponentTag && isDirectComponent(fiber)) { - return getCurrentFiberState(fiber) ?? {}; - } - } catch { - // Silently fail + if (fiber.tag === FunctionComponentTag && isDirectComponent(fiber)) { + return getCurrentFiberState(fiber) ?? {}; } return {}; }; @@ -273,42 +269,50 @@ export const getCurrentProps = (fiber: Fiber): Record => { fiber.alternate?.pendingProps || fiber.memoizedProps; - return { ...baseProps }; -}; - -export const getChangedProps = (fiber: Fiber): Set => { - const changes = new Set(); - if (!fiber.alternate) return changes; - - const previousProps = fiber.alternate.memoizedProps ?? {}; - const currentProps = fiber.memoizedProps ?? {}; + const result: Record = {}; - const propsOrder = getPropsOrder(fiber); - const orderedProps = [...propsOrder, ...Object.keys(currentProps)]; - const uniqueOrderedProps = [...new Set(orderedProps)]; + for (const [key, value] of Object.entries(baseProps)) { + result[key] = value; + if ((value && typeof value === 'object') || typeof value === 'function') { + if (fiber.alternate?.memoizedProps) { + const prevValue = fiber.alternate.memoizedProps[key]; + const status = value === prevValue ? 'memoized' : 'unmemoized'; + propsChangeCounts.set(`${key}:${status}`, 0); + } + } + } - for (const key of uniqueOrderedProps) { - if (key === 'children') continue; - if (!(key in currentProps)) continue; + return result; +}; - const currentValue = currentProps[key]; - const previousValue = previousProps[key]; +export const getChangedProps = (fiber: Fiber | null): Set => { + if (!fiber?.memoizedProps) return new Set(); - if (!isEqual(currentValue, previousValue)) { - changes.add(key); + const currentProps = fiber.memoizedProps; + const changes = new Set(); - if (typeof currentValue !== 'function') { - const count = (propsChangeCounts.get(key) ?? 0) + 1; - propsChangeCounts.set(key, count); + for (const [key, currentValue] of Object.entries(currentProps)) { + // Track memoization for non-primitive values (functions, objects, arrays) + if ( + (currentValue && typeof currentValue === 'object') || + typeof currentValue === 'function' + ) { + if (fiber.alternate?.memoizedProps) { + const prevValue = fiber.alternate.memoizedProps[key]; + const status = currentValue === prevValue ? 'memoized' : 'unmemoized'; + changes.add(`${key}:${status}`); } + continue; } - } - for (const key in previousProps) { - if (key === 'children') continue; - if (!(key in currentProps)) { + // Track changes for primitive values + if ( + fiber.alternate?.memoizedProps && + key in fiber.alternate.memoizedProps && + !isEqual(fiber.alternate.memoizedProps[key], currentValue) + ) { changes.add(key); - const count = (propsChangeCounts.get(key) ?? 0) + 1; + const count = (propsChangeCounts.get(key) || 0) + 1; propsChangeCounts.set(key, count); } } diff --git a/packages/scan/src/web/components/toggle/index.tsx b/packages/scan/src/web/components/toggle/index.tsx new file mode 100644 index 00000000..dc457525 --- /dev/null +++ b/packages/scan/src/web/components/toggle/index.tsx @@ -0,0 +1,26 @@ +import type { JSX } from 'preact'; +import { cn } from '~web/utils/helpers'; + +type ToggleProps = Omit, 'className' | 'onChange'> & { + checked: boolean; + onChange: ((e: Event) => void); +}; + +export const Toggle = ({ + checked, + onChange, + class: className, + ...props +}: ToggleProps) => { + return ( +
    + +
    +
    + ); +}; diff --git a/packages/scan/src/web/components/widget/header.tsx b/packages/scan/src/web/components/widget/header.tsx index 9dd250f6..777ef3f5 100644 --- a/packages/scan/src/web/components/widget/header.tsx +++ b/packages/scan/src/web/components/widget/header.tsx @@ -1,7 +1,9 @@ import { getDisplayName } from 'bippy'; -import { useEffect, useRef } from 'preact/hooks'; +import { useEffect, useRef, useState } from 'preact/hooks'; import { Store } from '~core/index'; import { replayComponent } from '~web/components/inspector'; +import { signalIsSettingsOpen } from '~web/state'; +import { cn } from '~web/utils/helpers'; import { Icon } from '../icon'; import { getCompositeComponentFromElement, @@ -19,11 +21,22 @@ export const BtnReplay = () => { }, }); - const { overrideProps, overrideHookState } = getOverrideMethods(); - const canEdit = !!overrideProps; + const [canEdit, setCanEdit] = useState(false); + const isSettingsOpen = signalIsSettingsOpen.value; + + useEffect(() => { + const { overrideProps } = getOverrideMethods(); + const canEdit = !!overrideProps; + + requestAnimationFrame(() => { + setCanEdit(canEdit); + }); + }, []); + const handleReplay = (e: MouseEvent) => { e.stopPropagation(); + const { overrideProps, overrideHookState } = getOverrideMethods(); const state = replayState.current; const button = e.currentTarget as HTMLElement; @@ -60,8 +73,13 @@ export const BtnReplay = () => { @@ -86,11 +104,13 @@ const useSubscribeFocusedFiber = (onUpdate: () => void) => { }, []); }; -export const Header = () => { +const HeaderInspect = () => { const refRaf = useRef(null); const refComponentName = useRef(null); const refMetrics = useRef(null); + const isSettingsOpen = signalIsSettingsOpen.value; + useSubscribeFocusedFiber(() => { cancelAnimationFrame(refRaf.current ?? 0); refRaf.current = requestAnimationFrame(() => { @@ -119,7 +139,52 @@ export const Header = () => { }); }); + return ( +
    + + +
    + ); +}; + +const HeaderSettings = () => { + const isSettingsOpen = signalIsSettingsOpen.value; + return ( + + ); +}; + +export const Header = () => { const handleClose = () => { + if (signalIsSettingsOpen.value) { + signalIsSettingsOpen.value = false; + return; + } + Store.inspectState.value = { kind: 'inspect-off', }; @@ -127,13 +192,11 @@ export const Header = () => { return (
    - - - +
    + + +
    + {Store.inspectState.value.kind !== 'inspect-off' && }
    -
    - -
    +
    diff --git a/packages/scan/src/web/components/widget/settings.tsx b/packages/scan/src/web/components/widget/settings.tsx new file mode 100644 index 00000000..eef20837 --- /dev/null +++ b/packages/scan/src/web/components/widget/settings.tsx @@ -0,0 +1,106 @@ +import { useCallback } from 'preact/hooks'; +import { ReactScanInternals, setOptions } from '~core/index'; +import { Toggle } from '~web/components/toggle'; +import { useDelayedValue } from '~web/hooks/use-mount-delay'; +import { signalIsSettingsOpen } from '~web/state'; +import { cn } from '~web/utils/helpers'; + +export const Settings = () => { + const isSettingsOpen = signalIsSettingsOpen.value; + const isMounted = useDelayedValue(isSettingsOpen, 0, 1000); + + const onSoundToggle = useCallback(() => { + const newSoundState = !ReactScanInternals.options.value.playSound; + setOptions({ playSound: newSoundState }); + }, []); + + const onToggle = useCallback((e: Event) => { + const target = e.currentTarget as HTMLInputElement; + const type = target.dataset.type; + const value = target.checked; + + if (type) { + setOptions({ [type]: value }); + } + }, []); + + return ( +
    + {isMounted && ( + <> +
    + Play Sound + +
    + +
    + Include Children + +
    + +
    + Log renders to the console + +
    + +
    + Report data to getReport() + +
    + +
    + Always show labels + +
    + +
    + Show labels on hover + +
    + +
    + Track unnecessary renders + +
    + + )} +
    + ); +}; diff --git a/packages/scan/src/web/components/widget/toolbar/arrows.tsx b/packages/scan/src/web/components/widget/toolbar/arrows.tsx index 8647b688..24e45ed5 100644 --- a/packages/scan/src/web/components/widget/toolbar/arrows.tsx +++ b/packages/scan/src/web/components/widget/toolbar/arrows.tsx @@ -5,6 +5,7 @@ import { type InspectableElement, getInspectableElements, } from '~web/components/inspector/utils'; +import { useDelayedValue } from '~web/hooks/use-mount-delay'; import { cn } from '~web/utils/helpers'; import { constant } from '~web/utils/preact/constant'; @@ -14,6 +15,7 @@ export const Arrows = constant(() => { const refAllElements = useRef>([]); const [shouldRender, setShouldRender] = useState(false); + const isMounted = useDelayedValue(shouldRender, 0, 1000); const findNextElement = useCallback( (currentElement: HTMLElement, direction: 'next' | 'previous') => { @@ -64,39 +66,59 @@ export const Arrows = constant(() => { // biome-ignore lint/correctness/useExhaustiveDependencies: no deps useEffect(() => { const unsubscribe = Store.inspectState.subscribe((state) => { - if (state.kind === 'focused') { + + if (state.kind === 'focused' && refButtonPrevious.current && refButtonNext.current) { refAllElements.current = getInspectableElements(); + + const hasPrevious = !!findNextElement( + state.focusedDomElement, + 'previous', + ); + refButtonPrevious.current.classList.toggle( + 'opacity-50', + !hasPrevious, + ); + refButtonPrevious.current.classList.toggle( + 'cursor-not-allowed', + !hasPrevious, + ); + + const hasNext = !!findNextElement(state.focusedDomElement, 'next'); + refButtonNext.current.classList.toggle( + 'opacity-50', + !hasNext, + ); + refButtonNext.current.classList.toggle( + 'cursor-not-allowed', + !hasNext, + ); + setShouldRender(true); - if (refButtonPrevious.current) { - const hasPrevious = !!findNextElement( - state.focusedDomElement, - 'previous', - ); - refButtonPrevious.current.classList.toggle( - 'opacity-50', - !hasPrevious, - ); - refButtonPrevious.current.classList.toggle( - 'cursor-not-allowed', - !hasPrevious, - ); - } - if (refButtonNext.current) { - const hasNext = !!findNextElement(state.focusedDomElement, 'next'); - refButtonNext.current.classList.toggle('opacity-50', !hasNext); - refButtonNext.current.classList.toggle( - 'cursor-not-allowed', - !hasNext, - ); - } } - if (state.kind === 'inspecting') { + if (state.kind === 'inspecting' && refButtonPrevious.current && refButtonNext.current) { + refButtonPrevious.current.classList.toggle( + 'opacity-50', + true, + ); + refButtonPrevious.current.classList.toggle( + 'cursor-not-allowed', + true, + ); + refButtonNext.current.classList.toggle( + 'opacity-50', + true, + ); + refButtonNext.current.classList.toggle( + 'cursor-not-allowed', + true, + ); setShouldRender(true); } if (state.kind === 'inspect-off') { refAllElements.current = []; + setShouldRender(false); } if (state.kind === 'uninitialized') { @@ -111,15 +133,17 @@ export const Arrows = constant(() => { }; }, []); - if (!shouldRender) return null; - return (
    @@ -136,7 +167,13 @@ export const Arrows = constant(() => { ref={refButtonNext} title="Next element" onClick={onNextFocus} - className="flex cursor-not-allowed items-center justify-center px-3 opacity-50" + className={cn( + 'button', + 'flex items-center justify-center', + 'px-3 opacity-50', + 'transition-all duration-300', + 'cursor-not-allowed', + )} > diff --git a/packages/scan/src/web/components/widget/toolbar/index.tsx b/packages/scan/src/web/components/widget/toolbar/index.tsx index fd49ff54..dcad17d8 100644 --- a/packages/scan/src/web/components/widget/toolbar/index.tsx +++ b/packages/scan/src/web/components/widget/toolbar/index.tsx @@ -1,14 +1,16 @@ -import { useCallback, useEffect } from 'preact/hooks'; -import { ReactScanInternals, Store, setOptions } from '~core/index'; +import { useCallback, useEffect, useRef } from 'preact/hooks'; +import { ReactScanInternals, Store } from '~core/index'; import { Icon } from '~web/components/icon'; import FpsMeter from '~web/components/widget/fps-meter'; import { Arrows } from '~web/components/widget/toolbar/arrows'; +import { signalIsSettingsOpen } from '~web/state'; import { cn } from '~web/utils/helpers'; import { constant } from '~web/utils/preact/constant'; export const Toolbar = constant(() => { - const inspectState = Store.inspectState; + const refSettingsButton = useRef(null); + const inspectState = Store.inspectState; const isInspectActive = inspectState.value.kind === 'inspecting'; const isInspectFocused = inspectState.value.kind === 'focused'; @@ -44,13 +46,12 @@ export const Toolbar = constant(() => { } }, []); - const onSoundToggle = useCallback(() => { - const newSoundState = !ReactScanInternals.options.value.playSound; - setOptions({ playSound: newSoundState }); + const onToggleSettings = useCallback(() => { + signalIsSettingsOpen.value = !signalIsSettingsOpen.value; }, []); useEffect(() => { - const unsubscribe = Store.inspectState.subscribe((state) => { + const unSubState = Store.inspectState.subscribe((state) => { if (state.kind === 'uninitialized') { Store.inspectState.value = { kind: 'inspect-off', @@ -58,8 +59,13 @@ export const Toolbar = constant(() => { } }); + const unSubSettings = signalIsSettingsOpen.subscribe((state) => { + refSettingsButton.current?.classList.toggle('text-inspect', state); + }); + return () => { - unsubscribe(); + unSubState(); + unSubSettings(); }; }, []); @@ -68,17 +74,22 @@ export const Toolbar = constant(() => { if (isInspectActive) { inspectIcon = ; - inspectColor = 'rgba(142, 97, 227, 1)'; + inspectColor = '#8e61e3'; } else if (isInspectFocused) { inspectIcon = ; - inspectColor = 'rgba(142, 97, 227, 1)'; + inspectColor = '#8e61e3'; } else { inspectIcon = ; inspectColor = '#999'; } return ( -
    +
    react-scan diff --git a/packages/scan/src/web/constants.ts b/packages/scan/src/web/constants.ts index 07c83175..240a9400 100644 --- a/packages/scan/src/web/constants.ts +++ b/packages/scan/src/web/constants.ts @@ -1,6 +1,6 @@ export const SAFE_AREA = 24; export const MIN_SIZE = { - width: 360, + width: 320, height: 36, } as const; diff --git a/packages/scan/src/web/hooks/use-mount-delay.ts b/packages/scan/src/web/hooks/use-mount-delay.ts new file mode 100644 index 00000000..a3337aa9 --- /dev/null +++ b/packages/scan/src/web/hooks/use-mount-delay.ts @@ -0,0 +1,58 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; + +/** + * Delays a boolean value change by a specified duration. + * Perfect for coordinating animations with state changes. + * + * @param {boolean} value - The boolean value to delay + * @param {number} onDelay - Milliseconds to wait before changing to true + * @param {number} [offDelay] - Milliseconds to wait before changing to false (defaults to onDelay) + * @returns {boolean} The delayed value + * + * @example + * // Delay both transitions by 300ms + * const isVisible = useDelayedValue(show, 300); + * + * @example + * // Quick show (100ms), slow hide (500ms) + * const isVisible = useDelayedValue(show, 100, 500); + * + * @example + * // Use with CSS transitions + * const isVisible = useDelayedValue(show, 300); + * return ( + *
    + * {content} + *
    + * ); + */ +export const useDelayedValue = ( + value: boolean, + onDelay: number, + offDelay: number = onDelay, +): boolean => { + const refTimeout = useRef(); + const [delayedValue, setDelayedValue] = useState(value); + + /* + * biome-ignore lint/correctness/useExhaustiveDependencies: + * delayedValue is intentionally omitted to prevent unnecessary timeouts + * and used only in the early return check + */ + useEffect(() => { + if (value === delayedValue) return; + + const delay = value ? onDelay : offDelay; + refTimeout.current = setTimeout(() => setDelayedValue(value), delay); + + return () => clearTimeout(refTimeout.current); + }, [value, onDelay, offDelay]); + + return delayedValue; +}; diff --git a/packages/scan/src/web/state.ts b/packages/scan/src/web/state.ts index 85afa553..94d6bd47 100644 --- a/packages/scan/src/web/state.ts +++ b/packages/scan/src/web/state.ts @@ -7,6 +7,7 @@ import type { import { LOCALSTORAGE_KEY, MIN_SIZE, SAFE_AREA } from './constants'; import { readLocalStorage, saveLocalStorage } from './utils/helpers'; +export const signalIsSettingsOpen = signal(false); export const signalRefWidget = signal(null); export const defaultWidgetConfig = { diff --git a/packages/scan/tailwind.config.mjs b/packages/scan/tailwind.config.mjs index 42de4b57..a2b19ed6 100644 --- a/packages/scan/tailwind.config.mjs +++ b/packages/scan/tailwind.config.mjs @@ -7,17 +7,27 @@ export default { theme: { extend: { fontFamily: { - mono: ['Menlo', 'Consolas', 'Monaco', 'Liberation Mono', 'Lucida Console', 'monospace'], + mono: [ + 'Menlo', + 'Consolas', + 'Monaco', + 'Liberation Mono', + 'Lucida Console', + 'monospace', + ], + }, + colors: { + inspect: '#8e61e3', }, fontSize: { - 'xxs': '0.5rem', + xxs: '0.5rem', }, cursor: { 'nwse-resize': 'nwse-resize', 'nesw-resize': 'nesw-resize', 'ns-resize': 'ns-resize', 'ew-resize': 'ew-resize', - 'move': 'move', + move: 'move', }, keyframes: { fadeIn: { @@ -43,8 +53,8 @@ export default { animation: { 'fade-in': 'fadeIn ease-in forwards', 'fade-out': 'fadeOut ease-out forwards', - 'rotate': 'rotate linear infinite', - 'shake': 'shake 0.4s ease-in-out forwards', + rotate: 'rotate linear infinite', + shake: 'shake 0.4s ease-in-out forwards', }, zIndex: { 100: 100, @@ -59,7 +69,7 @@ export default { 'cursor-nesw-resize', 'cursor-ns-resize', 'cursor-ew-resize', - 'cursor-move' + 'cursor-move', ], plugins: [ ({ addUtilities }) => { From dda77cfa3d71c1405d75bf232c854ac87e43e272 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Mon, 13 Jan 2025 01:05:36 +0200 Subject: [PATCH 14/15] fix: circular refsm, perf improvements --- packages/scan/src/web/assets/svgs/svgs.ts | 2 +- .../src/web/components/inspector/index.tsx | 885 +++++++++--------- .../components/inspector/overlay/index.tsx | 55 +- .../web/components/inspector/overlay/utils.ts | 563 +++++++---- .../src/web/components/widget/settings.tsx | 4 +- .../web/components/widget/toolbar/arrows.tsx | 4 +- packages/scan/src/web/constants.ts | 2 +- packages/website/app/page.tsx | 4 + packages/website/components/counter.tsx | 104 ++ packages/website/components/todo-demo.tsx | 8 +- 10 files changed, 972 insertions(+), 659 deletions(-) create mode 100644 packages/website/components/counter.tsx diff --git a/packages/scan/src/web/assets/svgs/svgs.ts b/packages/scan/src/web/assets/svgs/svgs.ts index 9e87a19d..0ec966e2 100644 --- a/packages/scan/src/web/assets/svgs/svgs.ts +++ b/packages/scan/src/web/assets/svgs/svgs.ts @@ -107,7 +107,7 @@ export const ICONS = ` - + diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index e5018cfc..56ec7cbb 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -17,47 +17,23 @@ import { cn, tryOrElse } from '~web/utils/helpers'; import { constant } from '~web/utils/preact/constant'; import { flashManager } from './flash-overlay'; import { - getChangedContext, - getChangedProps, - getChangedState, + type InspectorData, + collectInspectorData, + ensureRecord, getContextChangeCount, - getCurrentContext, - getCurrentProps, - getCurrentState, + getCurrentFiberState, getPropsChangeCount, getStateChangeCount, getStateNames, + isPromise, resetStateTracking, } from './overlay/utils'; import { getCompositeFiberFromElement, getOverrideMethods } from './utils'; -interface InspectorState { +interface InspectorState extends InspectorData { fiber: Fiber | null; - changes: { - state: Set; - props: Set; - context: Set; - }; - current: { - state: Record; - props: Record; - context: Record; - }; } -type TypedArray = - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array - | BigInt64Array - | BigUint64Array; - type InspectableValue = | Record | Array @@ -79,7 +55,7 @@ type InspectableValue = interface PropertyElementProps { name: string; - value: unknown; + value: unknown | ValueMetadata; section: string; level: number; parentPath?: string; @@ -99,7 +75,16 @@ interface EditableValueProps { onCancel: () => void; } -type IterableEntry = [key: string | number, value: unknown]; +interface ValueMetadata { + type: string; + displayValue: string; + value?: unknown; + size?: number; + length?: number; + byteLength?: number; + entries?: Record; + items?: Array; +} const globalInspectorState = { lastRendered: new Map(), @@ -111,32 +96,18 @@ const globalInspectorState = { resetStateTracking(); inspectorState.value = { fiber: null, - changes: { - state: new Set(), - props: new Set(), - context: new Set(), - }, - current: { - state: {}, - props: {}, - context: {}, - }, + fiberProps: { current: {}, changes: new Set() }, + fiberState: { current: {}, changes: new Set() }, + fiberContext: { current: {}, changes: new Set() }, }; }, }; const inspectorState = signal({ fiber: null, - changes: { - state: new Set(), - props: new Set(), - context: new Set(), - }, - current: { - state: {}, - props: {}, - context: {}, - }, + fiberProps: { current: {}, changes: new Set() }, + fiberState: { current: {}, changes: new Set() }, + fiberContext: { current: {}, changes: new Set() }, }); class InspectorErrorBoundary extends Component { @@ -182,13 +153,6 @@ const isExpandable = (value: unknown): value is InspectableValue => { return Object.keys(value).length > 0; }; -const isPromise = (value: unknown): value is Promise => { - return ( - !!value && - (value instanceof Promise || (typeof value === 'object' && 'then' in value)) - ); -}; - const isEditableValue = (value: unknown, parentPath?: string): boolean => { if (value == null) return true; @@ -249,13 +213,6 @@ const getPath = ( return `${componentName}.${section}.${key}`; }; -const getArrayLength = (obj: ArrayBufferView): number => { - if (obj instanceof DataView) { - return obj.byteLength; - } - return (obj as TypedArray).length; -}; - const sanitizeString = (value: string): string => { return value .replace(/[<>]/g, '') @@ -275,62 +232,8 @@ const sanitizeErrorMessage = (error: string): string => { }; const formatValue = (value: unknown): string => { - switch (typeof value) { - case 'undefined': - return 'undefined'; - case 'string': - return `"${value}"`; - case 'number': - case 'boolean': - case 'bigint': - return String(value); - case 'symbol': - return value.toString(); - case 'object': { - if (!value) { - return 'null'; - } - switch (true) { - case value instanceof Map: - return `Map(${value.size})`; - case value instanceof Set: - return `Set(${value.size})`; - case value instanceof Date: - return value - .toLocaleString(undefined, { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }) - .replace(/[/,-]/g, '.'); - case value instanceof RegExp: - return 'RegExp'; - case value instanceof Error: - return 'Error'; - case value instanceof ArrayBuffer: - return `ArrayBuffer(${value.byteLength})`; - case value instanceof DataView: - return `DataView(${value.byteLength})`; - case ArrayBuffer.isView(value): - return `${value.constructor.name}(${getArrayLength(value)})`; - case Array.isArray(value): - return `Array(${value.length})`; - case isPromise(value): - return 'Promise'; - default: { - const keys = Object.keys(value); - if (keys.length <= 5) return `{${keys.join(', ')}}`; - return `{${keys.slice(0, 5).join(', ')}, ...${keys.length - 5}}`; - } - } - } - default: - return typeof value; - } + const metadata = ensureRecord(value); + return metadata.displayValue as string; }; const formatForClipboard = (value: unknown): string => { @@ -339,6 +242,24 @@ const formatForClipboard = (value: unknown): string => { if (value === undefined) return 'undefined'; if (isPromise(value)) return 'Promise'; + if (typeof value === 'function') { + const fnStr = value.toString(); + try { + const formatted = fnStr + .replace(/\s+/g, ' ') // Normalize whitespace + .replace(/{\s+/g, '{\n ') // Add newline after { + .replace(/;\s+/g, ';\n ') // Add newline after ; + .replace(/}\s*$/g, '\n}') // Add newline before final } + .replace(/\(\s+/g, '(') // Remove space after ( + .replace(/\s+\)/g, ')') // Remove space before ) + .replace(/,\s+/g, ', '); // Normalize comma spacing + + return formatted; + } catch { + return fnStr; + } + } + switch (true) { case value instanceof Date: return value.toISOString(); @@ -396,8 +317,6 @@ const parseArrayValue = (value: string): Array => { if (char === '\\') { escapeNext = true; - current += char; - continue; } if (char === '"') { @@ -575,17 +494,18 @@ const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { } catch { initialValue = String(value); } - setEditValue(sanitizeString(initialValue)); - }, [value]); - - useEffect(() => { - refInput.current?.focus(); - - if (typeof value === 'string') { - refInput.current?.setSelectionRange(1, refInput.current.value.length - 1); - } else { - refInput.current?.select(); - } + const sanitizedValue = sanitizeString(initialValue); + setEditValue(sanitizedValue); + + requestAnimationFrame(() => { + if (!refInput.current) return; + refInput.current.focus(); + if (typeof value === 'string') { + refInput.current.setSelectionRange(1, sanitizedValue.length - 1); + } else { + refInput.current.select(); + } + }); }, [value]); const handleChange = useCallback((e: Event) => { @@ -617,6 +537,7 @@ const EditableValue = ({ value, onSave, onCancel }: EditableValueProps) => { } else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); + e.stopImmediatePropagation(); onCancel(); } }; @@ -713,7 +634,15 @@ const PropertyElement = ({ const [isEditing, setIsEditing] = useState(false); const prevValue = globalInspectorState.lastRendered.get(currentPath); - const isChanged = prevValue !== undefined && !isEqual(prevValue, value); + const isChanged = !isEqual(prevValue, value); + + useEffect(() => { + return () => { + if (refElement.current) { + flashManager.cleanup(refElement.current); + } + }; + }, []); useEffect(() => { globalInspectorState.lastRendered.set(currentPath, value); @@ -722,19 +651,12 @@ const PropertyElement = ({ const shouldFlash = isChanged && refElement.current && - prevValue !== undefined && !isFirstRender; if (shouldFlash && refElement.current && level === 0) { flashManager.create(refElement.current); } - - return () => { - if (refElement.current) { - flashManager.cleanup(refElement.current); - } - }; - }, [value, isChanged, currentPath, prevValue, level]); + }, [value, isChanged, currentPath, level]); const handleToggleExpand = useCallback(() => { setIsExpanded((prevState: boolean) => { @@ -748,56 +670,57 @@ const PropertyElement = ({ }); }, [currentPath]); - const renderNestedProperties = useCallback( - (obj: InspectableValue) => { - let entries: Array; - - if (obj instanceof ArrayBuffer) { - const view = new Uint8Array(obj); - entries = Array.from(view).map((v, i) => [i, v]); - } else if (obj instanceof DataView) { - const view = new Uint8Array(obj.buffer, obj.byteOffset, obj.byteLength); - entries = Array.from(view).map((v, i) => [i, v]); - } else if (ArrayBuffer.isView(obj)) { - if (obj instanceof BigInt64Array || obj instanceof BigUint64Array) { - entries = Array.from({ length: obj.length }, (_, i) => [i, obj[i]]); - } else { - entries = Array.from(obj as ArrayLike).map((v, i) => [i, v]); - } - } else if (obj instanceof Map) { - entries = Array.from(obj.entries()).map(([k, v]) => [String(k), v]); - } else if (obj instanceof Set) { - entries = Array.from(obj).map((v, i) => [i, v]); - } else if (Array.isArray(obj)) { - entries = obj.map((value, index) => [index, value]); - } else { - entries = Object.entries(obj); + const valuePreview = useMemo(() => { + if (typeof value === 'object' && value !== null) { + if ('displayValue' in value) { + return String(value.displayValue); } + } + return formatValue(value); + }, [value]); - const canEditChildren = !( - obj instanceof DataView || - obj instanceof ArrayBuffer || - ArrayBuffer.isView(obj) - ); + const clipboardText = useMemo(() => { + if (typeof value === 'object' && value !== null) { + if ('value' in value) { + return String(formatForClipboard(value.value)); + } + if ('displayValue' in value) { + return String(value.displayValue); + } + } + return String(formatForClipboard(value)); + }, [value]); - return entries.map(([key, value]) => ( - - )); - }, - [section, level, currentPath, objectPathMap, changedKeys], - ); + const isExpandableValue = useMemo(() => { + if (!value || typeof value !== 'object') return false; + + if ('type' in value) { + const metadata = value as ValueMetadata; + switch (metadata.type) { + case 'array': + case 'Map': + case 'Set': + return (metadata.size ?? metadata.length ?? 0) > 0; + case 'object': + return (metadata.size ?? 0) > 0; + case 'ArrayBuffer': + case 'DataView': + return (metadata.byteLength ?? 0) > 0; + case 'circular': + case 'promise': + case 'function': + case 'error': + return false; + default: + if ('entries' in metadata || 'items' in metadata) { + return true; + } + return false; + } + } - const valuePreview = useMemo(() => formatValue(value), [value]); + return isExpandable(value); + }, [value]); const { overrideProps, overrideHookState } = getOverrideMethods(); const canEdit = useMemo(() => { @@ -833,81 +756,76 @@ const PropertyElement = ({ return isBadRender; }, [currentPath, level, value]); - const clipboardText = useMemo(() => formatForClipboard(value), [value]); - const handleEdit = useCallback(() => { if (canEdit) { setIsEditing(true); } }, [canEdit]); - const handleSave = useCallback( - (newValue: unknown) => { - if (isEqual(value, newValue)) { - setIsEditing(false); - return; - } + const handleSave = useCallback((newValue: unknown) => { + if (isEqual(value, newValue)) { + setIsEditing(false); + return; + } - if (section === 'props' && overrideProps) { - tryOrElse(() => { - if (!fiber) return; - - if (parentPath) { - const parts = parentPath.split('.'); - const path = parts.filter( - (part) => part !== 'props' && part !== getDisplayName(fiber.type), - ); - path.push(name); - overrideProps(fiber, path, newValue); - } else { - overrideProps(fiber, [name], newValue); - } - }, null); - } + if (section === 'props' && overrideProps) { + tryOrElse(() => { + if (!fiber) return; - if (section === 'state' && overrideHookState) { - tryOrElse(() => { - if (!fiber) return; - - if (!parentPath) { - const stateNames = getStateNames(fiber); - const namedStateIndex = stateNames.indexOf(name); - const hookId = - namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; - overrideHookState(fiber, hookId, [], newValue); - } else { - const fullPathParts = parentPath.split('.'); - const stateIndex = fullPathParts.indexOf('state'); - if (stateIndex === -1) return; - - const statePath = fullPathParts.slice(stateIndex + 1); - const baseStateKey = statePath[0]; - const stateNames = getStateNames(fiber); - const namedStateIndex = stateNames.indexOf(baseStateKey); - const hookId = - namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; - - const currentState = getCurrentState(fiber); - if (!currentState || !(baseStateKey in currentState)) { - // biome-ignore lint/suspicious/noConsole: Intended debug output - console.warn(sanitizeErrorMessage('Invalid state key')); - return; - } + if (parentPath) { + const parts = parentPath.split('.'); + const path = parts.filter( + (part) => part !== 'props' && part !== getDisplayName(fiber.type), + ); + path.push(name); + overrideProps(fiber, path, newValue); + } else { + overrideProps(fiber, [name], newValue); + } + }, null); + } - const updatedState = updateNestedValue( - currentState[baseStateKey], - statePath.slice(1).concat(name), - newValue, - ); - overrideHookState(fiber, hookId, [], updatedState); + if (section === 'state' && overrideHookState) { + tryOrElse(() => { + if (!fiber) return; + + if (!parentPath) { + const stateNames = getStateNames(fiber); + const namedStateIndex = stateNames.indexOf(name); + const hookId = + namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; + overrideHookState(fiber, hookId, [], newValue); + } else { + const fullPathParts = parentPath.split('.'); + const stateIndex = fullPathParts.indexOf('state'); + if (stateIndex === -1) return; + + const statePath = fullPathParts.slice(stateIndex + 1); + const baseStateKey = statePath[0]; + const stateNames = getStateNames(fiber); + const namedStateIndex = stateNames.indexOf(baseStateKey); + const hookId = + namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; + + const currentState = inspectorState.value.fiberState.current; + if (!currentState || !(baseStateKey in currentState)) { + // biome-ignore lint/suspicious/noConsole: Intended debug output + console.warn(sanitizeErrorMessage('Invalid state key')); + return; } - }, null); - } - setIsEditing(false); - }, - [value, section, overrideProps, overrideHookState, fiber, name, parentPath], - ); + const updatedState = updateNestedValue( + currentState[baseStateKey], + statePath.slice(1).concat(name), + newValue, + ); + overrideHookState(fiber, hookId, [], updatedState); + } + }, null); + } + + setIsEditing(false); + }, [value, section, overrideProps, overrideHookState, fiber, name, parentPath]); const checkCircularInValue = useMemo((): boolean => { if (!value || typeof value !== 'object' || isPromise(value)) return false; @@ -915,6 +833,135 @@ const PropertyElement = ({ return 'type' in value && value.type === 'circular'; }, [value]); + const renderNestedProperties = useCallback( + (obj: unknown): preact.ComponentChildren => { + if (!obj || typeof obj !== 'object') return null; + + if ('type' in obj) { + const metadata = obj as ValueMetadata; + if ('entries' in metadata && metadata.entries) { + const entries = Object.entries(metadata.entries); + if (entries.length === 0) return null; + + return ( +
    + {entries.map(([key, val]) => ( + + ))} +
    + ); + } + + if ('items' in metadata && Array.isArray(metadata.items)) { + if (metadata.items.length === 0) return null; + return ( +
    + { + metadata.items.map((item, i) => { + const itemKey = `${currentPath}-item-${item.type}-${i}`; + return ( + + ); + }) + } +
    + ); + } + return null; + } + + let entries: Array<[key: string | number, value: unknown]>; + + if (obj instanceof ArrayBuffer) { + const view = new Uint8Array(obj); + entries = Array.from(view).map((v, i) => [i, v]); + } else if (obj instanceof DataView) { + const view = new Uint8Array(obj.buffer, obj.byteOffset, obj.byteLength); + entries = Array.from(view).map((v, i) => [i, v]); + } else if (ArrayBuffer.isView(obj)) { + if (obj instanceof BigInt64Array || obj instanceof BigUint64Array) { + entries = Array.from({ length: obj.length }, (_, i) => [ + i, + obj[i], + ]); + } else { + const typedArray = obj as unknown as ArrayLike; + entries = Array.from(typedArray).map((v, i) => [i, v]); + } + } else if (obj instanceof Map) { + entries = Array.from(obj.entries()).map(([k, v]) => [String(k), v]); + } else if (obj instanceof Set) { + entries = Array.from(obj).map((v, i) => [i, v]); + } else if (Array.isArray(obj)) { + entries = obj.map((value, index) => [`${index}`, value]); + } else { + entries = Object.entries(obj); + } + + if (entries.length === 0) return null; + + const canEditChildren = !( + obj instanceof DataView || + obj instanceof ArrayBuffer || + ArrayBuffer.isView(obj) + ); + + return ( +
    + { + entries.map(([key, val]) => { + const itemKey = `${currentPath}-${typeof key === 'number' ? `item-${key}` : key}`; + return ( + + ); + }) + } +
    + ); + }, + [section, level, currentPath, objectPathMap, changedKeys, allowEditing], + ); + + const renderMemoizationIcon = useMemo(() => { + if (changedKeys.has(`${name}:memoized`)) { + return ; + } + if (changedKeys.has(`${name}:unmemoized`)) { + return ; + } + return null; + }, [changedKeys, name]); + if (checkCircularInValue) { return (
    @@ -930,9 +977,9 @@ const PropertyElement = ({ return (
    -
    +
    { - isExpandable(value) && ( + isExpandableValue && (
    ); }); @@ -1244,24 +1280,6 @@ export const Inspector = constant(() => { let isProcessing = false; const pendingUpdates = new Set(); - const updateInspectorState = (fiber: Fiber) => { - refLastInspectedFiber.current = fiber; - - inspectorState.value = { - fiber, - changes: { - props: getChangedProps(fiber), - state: getChangedState(fiber), - context: getChangedContext(fiber), - }, - current: { - state: getCurrentState(fiber), - props: getCurrentProps(fiber), - context: getCurrentContext(fiber), - } - }; - }; - const processNextUpdate = () => { if (pendingUpdates.size === 0) { isProcessing = false; @@ -1272,7 +1290,12 @@ export const Inspector = constant(() => { pendingUpdates.delete(nextFiber); try { - updateInspectorState(nextFiber); + refLastInspectedFiber.current = nextFiber; + + inspectorState.value = { + fiber: nextFiber, + ...collectInspectorData(nextFiber), + }; } finally { if (pendingUpdates.size > 0) { queueMicrotask(processNextUpdate); @@ -1282,15 +1305,6 @@ export const Inspector = constant(() => { } }; - const processFiberUpdate = (fiber: Fiber) => { - pendingUpdates.add(fiber); - - if (!isProcessing) { - isProcessing = true; - queueMicrotask(processNextUpdate); - } - }; - const unSubState = Store.inspectState.subscribe((state) => { if (state.kind !== 'focused' || !state.focusedDomElement) { pendingUpdates.clear(); @@ -1310,24 +1324,27 @@ export const Inspector = constant(() => { globalInspectorState.cleanup(); refLastInspectedFiber.current = parentCompositeFiber; - getChangedProps(parentCompositeFiber); - getChangedState(parentCompositeFiber); - getChangedContext(parentCompositeFiber); + const { + fiberProps, + fiberState, + fiberContext, + } = collectInspectorData(parentCompositeFiber); inspectorState.value = { fiber: parentCompositeFiber, - changes: { - props: new Set(), - state: new Set(), - context: new Set(), + fiberProps: { + ...fiberProps, + changes: new Set(), + }, + fiberState: { + ...fiberState, + changes: new Set(), + }, + fiberContext: { + ...fiberContext, + changes: new Set(), }, - current: { - state: getCurrentState(parentCompositeFiber), - props: getCurrentProps(parentCompositeFiber), - context: getCurrentContext(parentCompositeFiber), - } }; - }); const unSubReport = Store.lastReportTime.subscribe(() => { @@ -1348,7 +1365,12 @@ export const Inspector = constant(() => { } if (parentCompositeFiber.type === refLastInspectedFiber.current?.type) { - processFiberUpdate(parentCompositeFiber); + pendingUpdates.add(parentCompositeFiber); + + if (!isProcessing) { + isProcessing = true; + queueMicrotask(processNextUpdate); + } } }); @@ -1367,10 +1389,12 @@ export const Inspector = constant(() => { className={cn( 'react-scan-inspector', 'opacity-0', + 'max-h-0', + 'overflow-hidden', 'transition-opacity duration-150 delay-0', 'pointer-events-none', { - 'opacity-100 delay-300 pointer-events-auto': !isSettingsOpen, + 'opacity-100 delay-300 pointer-events-auto max-h-["auto"]': !isSettingsOpen, }, )} > @@ -1384,38 +1408,31 @@ export const Inspector = constant(() => { }); export const replayComponent = async (fiber: Fiber): Promise => { - try { - const { overrideProps, overrideHookState } = getOverrideMethods(); - if (!overrideProps || !overrideHookState || !fiber) return; + const { overrideProps, overrideHookState } = getOverrideMethods(); + if (!overrideProps || !overrideHookState || !fiber) return; - const currentProps = fiber.memoizedProps || {}; - for (const key of Object.keys(currentProps)) { - try { - overrideProps(fiber, [key], currentProps[key]); - } catch { - // Silently ignore prop override errors - } - } + const currentProps = fiber.memoizedProps || {}; + for (const key of Object.keys(currentProps)) { + try { + overrideProps(fiber, [key], currentProps[key]); + } catch {} + } - const state = getCurrentState(fiber) ?? {}; - for (const key of Object.keys(state)) { + const currentState = getCurrentFiberState(fiber); + if (currentState) { + const stateNames = getStateNames(fiber); + for (const [key, value] of Object.entries(currentState)) { try { - const stateNames = getStateNames(fiber); const namedStateIndex = stateNames.indexOf(key); - const hookId = - namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; - overrideHookState(fiber, hookId, [], state[key]); - } catch { - // Silently ignore state override errors - } + const hookId = namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; + overrideHookState(fiber, hookId, [], value); + } catch {} } + } - let child = fiber.child; - while (child) { - await replayComponent(child); - child = child.sibling; - } - } catch { - // Silently ignore replay errors + let child = fiber.child; + while (child) { + await replayComponent(child); + child = child.sibling; } }; diff --git a/packages/scan/src/web/components/inspector/overlay/index.tsx b/packages/scan/src/web/components/inspector/overlay/index.tsx index 7676bad3..908b6eda 100644 --- a/packages/scan/src/web/components/inspector/overlay/index.tsx +++ b/packages/scan/src/web/components/inspector/overlay/index.tsx @@ -288,10 +288,10 @@ export const ScanOverlay = () => { if ( targetRect.width <= 0 || targetRect.height <= 0 || - targetRect.left < 0 || - targetRect.top < 0 || targetRect.left >= window.innerWidth || targetRect.top >= window.innerHeight || + (targetRect.left + targetRect.width <= 0) || + (targetRect.top + targetRect.height <= 0) || (targetRect.left === 0 && targetRect.top === 0) ) { handleNonHoverableArea(); @@ -503,29 +503,40 @@ export const ScanOverlay = () => { }; const handleKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Escape') return; + const state = Store.inspectState.peek(); const canvas = refCanvas.current; - if (!canvas || e.key !== 'Escape') return; + if (!canvas) return; - switch (state.kind) { - case 'focused': { - startFadeIn(); - refCurrentRect.current = null; - refLastHoveredElement.current = state.focusedDomElement; - Store.inspectState.value = { - kind: 'inspecting', - hoveredDomElement: state.focusedDomElement, - }; - break; - } - case 'inspecting': { - startFadeOut(() => { - signalIsSettingsOpen.value = false; + if (document.activeElement?.id === 'react-scan-root') { + return; + } + + if (state.kind === 'focused' || state.kind === 'inspecting') { + e.preventDefault(); + e.stopPropagation(); + + switch (state.kind) { + case 'focused': { + startFadeIn(); + refCurrentRect.current = null; + refLastHoveredElement.current = state.focusedDomElement; Store.inspectState.value = { - kind: 'inspect-off', + kind: 'inspecting', + hoveredDomElement: state.focusedDomElement, }; - }); - break; + break; + } + case 'inspecting': { + startFadeOut(() => { + signalIsSettingsOpen.value = false; + Store.inspectState.value = { + kind: 'inspect-off', + }; + }); + break; + } } } }; @@ -654,7 +665,7 @@ export const ScanOverlay = () => { capture: true, }); document.addEventListener('click', handleClick, { capture: true }); - window.addEventListener('keydown', handleKeyDown); + document.addEventListener('keydown', handleKeyDown, { capture: true }); return () => { unsubscribeAll(); @@ -668,7 +679,7 @@ export const ScanOverlay = () => { document.removeEventListener('pointerdown', handlePointerDown, { capture: true, }); - window.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keydown', handleKeyDown, { capture: true }); if (refRafId.current) { cancelAnimationFrame(refRafId.current); diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index 63fe9fe2..6a1b157c 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -27,70 +27,204 @@ interface ReactContext { const stateChangeCounts = new Map(); const propsChangeCounts = new Map(); const contextChangeCounts = new Map(); -let lastRenderedStates = new WeakMap(); +let lastRenderedStates = new WeakMap>(); const STATE_NAME_REGEX = /\[(?\w+),\s*set\w+\]/g; const PROPS_ORDER_REGEX = /\(\s*{\s*(?[^}]+)\s*}\s*\)/; -const ensureRecord = ( +export const isPromise = (value: unknown): value is Promise => { + return ( + !!value && + (value instanceof Promise || (typeof value === 'object' && 'then' in value)) + ); +}; + +export const ensureRecord = ( value: unknown, - seen = new WeakSet(), + maxDepth = 2, + seen = new WeakSet(), ): Record => { - if (value == null) { - return {}; + if (isPromise(value)) { + return { type: 'promise', displayValue: 'Promise' }; + } + + if (value === null) { + return { type: 'null', displayValue: 'null' }; + } + + if (value === undefined) { + return { type: 'undefined', displayValue: 'undefined' }; } + switch (typeof value) { - case 'object': - if (value instanceof Element) { - return { - type: 'Element', - tagName: value.tagName.toLowerCase(), - }; - } - if (value instanceof Promise || 'then' in value) { - return { type: 'promise' }; - } + case 'object': { if (seen.has(value)) { - return { type: 'circular' }; + return { type: 'circular', displayValue: '[Circular Reference]' }; } - if (Array.isArray(value)) { - seen.add(value); - const safeArray = value.map((item) => ensureRecord(item, seen)); - return { type: 'array', length: value.length, items: safeArray }; - } + if (!value) return { type: 'null', displayValue: 'null' }; seen.add(value); - /* - * biome-ignore lint/correctness/noSwitchDeclarations: - * Performance optimization: - * - Early type-based branching via typeof before expensive instanceof checks - * - Single allocation of result object - * - Avoids redundant checks in switch-case fallthrough - */ - const result: Record = {}; try { + const result: Record = {}; + + if (value instanceof Element) { + result.type = 'Element'; + result.tagName = value.tagName.toLowerCase(); + result.displayValue = value.tagName.toLowerCase(); + return result; + } + + if (value instanceof Map) { + result.type = 'Map'; + result.size = value.size; + result.displayValue = `Map(${value.size})`; + + if (maxDepth > 0) { + const entries: Record = {}; + let index = 0; + for (const [key, val] of value.entries()) { + if (index >= 50) break; + try { + entries[String(key)] = ensureRecord(val, maxDepth - 1, seen); + } catch { + entries[String(index)] = { + type: 'error', + displayValue: 'Error accessing Map entry', + }; + } + index++; + } + result.entries = entries; + } + return result; + } + + if (value instanceof Set) { + result.type = 'Set'; + result.size = value.size; + result.displayValue = `Set(${value.size})`; + + if (maxDepth > 0) { + result.items = Array.from(value) + .slice(0, 50) + .map((item) => ensureRecord(item, maxDepth - 1, seen)); + } + return result; + } + + if (value instanceof Date) { + result.type = 'Date'; + result.value = value.toISOString(); + result.displayValue = value.toLocaleString(); + return result; + } + + if (value instanceof RegExp) { + result.type = 'RegExp'; + result.value = value.toString(); + result.displayValue = value.toString(); + return result; + } + + if (value instanceof Error) { + result.type = 'Error'; + result.name = value.name; + result.message = value.message; + result.displayValue = `${value.name}: ${value.message}`; + return result; + } + + if (value instanceof ArrayBuffer) { + result.type = 'ArrayBuffer'; + result.byteLength = value.byteLength; + result.displayValue = `ArrayBuffer(${value.byteLength})`; + return result; + } + + if (value instanceof DataView) { + result.type = 'DataView'; + result.byteLength = value.byteLength; + result.displayValue = `DataView(${value.byteLength})`; + return result; + } + + if (ArrayBuffer.isView(value)) { + const typedArray = value as unknown as { + length: number; + constructor: { name: string }; + buffer: ArrayBuffer; + }; + result.type = typedArray.constructor.name; + result.length = typedArray.length; + result.byteLength = typedArray.buffer.byteLength; + result.displayValue = `${typedArray.constructor.name}(${typedArray.length})`; + return result; + } + + if (Array.isArray(value)) { + result.type = 'array'; + result.length = value.length; + result.displayValue = `Array(${value.length})`; + + if (maxDepth > 0) { + result.items = value + .slice(0, 50) + .map((item) => ensureRecord(item, maxDepth - 1, seen)); + } + return result; + } + const keys = Object.keys(value); - for (const key of keys) { - try { - const val = (value as Record)[key]; - result[key] = ensureRecord(val, seen); - } catch { - result[key] = { - type: 'error', - message: 'Failed to access property', - }; + result.type = 'object'; + result.size = keys.length; + result.displayValue = + keys.length <= 5 + ? `{${keys.join(', ')}}` + : `{${keys.slice(0, 5).join(', ')}, ...${keys.length - 5}}`; + + if (maxDepth > 0) { + const entries: Record = {}; + for (const key of keys.slice(0, 50)) { + try { + entries[key] = ensureRecord( + (value as Record)[key], + maxDepth - 1, + seen, + ); + } catch { + entries[key] = { + type: 'error', + displayValue: 'Error accessing property', + }; + } } + result.entries = entries; } return result; - } catch { - return { type: 'object' }; + } finally { + seen.delete(value); } + } + case 'string': + return { + type: 'string', + value, + displayValue: `"${value}"`, + }; case 'function': - return { type: 'function', name: value.name || 'anonymous' }; + return { + type: 'function', + displayValue: 'ƒ()', + name: value.name || 'anonymous', + }; default: - return { value }; + return { + type: typeof value, + value, + displayValue: String(value), + }; } }; @@ -98,7 +232,7 @@ export const resetStateTracking = () => { stateChangeCounts.clear(); propsChangeCounts.clear(); contextChangeCounts.clear(); - lastRenderedStates = new WeakMap(); + lastRenderedStates = new WeakMap>(); }; export const getStateChangeCount = (name: string): number => @@ -110,8 +244,6 @@ export const getContextChangeCount = (name: string): number => export const getStateNames = (fiber: Fiber): Array => { const componentSource = fiber.type?.toString?.() || ''; - // Return the matches if we found any, otherwise return empty array - // Empty array means we'll use numeric indices as fallback return componentSource ? Array.from( componentSource.matchAll(STATE_NAME_REGEX), @@ -137,7 +269,7 @@ export const isDirectComponent = (fiber: Fiber): boolean => { if (memoizedState.queue) { return true; } - const nextState = memoizedState.next; + const nextState: ExtendedMemoizedState | null = memoizedState.next; if (!nextState) break; memoizedState = nextState; } @@ -145,53 +277,6 @@ export const isDirectComponent = (fiber: Fiber): boolean => { return false; }; -export const getCurrentState = (fiber: Fiber | null) => { - if (!fiber) return {}; - - if (fiber.tag === FunctionComponentTag && isDirectComponent(fiber)) { - return getCurrentFiberState(fiber) ?? {}; - } - return {}; -}; - -export const getChangedState = (fiber: Fiber): Set => { - const changes = new Set(); - if (!fiber || fiber.tag !== FunctionComponentTag || !isDirectComponent(fiber)) - return changes; - - try { - const currentState = getCurrentFiberState(fiber); - if (!currentState) return changes; - - if (!fiber.alternate) { - lastRenderedStates.set(fiber, { ...currentState }); - return changes; - } - - const lastState = lastRenderedStates.get(fiber); - if (lastState) { - for (const name of Object.keys(currentState)) { - if (!isEqual(currentState[name], lastState[name])) { - changes.add(name); - if (lastState[name] !== undefined) { - const existingCount = stateChangeCounts.get(name) ?? 0; - stateChangeCounts.set(name, existingCount + 1); - } - } - } - } - - lastRenderedStates.set(fiber, { ...currentState }); - if (fiber.alternate) { - lastRenderedStates.set(fiber.alternate, { ...currentState }); - } - } catch { - // Silently fail - } - - return changes; -}; - interface ExtendedMemoizedState extends MemoizedState { queue?: { lastRenderedState: unknown; @@ -210,7 +295,9 @@ const getStateValue = (memoizedState: ExtendedMemoizedState): unknown => { return memoizedState.memoizedState; }; -const getCurrentFiberState = (fiber: Fiber): Record | null => { +export const getCurrentFiberState = ( + fiber: Fiber, +): Record | null => { if (fiber.tag !== FunctionComponentTag || !isDirectComponent(fiber)) { return null; } @@ -219,7 +306,7 @@ const getCurrentFiberState = (fiber: Fiber): Record | null => { ? (fiber.actualStartTime ?? 0) > (fiber.alternate.actualStartTime ?? 0) : true; - let memoizedState = currentIsNewer + let memoizedState: ExtendedMemoizedState | null = currentIsNewer ? fiber.memoizedState : (fiber.alternate?.memoizedState ?? fiber.memoizedState); @@ -229,17 +316,16 @@ const getCurrentFiberState = (fiber: Fiber): Record | null => { const stateNames = getStateNames(fiber); let index = 0; - while (memoizedState) { + while (memoizedState !== null) { if (memoizedState.queue) { const name = stateNames[index] || `{${index}}`; - try { - currentState[name] = getStateValue(memoizedState); - } catch { - // Silently fail - } + const value = getStateValue(memoizedState); + currentState[name] = isPromise(value) + ? { type: 'promise', displayValue: 'Promise' } + : value; index++; } - const nextState = memoizedState.next; + const nextState: ExtendedMemoizedState | null = memoizedState.next; if (!nextState) break; memoizedState = nextState; } @@ -247,82 +333,7 @@ const getCurrentFiberState = (fiber: Fiber): Record | null => { return currentState; }; -export const getPropsOrder = (fiber: Fiber): Array => { - const componentSource = fiber.type?.toString?.() || ''; - const match = componentSource.match(PROPS_ORDER_REGEX); - if (!match?.groups?.props) return []; - - return match.groups.props - .split(',') - .map((prop: string) => prop.trim().split(':')[0].split('=')[0].trim()) - .filter(Boolean); -}; - -export const getCurrentProps = (fiber: Fiber): Record => { - const currentIsNewer = fiber?.alternate - ? (fiber.actualStartTime ?? 0) > (fiber.alternate?.actualStartTime ?? 0) - : true; - - const baseProps = currentIsNewer - ? fiber.memoizedProps || fiber.pendingProps - : fiber.alternate?.memoizedProps || - fiber.alternate?.pendingProps || - fiber.memoizedProps; - - const result: Record = {}; - - for (const [key, value] of Object.entries(baseProps)) { - result[key] = value; - if ((value && typeof value === 'object') || typeof value === 'function') { - if (fiber.alternate?.memoizedProps) { - const prevValue = fiber.alternate.memoizedProps[key]; - const status = value === prevValue ? 'memoized' : 'unmemoized'; - propsChangeCounts.set(`${key}:${status}`, 0); - } - } - } - - return result; -}; - -export const getChangedProps = (fiber: Fiber | null): Set => { - if (!fiber?.memoizedProps) return new Set(); - - const currentProps = fiber.memoizedProps; - const changes = new Set(); - - for (const [key, currentValue] of Object.entries(currentProps)) { - // Track memoization for non-primitive values (functions, objects, arrays) - if ( - (currentValue && typeof currentValue === 'object') || - typeof currentValue === 'function' - ) { - if (fiber.alternate?.memoizedProps) { - const prevValue = fiber.alternate.memoizedProps[key]; - const status = currentValue === prevValue ? 'memoized' : 'unmemoized'; - changes.add(`${key}:${status}`); - } - continue; - } - - // Track changes for primitive values - if ( - fiber.alternate?.memoizedProps && - key in fiber.alternate.memoizedProps && - !isEqual(fiber.alternate.memoizedProps[key], currentValue) - ) { - changes.add(key); - const count = (propsChangeCounts.get(key) || 0) + 1; - propsChangeCounts.set(key, count); - } - } - - return changes; -}; - -export const getAllFiberContexts = ( - fiber: Fiber, -): Map => { +const getAllFiberContexts = (fiber: Fiber): Map => { const contexts = new Map(); if (!fiber) return contexts; @@ -336,7 +347,6 @@ export const getAllFiberContexts = ( const pendingValue = searchFiber.pendingProps?.value; const currentValue = contextType._currentValue; - // For built-in contexts if (contextType.displayName) { if (currentValue === null) { return null; @@ -399,25 +409,186 @@ export const getAllFiberContexts = ( return contexts; }; -export const getCurrentContext = (fiber: Fiber) => { - const contexts = getAllFiberContexts(fiber); - // TODO(Alexis): megamorphic code - const contextObj: Record = {}; +const getPropsOrder = (fiber: Fiber): Array => { + const componentSource = fiber.type?.toString?.() || ''; + const match = componentSource.match(PROPS_ORDER_REGEX); + if (!match?.groups?.props) return []; - for (const [contextName, value] of contexts) { - contextObj[contextName] = value.displayValue; + return match.groups.props + .split(',') + .map((prop: string) => prop.trim().split(':')[0].split('=')[0].trim()) + .filter(Boolean); +}; + +interface SectionData { + current: Record; + changes: Set; +} + +export interface InspectorData { + fiberProps: SectionData; + fiberState: SectionData; + fiberContext: SectionData; +} + +export const collectInspectorData = (fiber: Fiber): InspectorData => { + if (!fiber) { + return { + fiberProps: { current: {}, changes: new Set() }, + fiberState: { current: {}, changes: new Set() }, + fiberContext: { current: {}, changes: new Set() }, + }; } - return contextObj; -}; + const fiberProps = { + current: {} as Record, + changes: new Set(), + }; + + if (fiber.memoizedProps) { + const currentIsNewer = fiber?.alternate + ? (fiber.actualStartTime ?? 0) > (fiber.alternate?.actualStartTime ?? 0) + : true; + + const baseProps = currentIsNewer + ? fiber.memoizedProps || fiber.pendingProps + : fiber.alternate?.memoizedProps || + fiber.alternate?.pendingProps || + fiber.memoizedProps; + + const orderedProps = getPropsOrder(fiber); + const remainingProps = new Set(Object.keys(baseProps)); + + for (const key of orderedProps) { + if (key in baseProps) { + const value = baseProps[key]; + fiberProps.current[key] = isPromise(value) + ? { type: 'promise', displayValue: 'Promise' } + : value; + + if ( + (value && typeof value === 'object') || + typeof value === 'function' + ) { + if (fiber.alternate?.memoizedProps) { + const prevValue = fiber.alternate.memoizedProps[key]; + const status = value === prevValue ? 'memoized' : 'unmemoized'; + propsChangeCounts.set(`${key}:${status}`, 0); + } + } + remainingProps.delete(key); + } + } -export const getChangedContext = (fiber: Fiber): Set => { - const changes = new Set(); - if (!fiber.alternate) return changes; + for (const key of remainingProps) { + const value = baseProps[key]; + fiberProps.current[key] = isPromise(value) + ? { type: 'promise', displayValue: 'Promise' } + : value; + + if ((value && typeof value === 'object') || typeof value === 'function') { + if (fiber.alternate?.memoizedProps) { + const prevValue = fiber.alternate.memoizedProps[key]; + const status = value === prevValue ? 'memoized' : 'unmemoized'; + propsChangeCounts.set(`${key}:${status}`, 0); + } + } + } + + const currentProps = fiber.memoizedProps; + for (const [key, currentValue] of Object.entries(currentProps)) { + if ( + (currentValue && typeof currentValue === 'object') || + typeof currentValue === 'function' + ) { + if (fiber.alternate?.memoizedProps) { + const prevValue = fiber.alternate.memoizedProps[key]; + const status = currentValue === prevValue ? 'memoized' : 'unmemoized'; + fiberProps.changes.add(`${key}:${status}`); + } + continue; + } + + if ( + fiber.alternate?.memoizedProps && + key in fiber.alternate.memoizedProps && + !isEqual(fiber.alternate.memoizedProps[key], currentValue) + ) { + fiberProps.changes.add(key); + const count = (propsChangeCounts.get(key) || 0) + 1; + propsChangeCounts.set(key, count); + } + } + } - const currentContexts = getAllFiberContexts(fiber); + const fiberState = { + current: {} as Record, + changes: new Set(), + }; + + if (fiber.tag === FunctionComponentTag && isDirectComponent(fiber)) { + const stateNames = getStateNames(fiber); + const currentIsNewer = fiber.alternate + ? (fiber.actualStartTime ?? 0) > (fiber.alternate.actualStartTime ?? 0) + : true; + + let memoizedState: ExtendedMemoizedState | null = currentIsNewer + ? fiber.memoizedState + : (fiber.alternate?.memoizedState ?? fiber.memoizedState); + + let index = 0; + const lastState = lastRenderedStates.get(fiber); + + while (memoizedState !== null) { + if (memoizedState.queue) { + const name = stateNames[index] || `{${index}}`; + const value = getStateValue(memoizedState); + fiberState.current[name] = isPromise(value) + ? { type: 'promise', displayValue: 'Promise' } + : value; + index++; + } + const nextState: ExtendedMemoizedState | null = memoizedState.next; + if (!nextState) break; + memoizedState = nextState; + } + + if (lastState) { + for (const name of Object.keys(fiberState.current)) { + if (!isEqual(fiberState.current[name], lastState[name])) { + fiberState.changes.add(name); + if (lastState[name] !== undefined) { + const existingCount = stateChangeCounts.get(name) ?? 0; + stateChangeCounts.set(name, existingCount + 1); + } + } + } + } + + if (Object.keys(fiberState.current).length > 0) { + lastRenderedStates.set(fiber, { ...fiberState.current }); + if (fiber.alternate) { + lastRenderedStates.set(fiber.alternate, { ...fiberState.current }); + } + } + } + + const fiberContext = { + current: {} as Record, + changes: new Set(), + }; + + const contexts = getAllFiberContexts(fiber); + for (const [contextName, value] of contexts) { + if (isPromise(value.rawValue)) { + fiberContext.current[contextName] = { + type: 'promise', + displayValue: 'Promise', + }; + } else { + fiberContext.current[contextName] = value.displayValue; + } - for (const [contextName] of currentContexts) { let searchFiber: Fiber | null = fiber; let providerFiber: Fiber | null = null; @@ -434,7 +605,7 @@ export const getChangedContext = (fiber: Fiber): Set => { const alternateValue = providerFiber.alternate.memoizedProps?.value; if (!isEqual(currentProviderValue, alternateValue)) { - changes.add(contextName); + fiberContext.changes.add(contextName); contextChangeCounts.set( contextName, (contextChangeCounts.get(contextName) ?? 0) + 1, @@ -443,5 +614,9 @@ export const getChangedContext = (fiber: Fiber): Set => { } } - return changes; + return { + fiberProps, + fiberState, + fiberContext, + }; }; diff --git a/packages/scan/src/web/components/widget/settings.tsx b/packages/scan/src/web/components/widget/settings.tsx index eef20837..76abbc3c 100644 --- a/packages/scan/src/web/components/widget/settings.tsx +++ b/packages/scan/src/web/components/widget/settings.tsx @@ -29,10 +29,12 @@ export const Settings = () => { className={cn( 'react-scan-settings', 'opacity-0', + 'max-h-0', + 'overflow-hidden', 'transition-opacity duration-150 delay-0', 'pointer-events-none', { - 'opacity-100 delay-300 pointer-events-auto': isSettingsOpen, + 'opacity-100 delay-300 pointer-events-auto max-h-["auto"]': isSettingsOpen, }, )} > diff --git a/packages/scan/src/web/components/widget/toolbar/arrows.tsx b/packages/scan/src/web/components/widget/toolbar/arrows.tsx index 24e45ed5..a675a769 100644 --- a/packages/scan/src/web/components/widget/toolbar/arrows.tsx +++ b/packages/scan/src/web/components/widget/toolbar/arrows.tsx @@ -66,7 +66,6 @@ export const Arrows = constant(() => { // biome-ignore lint/correctness/useExhaustiveDependencies: no deps useEffect(() => { const unsubscribe = Store.inspectState.subscribe((state) => { - if (state.kind === 'focused' && refButtonPrevious.current && refButtonNext.current) { refAllElements.current = getInspectableElements(); @@ -170,7 +169,8 @@ export const Arrows = constant(() => { className={cn( 'button', 'flex items-center justify-center', - 'px-3 opacity-50', + 'px-3', + 'opacity-50', 'transition-all duration-300', 'cursor-not-allowed', )} diff --git a/packages/scan/src/web/constants.ts b/packages/scan/src/web/constants.ts index 240a9400..07c83175 100644 --- a/packages/scan/src/web/constants.ts +++ b/packages/scan/src/web/constants.ts @@ -1,6 +1,6 @@ export const SAFE_AREA = 24; export const MIN_SIZE = { - width: 320, + width: 360, height: 36, } as const; diff --git a/packages/website/app/page.tsx b/packages/website/app/page.tsx index 069b18da..d33904d3 100644 --- a/packages/website/app/page.tsx +++ b/packages/website/app/page.tsx @@ -54,6 +54,10 @@ export default function Home() { {/* for testing purposes only + */} + {/* + for testing purposes only + */}
    diff --git a/packages/website/components/counter.tsx b/packages/website/components/counter.tsx new file mode 100644 index 00000000..0eb05f7e --- /dev/null +++ b/packages/website/components/counter.tsx @@ -0,0 +1,104 @@ +import { createContext, useContext, useState, ReactNode } from 'react'; + +interface CounterContextType { + count: number; + increment: () => void; + decrement: () => void; + reset: () => void; +} + +const CounterContext = createContext(undefined); + +export const CounterProvider = ({ children }: { children: ReactNode }) => { + const [count, setCount] = useState(0); + + const increment = () => setCount(prev => prev + 1); + const decrement = () => setCount(prev => prev - 1); + const reset = () => setCount(0); + + const value = { + count, + increment, + decrement, + reset, + }; + + return ( + + {children} + + ); +} + +const useCounter = () => { + const context = useContext(CounterContext); + if (context === undefined) { + throw new Error('useCounter must be used within a CounterProvider'); + } + return context; +} + +const CounterDisplay = () => { + const { count } = useCounter(); + return

    Counter: {count}

    ; +} + +const CounterButton = ({ + variant = 'primary', + className = '', + ...props +}) => { + const baseClasses = "px-4 py-2 text-white rounded hover:opacity-90"; + const variantClasses = variant === 'primary' + ? 'bg-blue-500 hover:bg-blue-600' + : 'bg-gray-500 hover:bg-gray-600'; + + return ( + - ) - } + {isExpandableValue && ( + + )}
    - { - isBadRender && + {isBadRender && !changedKeys.has(`${name}:memoized`) && !changedKeys.has(`${name}:unmemoized`) && ( + )} + { + changedKeys.has(`${name}:unmemoized`) && ( + + ) + } + { + changedKeys.has(`${name}:memoized`) && ( + ) } - {renderMemoizationIcon}
    {name}:
    { isEditing && isEditableValue(value, parentPath) @@ -1117,95 +1083,89 @@ const PropertySection = ({ title, section }: PropertySectionProps) => { const WhatChanged = constant(() => { const refPrevFiber = useRef(null); const [isExpanded, setIsExpanded] = useState(Store.wasDetailsOpen.value); - const [shouldShow, setShouldShow] = useState(false); - const { - fiber, - fiberProps, - fiberState, - fiberContext, - } = inspectorState.value; + const { fiber, fiberProps, fiberState, fiberContext } = inspectorState.value; const renderSection = useCallback(( sectionName: 'state' | 'props' | 'context', - items: Set, - getCount: (key: string) => number, + items: SectionData, ) => { - const elements = Array.from(items).reduce((acc, key) => { - if (sectionName === 'props') { - const isUnmemoized = key.endsWith(':unmemoized'); - if (isUnmemoized) { - acc.push( -
  • -
    - {key.split(':')[0]}{' '} - -
    -
  • , - ); - } - } - - const count = getCount(key); - if (count > 0) { - const displayKey = - sectionName === 'context' ? key.replace(/^context\./, '') : key; - acc.push( -
  • - {displayKey} ×{count} -
  • , - ); - } - - return acc; - }, []); + const elements = Array.from(items.changes) + .reduce((acc, key) => { + if (sectionName === 'props') { + const isUnmemoized = key.endsWith(':unmemoized'); + if (isUnmemoized) { + acc.push( +
  • +
    + {key.split(':')[0]}{' '} + +
    +
  • , + ); + return acc; + } + } - if (!elements.length) return null; + const count = items.changesCounts.get(key) ?? 0; + if (count > 0) { + const displayKey = + sectionName === 'context' ? key.replace(/^context\./, '') : key; - return ( - <> -
    {sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}:
    -
      {elements}
    - - ); - }, []); + acc.push( +
  • + {displayKey} ×{count} +
  • , + ); + } - const stateSection = useMemo( - () => renderSection('state', fiberState.changes, getStateChangeCount), - [fiberState.changes, renderSection], - ); + return acc; + }, []); - const propsSection = useMemo( - () => renderSection('props', fiberProps.changes, getPropsChangeCount), - [fiberProps.changes, renderSection], - ); + if (!elements.length) return null; - const contextSection = useMemo( - () => renderSection('context', fiberContext.changes, getContextChangeCount), - [fiberContext.changes, renderSection], - ); + return ( + <> +
    + {sectionName.charAt(0).toUpperCase() + sectionName.slice(1)}: +
    +
      {elements}
    + + ); + }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: const { hasChanges, sections } = useMemo(() => { - return { - hasChanges: !!(stateSection || propsSection || contextSection), - sections: [stateSection, propsSection, contextSection], - }; - }, [stateSection, propsSection, contextSection]); - - useEffect(() => { if (!refPrevFiber.current || refPrevFiber.current.type !== fiber?.type) { refPrevFiber.current = fiber; - setShouldShow(false); - return; + return { + hasChanges: false, + sections: [], + }; } - refPrevFiber.current = fiber; - setShouldShow(hasChanges); - }, [hasChanges, fiber]); + const hasChanges = !!( + fiberProps.changes.size > 0 || + fiberState.changes.size > 0 || + fiberContext.changes.size > 0 + ); + + const sections = [ + renderSection('props', fiberProps), + renderSection('state', fiberState), + renderSection('context', fiberContext), + ]; + + + return { + hasChanges, + sections, + }; + }, [fiberState, fiberProps, fiberContext]); const handleToggle = useCallback(() => { setIsExpanded((state) => { @@ -1216,14 +1176,11 @@ const WhatChanged = constant(() => { return (
    - {shouldShow && refPrevFiber.current?.type === fiber?.type && ( + {hasChanges && (
    e.key === 'Enter' && handleToggle()} @@ -1236,7 +1193,7 @@ const WhatChanged = constant(() => { 'opacity-0', 'transition-all duration-300 delay-300', { - 'opacity-100 delay-0': shouldShow, + 'opacity-100 delay-0': hasChanges, }, )} > @@ -1273,44 +1230,58 @@ const WhatChanged = constant(() => { export const Inspector = constant(() => { const refLastInspectedFiber = useRef(null); + const refPendingUpdates = useRef>(new Set()); const isSettingsOpen = signalIsSettingsOpen.value; useEffect(() => { - let isProcessing = false; - const pendingUpdates = new Set(); - const processNextUpdate = () => { - if (pendingUpdates.size === 0) { - isProcessing = false; - return; - } + const processUpdate = () => { + if (refPendingUpdates.current.size === 0) return; - const nextFiber = Array.from(pendingUpdates)[0]; - pendingUpdates.delete(nextFiber); + const fiber = Array.from(refPendingUpdates.current)[0]; + refPendingUpdates.current.clear(); - try { - refLastInspectedFiber.current = nextFiber; + const timeline = timelineState.value; + if (timeline.isReplaying) { + const update = timeline.updates[timeline.currentIndex]; + refLastInspectedFiber.current = update.fiber; inspectorState.value = { - fiber: nextFiber, - ...collectInspectorData(nextFiber), + fiber: update.fiber, + fiberProps: update.props, + fiberState: update.state, + fiberContext: update.context, }; - } finally { - if (pendingUpdates.size > 0) { - queueMicrotask(processNextUpdate); - } else { - isProcessing = false; - } + timelineState.value.isReplaying = false; + return; + } + + refLastInspectedFiber.current = fiber; + inspectorState.value = { + fiber, + ...collectInspectorData(fiber), + }; + + if (refPendingUpdates.current.size > 0) { + queueMicrotask(processUpdate); } }; + const scheduleUpdate = (fiber: Fiber) => { + refPendingUpdates.current.add(fiber); + queueMicrotask(processUpdate); + }; + const unSubState = Store.inspectState.subscribe((state) => { if (state.kind !== 'focused' || !state.focusedDomElement) { - pendingUpdates.clear(); + refPendingUpdates.current.clear(); + refLastInspectedFiber.current = null; + globalInspectorState.cleanup(); return; } + if (state.kind === 'focused') { signalIsSettingsOpen.value = false; } @@ -1320,37 +1291,19 @@ export const Inspector = constant(() => { ); if (!parentCompositeFiber) return; - pendingUpdates.clear(); - globalInspectorState.cleanup(); - refLastInspectedFiber.current = parentCompositeFiber; - - const { - fiberProps, - fiberState, - fiberContext, - } = collectInspectorData(parentCompositeFiber); + if (refLastInspectedFiber.current?.type !== parentCompositeFiber.type) { + refPendingUpdates.current.clear(); + globalInspectorState.cleanup(); + scheduleUpdate(parentCompositeFiber); + } - inspectorState.value = { - fiber: parentCompositeFiber, - fiberProps: { - ...fiberProps, - changes: new Set(), - }, - fiberState: { - ...fiberState, - changes: new Set(), - }, - fiberContext: { - ...fiberContext, - changes: new Set(), - }, - }; }); - const unSubReport = Store.lastReportTime.subscribe(() => { + const unSubLastReportTime = Store.lastReportTime.subscribe(() => { const inspectState = Store.inspectState.value; - if (inspectState.kind !== 'focused') { - pendingUpdates.clear(); + if (inspectState.kind !== 'focused' || !inspectState.focusedDomElement) { + refPendingUpdates.current.clear(); + refLastInspectedFiber.current = null; return; } @@ -1364,22 +1317,71 @@ export const Inspector = constant(() => { return; } - if (parentCompositeFiber.type === refLastInspectedFiber.current?.type) { - pendingUpdates.add(parentCompositeFiber); + scheduleUpdate(parentCompositeFiber); + + requestAnimationFrame(() => { + if (!element.isConnected) { + refPendingUpdates.current.clear(); + refLastInspectedFiber.current = null; + globalInspectorState.cleanup(); + Store.inspectState.value = { + kind: 'inspecting', + hoveredDomElement: null, + }; + } + }); + }); + + const unSubInspectorState = inspectorState.subscribe((state) => { + if (!state.fiber || !refLastInspectedFiber.current) return; + if (state.fiber.type !== refLastInspectedFiber.current.type) return; + if (timelineState.value.isReplaying) return; + + const update: TimelineUpdate = { + fiber: state.fiber, + timestamp: Date.now(), + props: state.fiberProps, + state: state.fiberState, + context: state.fiberContext, + stateNames: getStateNames(state.fiber), + }; + + const { updates, currentIndex, totalUpdates } = timelineState.value; + let newUpdates: TimelineUpdate[]; - if (!isProcessing) { - isProcessing = true; - queueMicrotask(processNextUpdate); + if (updates.length >= TIMELINE_MAX_UPDATES) { + if (currentIndex < updates.length - 1) { + newUpdates = [...updates.slice(0, currentIndex + 1), update].slice(-TIMELINE_MAX_UPDATES); + } else { + newUpdates = [...updates.slice(1), update]; + } + } else { + if (currentIndex < updates.length - 1) { + newUpdates = [...updates.slice(0, currentIndex + 1), update]; + } else { + newUpdates = [...updates, update]; } } + + const newIndex = newUpdates.length - 1; + const newTotal = currentIndex < updates.length - 1 + ? totalUpdates - (updates.length - currentIndex - 1) + 1 + : totalUpdates + 1; + + timelineState.value = { + ...timelineState.value, + updates: newUpdates, + currentIndex: newIndex, + totalUpdates: newTotal, + }; }); return () => { unSubState(); - unSubReport(); - pendingUpdates.clear(); + unSubLastReportTime(); + unSubInspectorState(); + refPendingUpdates.current.clear(); globalInspectorState.cleanup(); - resetStateTracking(); }; }, []); @@ -1394,10 +1396,12 @@ export const Inspector = constant(() => { 'transition-opacity duration-150 delay-0', 'pointer-events-none', { - 'opacity-100 delay-300 pointer-events-auto max-h-["auto"]': !isSettingsOpen, + 'opacity-100 delay-300 pointer-events-auto max-h-["auto"]': + !isSettingsOpen, }, )} > + {/* */} @@ -1412,7 +1416,15 @@ export const replayComponent = async (fiber: Fiber): Promise => { if (!overrideProps || !overrideHookState || !fiber) return; const currentProps = fiber.memoizedProps || {}; - for (const key of Object.keys(currentProps)) { + const propKeys = Object.keys(currentProps).filter((key) => { + const value = currentProps[key]; + if (Array.isArray(value) || typeof value === 'string') { + return !Number.isInteger(Number(key)) && key !== 'length'; + } + return true; + }); + + for (const key of propKeys) { try { overrideProps(fiber, [key], currentProps[key]); } catch {} @@ -1421,15 +1433,50 @@ export const replayComponent = async (fiber: Fiber): Promise => { const currentState = getCurrentFiberState(fiber); if (currentState) { const stateNames = getStateNames(fiber); + + // First, handle named state hooks for (const [key, value] of Object.entries(currentState)) { try { const namedStateIndex = stateNames.indexOf(key); - const hookId = namedStateIndex !== -1 ? namedStateIndex.toString() : '0'; - overrideHookState(fiber, hookId, [], value); + if (namedStateIndex !== -1) { + const hookId = namedStateIndex.toString(); + // For arrays and objects, we need to clone to trigger updates + const stateValue = Array.isArray(value) + ? [...value] + : typeof value === 'object' && value !== null + ? { ...value } + : value; + overrideHookState(fiber, hookId, [], stateValue); + } } catch {} } + + // Then handle unnamed state hooks + let hookIndex = 0; + let currentHook = fiber.memoizedState; + while (currentHook !== null) { + try { + const hookId = hookIndex.toString(); + const value = currentHook.memoizedState; + + // Only update if this hook isn't already handled by named states + if (!stateNames.includes(hookId)) { + // For arrays and objects, we need to clone to trigger updates + const stateValue = Array.isArray(value) + ? [...value] + : typeof value === 'object' && value !== null + ? { ...value } + : value; + overrideHookState(fiber, hookId, [], stateValue); + } + } catch {} + + currentHook = currentHook.next as typeof currentHook; + hookIndex++; + } } + // Recursively handle children let child = fiber.child; while (child) { await replayComponent(child); diff --git a/packages/scan/src/web/components/inspector/overlay/utils.ts b/packages/scan/src/web/components/inspector/overlay/utils.ts index 6a1b157c..6d7a7b83 100644 --- a/packages/scan/src/web/components/inspector/overlay/utils.ts +++ b/packages/scan/src/web/components/inspector/overlay/utils.ts @@ -27,6 +27,9 @@ interface ReactContext { const stateChangeCounts = new Map(); const propsChangeCounts = new Map(); const contextChangeCounts = new Map(); +const stateChanges = new Set(); +const propsChanges = new Set(); +const contextChanges = new Set(); let lastRenderedStates = new WeakMap>(); const STATE_NAME_REGEX = /\[(?\w+),\s*set\w+\]/g; @@ -38,7 +41,6 @@ export const isPromise = (value: unknown): value is Promise => { (value instanceof Promise || (typeof value === 'object' && 'then' in value)) ); }; - export const ensureRecord = ( value: unknown, maxDepth = 2, @@ -232,16 +234,12 @@ export const resetStateTracking = () => { stateChangeCounts.clear(); propsChangeCounts.clear(); contextChangeCounts.clear(); - lastRenderedStates = new WeakMap>(); + stateChanges.clear(); + propsChanges.clear(); + contextChanges.clear(); + lastRenderedStates = new WeakMap(); }; -export const getStateChangeCount = (name: string): number => - stateChangeCounts.get(name) ?? 0; -export const getPropsChangeCount = (name: string): number => - propsChangeCounts.get(name) ?? 0; -export const getContextChangeCount = (name: string): number => - contextChangeCounts.get(name) ?? 0; - export const getStateNames = (fiber: Fiber): Array => { const componentSource = fiber.type?.toString?.() || ''; return componentSource @@ -420,9 +418,10 @@ const getPropsOrder = (fiber: Fiber): Array => { .filter(Boolean); }; -interface SectionData { +export interface SectionData { current: Record; changes: Set; + changesCounts: Map; } export interface InspectorData { @@ -434,20 +433,43 @@ export interface InspectorData { export const collectInspectorData = (fiber: Fiber): InspectorData => { if (!fiber) { return { - fiberProps: { current: {}, changes: new Set() }, - fiberState: { current: {}, changes: new Set() }, - fiberContext: { current: {}, changes: new Set() }, + fiberProps: { + current: {}, + changes: new Set(), + changesCounts: new Map(), + }, + fiberState: { + current: {}, + changes: new Set(), + changesCounts: new Map(), + }, + fiberContext: { + current: {}, + changes: new Set(), + changesCounts: new Map(), + }, }; } const fiberProps = { current: {} as Record, - changes: new Set(), + changes: propsChanges, + changesCounts: new Map(), }; if (fiber.memoizedProps) { + const propsOrder = getPropsOrder(fiber); + const orderedProps = propsOrder.length + ? [ + ...propsOrder, + ...Object.keys(fiber.memoizedProps).filter( + (key) => !propsOrder.includes(key), + ), + ] + : Object.keys(fiber.memoizedProps); + const currentIsNewer = fiber?.alternate - ? (fiber.actualStartTime ?? 0) > (fiber.alternate?.actualStartTime ?? 0) + ? (fiber.actualStartTime ?? 0) > (fiber.alternate.actualStartTime ?? 0) : true; const baseProps = currentIsNewer @@ -456,36 +478,9 @@ export const collectInspectorData = (fiber: Fiber): InspectorData => { fiber.alternate?.pendingProps || fiber.memoizedProps; - const orderedProps = getPropsOrder(fiber); - const remainingProps = new Set(Object.keys(baseProps)); - for (const key of orderedProps) { - if (key in baseProps) { - const value = baseProps[key]; - fiberProps.current[key] = isPromise(value) - ? { type: 'promise', displayValue: 'Promise' } - : value; - - if ( - (value && typeof value === 'object') || - typeof value === 'function' - ) { - if (fiber.alternate?.memoizedProps) { - const prevValue = fiber.alternate.memoizedProps[key]; - const status = value === prevValue ? 'memoized' : 'unmemoized'; - propsChangeCounts.set(`${key}:${status}`, 0); - } - } - remainingProps.delete(key); - } - } - - for (const key of remainingProps) { const value = baseProps[key]; - fiberProps.current[key] = isPromise(value) - ? { type: 'promise', displayValue: 'Promise' } - : value; - + fiberProps.current[key] = value; if ((value && typeof value === 'object') || typeof value === 'function') { if (fiber.alternate?.memoizedProps) { const prevValue = fiber.alternate.memoizedProps[key]; @@ -495,8 +490,12 @@ export const collectInspectorData = (fiber: Fiber): InspectorData => { } } - const currentProps = fiber.memoizedProps; - for (const [key, currentValue] of Object.entries(currentProps)) { + for (const key of orderedProps) { + const currentValue = fiber.memoizedProps[key]; + fiberProps.current[key] = isPromise(currentValue) + ? { type: 'promise', displayValue: 'Promise' } + : currentValue; + if ( (currentValue && typeof currentValue === 'object') || typeof currentValue === 'function' @@ -504,26 +503,35 @@ export const collectInspectorData = (fiber: Fiber): InspectorData => { if (fiber.alternate?.memoizedProps) { const prevValue = fiber.alternate.memoizedProps[key]; const status = currentValue === prevValue ? 'memoized' : 'unmemoized'; - fiberProps.changes.add(`${key}:${status}`); + propsChanges.add(`${key}:${status}`); } continue; } - if ( - fiber.alternate?.memoizedProps && - key in fiber.alternate.memoizedProps && - !isEqual(fiber.alternate.memoizedProps[key], currentValue) - ) { - fiberProps.changes.add(key); - const count = (propsChangeCounts.get(key) || 0) + 1; - propsChangeCounts.set(key, count); + if (fiber.alternate?.memoizedProps) { + if (!isEqual(fiber.alternate.memoizedProps[key], currentValue)) { + if (propsChanges.has(key)) { + propsChanges.add(key); + const count = (propsChangeCounts.get(key) || 0) + 1; + propsChangeCounts.set(key, count); + } else { + propsChanges.add(key); + propsChangeCounts.set(key, 0); + } + } + } else { + propsChanges.add(key); + propsChangeCounts.set(key, 0); } } } + fiberProps.changesCounts = new Map(propsChangeCounts); + const fiberState = { current: {} as Record, - changes: new Set(), + changes: stateChanges, + changesCounts: stateChangeCounts, }; if (fiber.tag === FunctionComponentTag && isDirectComponent(fiber)) { @@ -573,9 +581,12 @@ export const collectInspectorData = (fiber: Fiber): InspectorData => { } } + fiberState.changesCounts = new Map(stateChangeCounts); + const fiberContext = { current: {} as Record, - changes: new Set(), + changes: contextChanges, + changesCounts: new Map(), }; const contexts = getAllFiberContexts(fiber); @@ -614,6 +625,8 @@ export const collectInspectorData = (fiber: Fiber): InspectorData => { } } + fiberContext.changesCounts = new Map(contextChangeCounts); + return { fiberProps, fiberState, diff --git a/packages/scan/src/web/components/inspector/states.ts b/packages/scan/src/web/components/inspector/states.ts new file mode 100644 index 00000000..4687e430 --- /dev/null +++ b/packages/scan/src/web/components/inspector/states.ts @@ -0,0 +1,74 @@ +import { signal } from '@preact/signals'; +import type { Fiber } from 'bippy'; +import { flashManager } from './flash-overlay'; +import { + type InspectorData, + type SectionData, + resetStateTracking, +} from './overlay/utils'; + +export interface TimelineUpdate { + fiber: Fiber; + timestamp: number; + props: SectionData; + state: SectionData; + context: SectionData; + stateNames: string[]; +} + +export interface TimelineState { + updates: TimelineUpdate[]; + currentIndex: number; + isReplaying: boolean; + totalUpdates: number; +} + +export const TIMELINE_MAX_UPDATES = 100; + +const timelineStateDefault: TimelineState = { + updates: [], + currentIndex: -1, + isReplaying: false, + totalUpdates: 0, +}; + +export const timelineState = signal(timelineStateDefault); + +interface InspectorState extends InspectorData { + fiber: Fiber | null; +} + +const inspectorStateDefault: InspectorState = { + fiber: null, + fiberProps: { + current: {}, + changes: new Set(), + changesCounts: new Map(), + }, + fiberState: { + current: {}, + changes: new Set(), + changesCounts: new Map(), + }, + fiberContext: { current: {}, changes: new Set(), changesCounts: new Map() }, +}; + +export const inspectorState = signal(inspectorStateDefault); + +export const globalInspectorState = { + lastRendered: new Map(), + expandedPaths: new Set(), + cleanup: () => { + globalInspectorState.lastRendered.clear(); + globalInspectorState.expandedPaths.clear(); + flashManager.cleanupAll(); + resetStateTracking(); + inspectorState.value = inspectorStateDefault; + timelineState.value = { + updates: [], + currentIndex: -1, + isReplaying: false, + totalUpdates: 0, + }; + }, +}; diff --git a/packages/scan/src/web/components/inspector/timeline.tsx b/packages/scan/src/web/components/inspector/timeline.tsx new file mode 100644 index 00000000..58409ce8 --- /dev/null +++ b/packages/scan/src/web/components/inspector/timeline.tsx @@ -0,0 +1,167 @@ +// TODO: @pivanov - improve UI and finish the implementation +import type { Fiber } from 'bippy'; +import { useCallback, useRef } from 'preact/hooks'; +import { Icon } from '~web/components/icon'; +import { collectInspectorData, getStateNames } from './overlay/utils'; +import { type TimelineUpdate, inspectorState, timelineState } from './states'; +import { getOverrideMethods } from './utils'; + +export const Timeline = () => { + const refLastFiber = useRef(null); + + const handleRecord = useCallback(() => { + if (!inspectorState.value.fiber) return; + + const fiber = inspectorState.value.fiber; + const inspectorData = collectInspectorData(fiber); + + const initialUpdate: TimelineUpdate = { + fiber, + timestamp: Date.now(), + props: inspectorData.fiberProps, + state: inspectorData.fiberState, + context: inspectorData.fiberContext, + stateNames: getStateNames(fiber), + }; + + timelineState.value = { + updates: [initialUpdate], + currentIndex: 0, + isReplaying: false, + totalUpdates: 1, + }; + refLastFiber.current = fiber; + }, []); + + const handleStop = useCallback(() => { + refLastFiber.current = null; + timelineState.value = { + updates: [], + currentIndex: -1, + isReplaying: false, + totalUpdates: 0, + }; + }, []); + + const updateFiber = useCallback(async (update: TimelineUpdate) => { + const { overrideProps, overrideHookState } = getOverrideMethods(); + if (!overrideProps || !overrideHookState || !update.fiber) return; + + // Apply props changes from stored props + const props = update.props.current; + for (const key of Object.keys(props)) { + try { + overrideProps(update.fiber, [key], props[key]); + } catch {} + } + + // Apply state changes from stored state + const state = update.state.current; + const stateNames = update.stateNames; + if (state && stateNames) { + for (const [key, value] of Object.entries(state)) { + try { + const namedStateIndex = stateNames.indexOf(key); + if (namedStateIndex !== -1) { + overrideHookState(update.fiber, namedStateIndex.toString(), [], value); + } + } catch {} + } + } + }, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: no deps + const handlePrevious = useCallback(async () => { + const { updates, currentIndex } = timelineState.value; + if (currentIndex <= 0) return; + + const newIndex = currentIndex - 1; + const update = updates[newIndex]; + + await updateFiber(update); + + timelineState.value = { + ...timelineState.value, + currentIndex: newIndex, + isReplaying: true, + }; + }, []); + + // biome-ignore lint/correctness/useExhaustiveDependencies: no deps + const handleNext = useCallback(async () => { + const { updates, currentIndex } = timelineState.value; + if (currentIndex >= updates.length - 1) return; + + const newIndex = currentIndex + 1; + const update = updates[newIndex]; + + await updateFiber(update); + + timelineState.value = { + ...timelineState.value, + currentIndex: newIndex, + isReplaying: true, + }; + }, []); + + const { updates, currentIndex, totalUpdates } = timelineState.value; + const isRecording = refLastFiber.current !== null; + + return ( +
    + {isRecording ? ( + + ) : ( + + )} + + {isRecording && ( + <> + +
    + {updates.length > 0 + ? `${currentIndex + 1}/${totalUpdates}` + : '0/0'} +
    + + + )} +
    + ); +};