diff --git a/.circleci/config.yml b/.circleci/config.yml index a054db739f..6e4d2b9f7b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,9 @@ jobs: build: docker: # specify the version you desire here + - image: node:current - image: vuejs/ci + resource_class: medium+ working_directory: ~/repo diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index b78fc6d2e7..0000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,119 +0,0 @@ -module.exports = { - root: true, - env: { - browser: true, - }, - extends: [ - 'plugin:vue/recommended', - '@vue/standard', - '@vue/typescript/recommended', - ], - parserOptions: { - ecmaVersion: 2020, - }, - globals: { - bridge: true, - chrome: true, - localStorage: 'off', - HTMLDocument: true, - name: 'off', - browser: true, - }, - rules: { - 'vue/html-closing-bracket-newline': [ - 'error', - { - singleline: 'never', - multiline: 'always', - }, - ], - 'no-var': ['error'], - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'none', - }, - singleline: { - delimiter: 'comma', - }, - }, - ], - '@typescript-eslint/ban-ts-comment': 'warn', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - camelcase: 'warn', - 'no-prototype-builtins': 'off', - 'no-use-before-define': 'off', - 'no-console': ['error', { allow: ['warn', 'error'] }], - 'comma-dangle': ['error', 'always-multiline'], - quotes: ['error', 'single', { allowTemplateLiterals: true }], - }, - ignorePatterns: [ - 'node_modules/', - '/packages/*/lib/', - 'dist/', - 'build/', - 'build-node/', - '/legacy', - ], - overrides: [ - { - files: [ - 'release.js', - 'sign-firefox.js', - 'extension-zips.js', - 'packages/build-tools/**', - 'packages/shell-electron/**', - '**webpack.config.js', - ], - rules: { - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/camelcase': 'off', - }, - }, - { - files: ['packages/shell-dev-vue3/**'], - rules: { - 'vue/valid-template-root': 'off', - }, - }, - { - files: [ - 'packages/shell-dev-vue2/**', - 'packages/shell-dev-vue3/**', - ], - rules: { - '@typescript-eslint/no-unused-vars': 'off', - 'vue/require-default-prop': 'off', - 'vue/require-prop-types': 'off', - 'no-console': 'off', - }, - }, - { - files: [ - 'packages/shell-host/**', - ], - globals: { - localStorage: false, - }, - rules: { - 'no-console': 'off', - }, - }, - { - files: [ - 'packages/app-backend-core/src/hook.ts', - ], - rules: { - 'no-restricted-syntax': ['error', { - selector: 'ImportDeclaration', - message: 'File is injected with a `Function.toString()`, imports will not work', - }, { - selector: `CallExpression[callee.name='require']`, - message: 'File is injected with a `Function.toString()`, require will not work', - }], - }, - }, - ], -} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 73037bae7c..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..ebd154bd1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,78 @@ +name: 🐞 Bug report +description: Create a report to help us improve +body: + - type: markdown + attributes: + value: | + **Before You Start...** + + This form is only for submitting bug reports. If you have a usage question + or are unsure if this is really a bug, make sure to: + + - Read the [docs](https://devtools.vuejs.org/) + - Read the [FAQ](https://devtools.vuejs.org/guide/faq.html) + - Ask on [GitHub Discussions](https://github.com/vuejs/devtools/discussions) + - Ask on [Discord Chat](https://chat.vuejs.org/) + + Also try to search for your issue - it may have already been answered or even fixed in the development branch. + However, if you find that an old, closed issue still persists in the latest version, + you should open a new issue using the form below instead of commenting on the old issue. + - type: input + id: version + attributes: + label: Vue devtools version + description: | + Open your browser Extensions page and find the Vue devtools extension. Then write in this field the version number shown next to the Vue devtools extension name. + validations: + required: true + - type: input + id: reproduction-link + attributes: + label: Link to minimal reproduction + description: | + The easiest way to provide a reproduction is by showing the bug in [The SFC Playground](https://sfc.vuejs.org/). + If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://vite.new/vue). + If neither of these are suitable, you can always provide a GitHub repository. + + The reproduction should be **minimal** - i.e. it should contain only the bare minimum amount of code needed + to show the bug. See [Bug Reproduction Guidelines](https://github.com/vuejs/core/blob/main/.github/bug-repro-guidelines.md) for more details. + + Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided. + placeholder: Reproduction Link + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce & screenshots + description: | + What do we need to do after opening your repro in order to make the bug happen in the devtools? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can upload screenshots and use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code. + placeholder: Steps to reproduce + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is expected? + validations: + required: true + - type: textarea + id: actually-happening + attributes: + label: What is actually happening? + validations: + required: true + - type: textarea + id: system-info + attributes: + label: System Info + description: Output of `npx envinfo --system --npmPackages vue --binaries --browsers` + render: shell + placeholder: System, Binaries, Browsers + validations: + required: true + - type: textarea + id: additional-comments + attributes: + label: Any additional comments? + description: e.g. some background/context of how you ran into this bug. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3a4001789a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: I have a performance issue + url: https://devtools.vuejs.org/guide/devtools-perf.html + about: Follow the guide to share performance profiling data with us! + - name: Questions & Discussions + url: https://github.com/vuejs/devtools/discussions + about: Use GitHub discussions for message-board style questions and discussions. + - name: Discord Chat + url: https://chat.vuejs.org + about: Ask questions and discuss with other Vue users in real time. + - name: GitHub Sponsor + url: https://github.com/sponsors/Akryum + about: Love the Vue devtools? Please consider supporting us via GitHub sponsors. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..b52c4120f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,28 @@ +name: 🚀 New feature proposal +description: Suggest an idea for this project +labels: [':sparkles: feature request'] +body: + - type: markdown + attributes: + value: | + **Before You Start...** + + This form is only for submitting feature requests. If you have a usage question + or are unsure if this is really a bug, make sure to: + + - Read the [docs](https://devtools.vuejs.org/) + - Read the [FAQ](https://devtools.vuejs.org/guide/faq.html) + - Ask on [GitHub Discussions](https://github.com/vuejs/devtools/discussions) + - Ask on [Discord Chat](https://chat.vuejs.org/) + + Also try to search for your issue - another user may have already requested something similar! + + - type: textarea + id: problem-description + attributes: + label: What problem does this feature solve? + description: | + Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature? + placeholder: Problem description + validations: + required: true diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 8c642148d7..43d3384ef8 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -1,56 +1,9 @@ -name: Create Release +name: Create release on: push: tags: - - "v*" - -# jobs: -# build: -# runs-on: ubuntu-latest -# steps: -# - name: Checkout -# uses: actions/checkout@v2 -# with: -# fetch-depth: 0 - -# - name: Set up Node.js -# uses: actions/setup-node@v2 -# with: -# node-version: 14.x - -# - name: Set up lerna yarn cache -# uses: actions/cache@v2 -# with: -# path: | -# node_modules -# */*/node_modules -# ~/.cache/yarn -# ~/.cache/Cypress -# key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - -# - name: Install yarn dependencies -# if: steps.cache.outputs.cache-hit != 'true' -# run: yarn install --pure-lockfile - -# - name: Build the extension -# run: yarn run build && node release.js && yarn run test && yarn run zip - -# - name: Create Release for Tag -# id: release_tag -# uses: Akryum/release-tag@conventional -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# with: -# tag_name: ${{ github.ref }} -# preset: angular - -# - name: Upload artifacts -# uses: softprops/action-gh-release@v1 -# if: startsWith(github.ref, 'refs/tags/') -# with: -# files: | -# dist/devtools-* + - 'v*' jobs: build: @@ -64,10 +17,8 @@ jobs: - name: Create Release for Tag id: release_tag - uses: Akryum/release-tag@conventional + uses: Akryum/release-tag@v4.0.7 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} - preset: angular # Use conventional-changelog preset - diff --git a/.vscode/settings.json b/.vscode/settings.json index 3662b3700e..25fa6215fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/README.md b/README.md index a4c634dbae..ffccc237d8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# Try the next iteration of Vue Devtools! + +We have a brand new version of Devtools being developed at [vuejs/devtools-next](https://github.com/vuejs/devtools-next). It is now in beta, please help us [test it out](https://devtools-next.vuejs.org/getting-started/installation)! + +--- + # vue-devtools ![screenshot](./media/screenshot-shadow.png) diff --git a/babel.config.js b/babel.config.js index bc074679d5..638ae723fc 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,7 +2,8 @@ module.exports = { root: true, presets: [ [ - '@babel/env', { + '@babel/env', + { modules: false, }, ], diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index 0c938b0ea9..4a30a523cf 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -3,7 +3,7 @@ module.exports = { 'cypress', ], env: { - mocha: true, + 'mocha': true, 'cypress/globals': true, }, rules: { diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json index da18d9352a..02e4254378 100644 --- a/cypress/fixtures/example.json +++ b/cypress/fixtures/example.json @@ -2,4 +2,4 @@ "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file +} diff --git a/cypress/integration/components-tab.js b/cypress/integration/components-tab.js index b46356bd03..953668254b 100644 --- a/cypress/integration/components-tab.js +++ b/cypress/integration/components-tab.js @@ -63,7 +63,7 @@ suite('components tab', () => { cy.get('.data-el.props .data-field:nth-child(2)').contains('msg:"hi"') cy.get('.data-el.props .data-field:nth-child(3)').contains('obj:undefined') // Regexp - cy.get('.data-el.data .data-field:nth-child(8)').then(el => { + cy.get('.data-el.data .data-field:nth-child(8)').then((el) => { expect(el.text()).to.include('regex:/(a\\w+b)/g') }) // Literals @@ -106,7 +106,7 @@ suite('components tab', () => { .click({ force: true }) }) cy.get('.action-header .title').contains('Mine') - cy.get('.tree').then(el => { + cy.get('.tree').then((el) => { expect(el.text()).to.include('') }) }) diff --git a/cypress/integration/vuex-tab.js b/cypress/integration/vuex-tab.js index 3af2f97c17..9ab0214f1d 100644 --- a/cypress/integration/vuex-tab.js +++ b/cypress/integration/vuex-tab.js @@ -54,13 +54,13 @@ suite('vuex tab', () => { .should('not.have.class', 'active') cy.get('.recording-vuex-state').should('not.be.visible') cy.get('.loading-vuex-state').should('not.be.visible') - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('type:"INCREMENT"') expect(el.text()).to.include('count:2') expect(el.text()).to.include('Error from getter') }) cy.get('.data-field .key').contains('lastCountPayload').click() - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('a:1') expect(el.text()).to.include('b:Object') }) @@ -88,7 +88,7 @@ suite('vuex tab', () => { cy.get('.recording-vuex-state').should('not.be.visible') cy.get('.loading-vuex-state').should('not.be.visible') cy.get('.recording-vuex-state').should('not.be.visible') - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('type:"INCREMENT"') expect(el.text()).to.include('count:1') }) @@ -111,7 +111,7 @@ suite('vuex tab', () => { cy.get('.history .entry[data-index="0"]') .should('have.class', 'inspected') .should('not.have.class', 'active') - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('count:0') }) cy.get('#target').iframe().then(({ get }) => { @@ -133,7 +133,7 @@ suite('vuex tab', () => { cy.get('.history .entry[data-index="4"]') .should('have.class', 'inspected') .should('have.class', 'active') - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('count:2') }) cy.get('#target').iframe().then(({ get }) => { @@ -147,7 +147,7 @@ suite('vuex tab', () => { cy.get('.history .entry[data-index="0"]') .should('have.class', 'inspected') .should('have.class', 'active') - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('count:2') }) cy.get('#target').iframe().then(({ get }) => { @@ -204,9 +204,9 @@ suite('vuex tab', () => { cy.wait(500) cy.get('.message.invalid-json').should('not.be.visible') cy.wait(500) - cy.get('.vuex-state-inspector').then(el => { + cy.get('.vuex-state-inspector').then((el) => { expect(el.text()).to.include('count:42') - expect(el.text()).to.include('date:' + new Date('Fri Dec 22 2017 10:12:04 GMT+0100 (CET)')) + expect(el.text()).to.include(`date:${new Date('Fri Dec 22 2017 10:12:04 GMT+0100 (CET)')}`) }) cy.get('.import').click() cy.get('.import-state').should('not.be.visible') diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 2b174d5033..1cb1467bff 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -11,7 +11,7 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) -module.exports = (on, config) => { +module.exports = (_on, _config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config diff --git a/cypress/support/commands.js b/cypress/support/commands.js index de6b360e0a..336ac74b8e 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -25,14 +25,14 @@ // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) Cypress.Commands.add('vueCheckInit', () => { - cy.get('.message .text').should('be.visible', { timeout: 10000 }).then(el => { + cy.get('.message .text').should('be.visible', { timeout: 10000 }).then((el) => { expect(el.text()).to.include('Ready. Detected Vue') }) cy.get('.instance').eq(0).contains('Root') }) // Add iframe support until becomes part of the framework -Cypress.Commands.add('iframe', { prevSubject: 'element' }, $iframe => { +Cypress.Commands.add('iframe', { prevSubject: 'element' }, ($iframe) => { const get = selector => cy.wait(500).wrap($iframe.contents().find(selector)) const el = $iframe[0] @@ -40,7 +40,7 @@ Cypress.Commands.add('iframe', { prevSubject: 'element' }, $iframe => { if (iframeDoc.readyState === 'complete') { return Cypress.Promise.resolve({ body: $iframe.contents().find('body'), get }) } - return new Cypress.Promise(resolve => { + return new Cypress.Promise((resolve) => { $iframe.on('load', () => { resolve({ body: $iframe.contents().find('body'), get }) }) diff --git a/cypress/utils/suite.js b/cypress/utils/suite.js index 184eb4bdbe..d1eb6f127c 100644 --- a/cypress/utils/suite.js +++ b/cypress/utils/suite.js @@ -1,4 +1,4 @@ -export function suite (description, tests) { +export function suite(description, tests) { describe(description, () => { before(() => { cy.visit('/') diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000..82cafd35fd --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,43 @@ +const antfu = require('@antfu/eslint-config').default + +module.exports = antfu({ + ignores: [ + '**/dist', + ], +}, { + rules: { + 'curly': ['error', 'all'], + 'node/prefer-global/process': 'off', + }, +}, { + files: [ + 'packages/shell-dev*/**', + ], + rules: { + 'no-console': 'off', + 'unused-imports/no-unused-vars': 'off', + 'vue/require-explicit-emits': 'off', + 'vue/custom-event-name-casing': 'off', + 'vue/no-deprecated-functional-template': 'off', + 'vue/no-deprecated-filter': 'off', + 'vue/no-unused-refs': 'off', + 'vue/require-component-is': 'off', + 'vue/return-in-computed-property': 'off', + }, +}, { + files: [ + 'packages/shell-host/**', + ], + rules: { + 'no-console': 'off', + }, +}, { + files: [ + 'package.json', + 'packages/*/package.json', + 'packages/*/manifest.json', + ], + rules: { + 'style/eol-last': 'off', + }, +}) diff --git a/extension-zips.js b/extension-zips.js index 4cc14900c9..b678ce9ce2 100644 --- a/extension-zips.js +++ b/extension-zips.js @@ -1,7 +1,9 @@ // require modules -const fs = require('fs') -const path = require('path') +const fs = require('node:fs') +const path = require('node:path') +const process = require('node:process') const archiver = require('archiver') + const IS_CI = !!(process.env.CIRCLECI || process.env.GITHUB_ACTIONS) const ProgressBar = !IS_CI ? require('progress') : {} const readDirGlob = !IS_CI ? require('readdir-glob') : {} @@ -18,18 +20,20 @@ const INCLUDE_GLOBS = [ // SKIP_GLOBS makes glob searches more efficient const SKIP_DIR_GLOBS = ['node_modules', 'src'] -function bytesToSize (bytes) { +function bytesToSize(bytes) { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] - if (bytes === 0) return '0 Byte' - const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) - return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i] + if (bytes === 0) { + return '0 Byte' + } + const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) + return `${Math.round(bytes / 1024 ** i, 2)} ${sizes[i]}` } (async () => { await writeZip('devtools-chrome.zip', 'shell-chrome') - await writeZip('devtools-firefox.zip', 'shell-chrome') + await writeZip('devtools-firefox.zip', 'shell-firefox') - async function writeZip (fileName, packageDir) { + async function writeZip(fileName, packageDir) { // create a file to stream archive data to. const output = fs.createWriteStream(path.join(__dirname, 'dist', fileName)) const archive = archiver('zip', { @@ -45,14 +49,15 @@ function bytesToSize (bytes) { tSize: '0 Bytes', } - async function parseFileStats () { + async function parseFileStats() { return new Promise((resolve, reject) => { - const globber = readDirGlob(path.join('packages', packageDir), - { pattern: INCLUDE_GLOBS, skip: SKIP_DIR_GLOBS, mark: true, stat: true }) - globber.on('match', match => { - if (!match.stat.isDirectory()) status.total++ + const globber = readDirGlob(path.join('packages', packageDir), { pattern: INCLUDE_GLOBS, skip: SKIP_DIR_GLOBS, mark: true, stat: true }) + globber.on('match', (match) => { + if (!match.stat.isDirectory()) { + status.total++ + } }) - globber.on('error', err => { + globber.on('error', (err) => { reject(err) }) globber.on('end', () => { @@ -60,7 +65,7 @@ function bytesToSize (bytes) { }) }) } - await parseFileStats().catch(err => { + await parseFileStats().catch((err) => { console.error(err) process.exit(1) }) @@ -77,7 +82,7 @@ function bytesToSize (bytes) { const n = entry.name status.written++ status.cFile = n.length > 14 - ? '...' + n.slice(n.length - 11) + ? `...${n.slice(n.length - 11)}` : n status.cSize = bytesToSize(entry.stats.size) status.tBytes += entry.stats.size @@ -101,12 +106,12 @@ function bytesToSize (bytes) { // This event is fired when the data source is drained no matter what was the data source. // It is not part of this library but rather from the NodeJS Stream API. // @see: https://nodejs.org/api/stream.html#stream_event_end - output.on('end', function () { + output.on('end', () => { 'nothing' }) // good practice to catch warnings (ie stat failures and other non-blocking errors) - archive.on('warning', function (err) { + archive.on('warning', (err) => { if (err.code !== 'ENOENT') { // throw error console.error(err) @@ -115,7 +120,7 @@ function bytesToSize (bytes) { }) // good practice to catch this error explicitly - archive.on('error', function (err) { + archive.on('error', (err) => { console.error(err) process.exit(1) }) @@ -123,7 +128,7 @@ function bytesToSize (bytes) { // pipe archive data to the file archive.pipe(output) - INCLUDE_GLOBS.forEach(glob => { + INCLUDE_GLOBS.forEach((glob) => { // append files from a glob pattern archive.glob(glob, { cwd: path.join('packages', packageDir), skip: SKIP_DIR_GLOBS }) }) diff --git a/package.json b/package.json index b1e622ab04..0850c64993 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,35 @@ { "name": "vue-devtools", - "version": "6.2.1", - "description": "devtools for Vue.js!", + "version": "6.6.4", "private": true, + "description": "devtools for Vue.js!", "workspaces": [ "packages/*" ], + "author": "Evan You", + "license": "MIT", + "homepage": "https://github.com/vuejs/vue-devtools#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/vue-devtools.git" + }, + "bugs": { + "url": "https://github.com/vuejs/vue-devtools/issues" + }, + "engines": { + "node": ">=8.10" + }, "scripts": { "dev:vue2": "concurrently \"cd packages/shell-dev-vue2 && yarn dev\" \"cd packages/shell-host && yarn dev\"", "dev:vue3": "concurrently \"cd packages/shell-dev-vue3 && yarn dev\" \"cd packages/shell-host && yarn dev\"", "dev:chrome": "cd packages/shell-chrome && webpack --watch", "dev:chrome:prod": "cd packages/shell-chrome && cross-env NODE_ENV=production webpack --watch", + "dev:firefox": "cd packages/shell-firefox && webpack --watch", "dev:electron": "cd packages/shell-electron && npm run dev", "build": "lerna run build", "build:watch": "lerna run build --scope @vue-devtools/app-backend* --scope @vue-devtools/shared-* --scope @vue/devtools-api && lerna run build:watch --stream --no-sort --concurrency 99", - "lint": "eslint --ext .js,.ts,.vue .", - "run:firefox": "web-ext run -s packages/shell-chrome -a dist -i src", + "lint": "eslint .", + "run:firefox": "web-ext run -s packages/shell-firefox -a dist -i src -u http://localhost:8090/target.html", "zip": "node ./extension-zips.js", "sign:firefox": "node ./sign-firefox.js", "release": "npm run test && node release.js && npm run build && npm run zip && npm run pub", @@ -32,36 +46,17 @@ "docs:build": "cd packages/docs && vitepress build src", "docs:serve": "cd packages/docs && vitepress serve src" }, - "repository": { - "type": "git", - "url": "git+https://github.com/vuejs/vue-devtools.git" - }, - "author": "Evan You", - "license": "MIT", - "bugs": { - "url": "https://github.com/vuejs/vue-devtools/issues" - }, - "homepage": "https://github.com/vuejs/vue-devtools#readme", "devDependencies": { + "@antfu/eslint-config": "^2.19.1", "@tailwindcss/postcss7-compat": "^2.0.4", "@types/chrome": "^0.0.139", "@types/speakingurl": "^13.0.3", - "@typescript-eslint/eslint-plugin": "^4.23.0", - "@typescript-eslint/parser": "^4.23.0", - "@vue/eslint-config-standard": "^6.0.0", - "@vue/eslint-config-typescript": "^7.0.0", "archiver": "^5.3.0", "autoprefixer": "^9.1.5", "concurrently": "^5.1.0", "cross-env": "^5.2.0", "cypress": "^3.1.0", - "eslint": "^7.26.0", - "eslint-plugin-cypress": "^2.0.1", - "eslint-plugin-import": "^2.20.2", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-standard": "^5.0.0", - "eslint-plugin-vue": "^6.0.0", + "eslint": "^9.3.0", "execa": "^4.0.3", "inquirer": "^6.2.0", "lerna": "^4.0.0", @@ -69,15 +64,13 @@ "rimraf": "^3.0.2", "semver": "^5.5.1", "start-server-and-test": "^1.7.1", + "svg-inline-loader": "^0.8.2", "tailwindcss": "npm:@tailwindcss/postcss7-compat", - "vue-loader": "^15.9.0", - "webpack-dev-server": "^4.0.0-beta.0" + "vue-loader": "^17.2.2", + "webpack-dev-server": "^4.15.1" }, "resolutions": { "cypress": "=3.4.1", - "webpack-dev-server": "=4.0.0-rc.0" - }, - "engines": { - "node": ">=8.10" + "webpack-dev-server": "^4.15.1" } } \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 3f1dc67517..85e97065ca 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,25 +1,25 @@ { "name": "@vue/devtools-api", - "version": "6.2.1", + "version": "6.6.4", "description": "Interact with the Vue devtools from the page", - "main": "lib/cjs/index.js", - "browser": "lib/esm/index.js", - "module": "lib/esm/index.js", - "types": "lib/esm/index.d.ts", - "sideEffects": false, "author": { "name": "Guillaume Chau" }, - "files": [ - "lib/esm", - "lib/cjs" - ], "license": "MIT", "repository": { "url": "https://github.com/vuejs/vue-devtools.git", "type": "git", "directory": "packages/api" }, + "sideEffects": false, + "main": "lib/cjs/index.js", + "browser": "lib/esm/index.js", + "module": "lib/esm/index.js", + "types": "lib/esm/index.d.ts", + "files": [ + "lib/cjs", + "lib/esm" + ], "publishConfig": { "access": "public" }, @@ -30,8 +30,8 @@ "build:watch": "yarn tsc --module es2015 --outDir lib/esm -d -w --sourceMap" }, "devDependencies": { - "@types/node": "^13.9.1", + "@types/node": "^20.11.16", "@types/webpack-env": "^1.15.1", - "typescript": "^4.5.2" + "typescript": "^5.3.3" } } \ No newline at end of file diff --git a/packages/api/src/api/api.ts b/packages/api/src/api/api.ts index 0e6f73c468..ab46a7e4d9 100644 --- a/packages/api/src/api/api.ts +++ b/packages/api/src/api/api.ts @@ -6,24 +6,24 @@ import type { ID } from './util.js' export interface DevtoolsPluginApi { on: Hookable - notifyComponentUpdate (instance?: ComponentInstance): void - addTimelineLayer (options: TimelineLayerOptions): void - addTimelineEvent (options: TimelineEventOptions): void - addInspector (options: CustomInspectorOptions): void - sendInspectorTree (inspectorId: string): void - sendInspectorState (inspectorId: string): void - selectInspectorNode (inspectorId: string, nodeId: string): void - getComponentBounds (instance: ComponentInstance): Promise - getComponentName (instance: ComponentInstance): Promise - getComponentInstances (app: App): Promise - highlightElement (instance: ComponentInstance): void - unhighlightElement (): void - getSettings (pluginId?: string): TSettings - now (): number + notifyComponentUpdate: (instance?: ComponentInstance) => void + addTimelineLayer: (options: TimelineLayerOptions) => void + addTimelineEvent: (options: TimelineEventOptions) => void + addInspector: (options: CustomInspectorOptions) => void + sendInspectorTree: (inspectorId: string) => void + sendInspectorState: (inspectorId: string) => void + selectInspectorNode: (inspectorId: string, nodeId: string) => void + getComponentBounds: (instance: ComponentInstance) => Promise + getComponentName: (instance: ComponentInstance) => Promise + getComponentInstances: (app: App) => Promise + highlightElement: (instance: ComponentInstance) => void + unhighlightElement: () => void + getSettings: (pluginId?: string) => TSettings + now: () => number /** - * @private Not implemented yet + * @private */ - setSettings (values: TSettings): void + setSettings: (values: TSettings) => void } export interface AppRecord { diff --git a/packages/api/src/api/component.ts b/packages/api/src/api/component.ts index 5e545bf0f5..9b6d934865 100644 --- a/packages/api/src/api/component.ts +++ b/packages/api/src/api/component.ts @@ -17,6 +17,7 @@ export interface ComponentTreeNode { isRouterView?: boolean macthedRouteSegment?: string tags: InspectorNodeTag[] + autoOpen: boolean meta?: any } @@ -55,7 +56,7 @@ export interface ComponentCustomState extends ComponentStateBase { value: CustomState } -export type CustomState = { +export interface CustomState { _custom: { type: ComponentBuiltinCustomStateTypes | string objectType?: string diff --git a/packages/api/src/api/hooks.ts b/packages/api/src/api/hooks.ts index dededdc1a0..8a88a619f7 100644 --- a/packages/api/src/api/hooks.ts +++ b/packages/api/src/api/hooks.ts @@ -1,7 +1,8 @@ -import type { ComponentTreeNode, InspectedComponentData, ComponentInstance, ComponentDevtoolsOptions } from './component.js' +import type { ComponentDevtoolsOptions, ComponentInstance, ComponentTreeNode, InspectedComponentData } from './component.js' import type { App } from './app.js' import type { CustomInspectorNode, CustomInspectorState, TimelineEvent } from './api.js' +// eslint-disable-next-line no-restricted-syntax export const enum Hooks { TRANSFORM_CALL = 'transformCall', GET_APP_RECORD_NAME = 'getAppRecordName', @@ -34,7 +35,7 @@ export interface ComponentBounds { height: number } -export type HookPayloads = { +export interface HookPayloads { [Hooks.TRANSFORM_CALL]: { callName: string inArgs: any[] @@ -56,6 +57,7 @@ export type HookPayloads = { componentTreeData: ComponentTreeNode[] maxDepth: number filter: string + recursively: boolean } [Hooks.VISIT_COMPONENT_TREE]: { app: App @@ -160,26 +162,26 @@ export type EditStatePayload = { export type HookHandler = (payload: TPayload, ctx: TContext) => void | Promise export interface Hookable { - transformCall (handler: HookHandler) - getAppRecordName (handler: HookHandler) - getAppRootInstance (handler: HookHandler) - registerApplication (handler: HookHandler) - walkComponentTree (handler: HookHandler) - visitComponentTree (handler: HookHandler) - walkComponentParents (handler: HookHandler) - inspectComponent (handler: HookHandler) - getComponentBounds (handler: HookHandler) - getComponentName (handler: HookHandler) - getComponentInstances (handler: HookHandler) - getElementComponent (handler: HookHandler) - getComponentRootElements (handler: HookHandler) - editComponentState (handler: HookHandler) - getComponentDevtoolsOptions (handler: HookHandler) - getComponentRenderCode (handler: HookHandler) - inspectTimelineEvent (handler: HookHandler) - timelineCleared (handler: HookHandler) - getInspectorTree (handler: HookHandler) - getInspectorState (handler: HookHandler) - editInspectorState (handler: HookHandler) - setPluginSettings (handler: HookHandler) + transformCall: (handler: HookHandler) => any + getAppRecordName: (handler: HookHandler) => any + getAppRootInstance: (handler: HookHandler) => any + registerApplication: (handler: HookHandler) => any + walkComponentTree: (handler: HookHandler) => any + visitComponentTree: (handler: HookHandler) => any + walkComponentParents: (handler: HookHandler) => any + inspectComponent: (handler: HookHandler) => any + getComponentBounds: (handler: HookHandler) => any + getComponentName: (handler: HookHandler) => any + getComponentInstances: (handler: HookHandler) => any + getElementComponent: (handler: HookHandler) => any + getComponentRootElements: (handler: HookHandler) => any + editComponentState: (handler: HookHandler) => any + getComponentDevtoolsOptions: (handler: HookHandler) => any + getComponentRenderCode: (handler: HookHandler) => any + inspectTimelineEvent: (handler: HookHandler) => any + timelineCleared: (handler: HookHandler) => any + getInspectorTree: (handler: HookHandler) => any + getInspectorState: (handler: HookHandler) => any + editInspectorState: (handler: HookHandler) => any + setPluginSettings: (handler: HookHandler) => any } diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index 84b661dd33..f5916ed2f2 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -1,5 +1,5 @@ -import type { PluginDescriptor, SetupFunction } from './index.js' import type { ApiProxy } from './proxy.js' +import type { PluginDescriptor, SetupFunction } from './index.js' export interface PluginQueueItem { pluginDescriptor: PluginDescriptor @@ -12,16 +12,16 @@ interface GlobalTarget { __VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__?: boolean } -export function getDevtoolsGlobalHook (): any { +export function getDevtoolsGlobalHook(): any { return (getTarget() as any).__VUE_DEVTOOLS_GLOBAL_HOOK__ } -export function getTarget (): GlobalTarget { - // @ts-ignore +export function getTarget(): GlobalTarget { + // @ts-expect-error navigator and windows are not available in all environments return (typeof navigator !== 'undefined' && typeof window !== 'undefined') ? window - : typeof global !== 'undefined' - ? global + : typeof globalThis !== 'undefined' + ? globalThis : {} } diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 0ef4e1cbb2..c929458de9 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,8 +1,8 @@ -import { getTarget, getDevtoolsGlobalHook, isProxyAvailable } from './env.js' +import { getDevtoolsGlobalHook, getTarget, isProxyAvailable } from './env.js' import { HOOK_SETUP } from './const.js' import type { DevtoolsPluginApi } from './api/index.js' import { ApiProxy } from './proxy.js' -import type { PluginDescriptor, ExtractSettingsTypes, PluginSettingsItem } from './plugin.js' +import type { ExtractSettingsTypes, PluginDescriptor, PluginSettingsItem } from './plugin.js' export * from './api/index.js' export * from './plugin.js' @@ -12,15 +12,13 @@ export { PluginQueueItem } from './env.js' // https://github.com/microsoft/TypeScript/issues/30680#issuecomment-752725353 type Cast = A extends B ? A : B type Narrowable = -| string -| number -| bigint -| boolean -type Narrow = Cast }) -> + | string + | number + | bigint + | boolean +type Narrow = Cast })> // Prevent properties not in PluginDescriptor // We need this because of the `extends` in the generic TDescriptor @@ -32,15 +30,16 @@ export type SetupFunction = (api: DevtoolsPluginApi) export function setupDevtoolsPlugin< TDescriptor extends Exact, - TSettings = ExtractSettingsTypes ? S : Record : Record>, -> (pluginDescriptor: Narrow, setupFn: SetupFunction) { + TSettings = ExtractSettingsTypes ? S : Record : Record>, +>(pluginDescriptor: Narrow, setupFn: SetupFunction) { const descriptor = pluginDescriptor as unknown as PluginDescriptor const target = getTarget() const hook = getDevtoolsGlobalHook() const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) { hook.emit(HOOK_SETUP, pluginDescriptor, setupFn) - } else { + } + else { const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [] @@ -50,6 +49,8 @@ export function setupDevtoolsPlugin< proxy, }) - if (proxy) setupFn(proxy.proxiedTarget as DevtoolsPluginApi) + if (proxy) { + setupFn(proxy.proxiedTarget as DevtoolsPluginApi) + } } } diff --git a/packages/api/src/plugin.ts b/packages/api/src/plugin.ts index 328f5f4fd0..f087187936 100644 --- a/packages/api/src/plugin.ts +++ b/packages/api/src/plugin.ts @@ -35,17 +35,17 @@ export type PluginSettingsItem = { }) type InferSettingsType< - T extends PluginSettingsItem + T extends PluginSettingsItem, > = [T] extends [{ type: 'boolean' }] ? boolean : [T] extends [{ type: 'choice' }] - ? T['options'][number]['value'] - : [T] extends [{ type: 'text' }] - ? string - : unknown + ? T['options'][number]['value'] + : [T] extends [{ type: 'text' }] + ? string + : unknown export type ExtractSettingsTypes< - O extends Record + O extends Record, > = { [K in keyof O]: InferSettingsType } diff --git a/packages/api/src/proxy.ts b/packages/api/src/proxy.ts index 44c7af9354..1727060323 100644 --- a/packages/api/src/proxy.ts +++ b/packages/api/src/proxy.ts @@ -21,7 +21,7 @@ export class ApiProxy = DevtoolsPluginApi hook: any fallbacks: Record - constructor (plugin: PluginDescriptor, hook: any) { + constructor(plugin: PluginDescriptor, hook: any) { this.target = null this.targetQueue = [] this.onQueue = [] @@ -42,23 +42,25 @@ export class ApiProxy = DevtoolsPluginApi const raw = localStorage.getItem(localSettingsSaveId) const data = JSON.parse(raw) Object.assign(currentSettings, data) - } catch (e) { + } + catch (e) { // noop } this.fallbacks = { - getSettings () { + getSettings() { return currentSettings }, - setSettings (value) { + setSettings(value) { try { localStorage.setItem(localSettingsSaveId, JSON.stringify(value)) - } catch (e) { + } + catch (e) { // noop } currentSettings = value }, - now () { + now() { return now() }, } @@ -75,7 +77,8 @@ export class ApiProxy = DevtoolsPluginApi get: (_target, prop: string) => { if (this.target) { return this.target.on[prop] - } else { + } + else { return (...args) => { this.onQueue.push({ method: prop, @@ -90,9 +93,11 @@ export class ApiProxy = DevtoolsPluginApi get: (_target, prop: string) => { if (this.target) { return this.target[prop] - } else if (prop === 'on') { + } + else if (prop === 'on') { return this.proxiedOn - } else if (Object.keys(this.fallbacks).includes(prop)) { + } + else if (Object.keys(this.fallbacks).includes(prop)) { return (...args) => { this.targetQueue.push({ method: prop, @@ -101,9 +106,10 @@ export class ApiProxy = DevtoolsPluginApi }) return this.fallbacks[prop](...args) } - } else { + } + else { return (...args) => { - return new Promise(resolve => { + return new Promise((resolve) => { this.targetQueue.push({ method: prop, args, @@ -116,7 +122,7 @@ export class ApiProxy = DevtoolsPluginApi }) } - async setRealTarget (target: TTarget) { + async setRealTarget(target: TTarget) { this.target = target for (const item of this.onQueue) { diff --git a/packages/api/src/time.ts b/packages/api/src/time.ts index 7080caf59c..87f919c6bb 100644 --- a/packages/api/src/time.ts +++ b/packages/api/src/time.ts @@ -1,22 +1,24 @@ let supported: boolean let perf: Performance -export function isPerformanceSupported () { +export function isPerformanceSupported() { if (supported !== undefined) { return supported } if (typeof window !== 'undefined' && window.performance) { supported = true perf = window.performance - } else if (typeof global !== 'undefined' && (global as any).perf_hooks?.performance) { + } + else if (typeof globalThis !== 'undefined' && (globalThis as any).perf_hooks?.performance) { supported = true - perf = (global as any).perf_hooks.performance - } else { + perf = (globalThis as any).perf_hooks.performance + } + else { supported = false } return supported } -export function now () { +export function now() { return isPerformanceSupported() ? perf.now() : Date.now() } diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 2a5a2092d1..51b2660543 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -1,23 +1,23 @@ { "compilerOptions": { "target": "ES2017", + "lib": ["ESNext", "DOM"], "module": "ESNext", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "removeComments": false, "resolveJsonModule": true, - "skipLibCheck": true, "types": ["node", "webpack-env"], - "sourceMap": false, - "preserveWatchOutput": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "alwaysStrict": true, // Strict "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "lib": ["ESNext", "DOM"] + "removeComments": false, + "sourceMap": false, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true }, "include": ["src/**/*"], "exclude": ["node_modules"] diff --git a/packages/app-backend-api/package.json b/packages/app-backend-api/package.json index 64bb1f599c..a271ec9312 100644 --- a/packages/app-backend-api/package.json +++ b/packages/app-backend-api/package.json @@ -10,12 +10,12 @@ "ts": "tsc -d -outDir lib" }, "dependencies": { - "@vue/devtools-api": "^6.0.0-beta.1", - "@vue-devtools/shared-utils": "^0.0.0" + "@vue-devtools/shared-utils": "^0.0.0", + "@vue/devtools-api": "^6.0.0-beta.1" }, "devDependencies": { - "@types/node": "^13.9.1", + "@types/node": "^20.11.16", "@types/webpack-env": "^1.15.1", - "typescript": "^4.5.2" + "typescript": "^5.3.3" } } diff --git a/packages/app-backend-api/src/api.ts b/packages/app-backend-api/src/api.ts index bc2b86bd27..c08f96cd23 100644 --- a/packages/app-backend-api/src/api.ts +++ b/packages/app-backend-api/src/api.ts @@ -1,33 +1,37 @@ -import { +import type { Bridge, - hasPluginPermission, +} from '@vue-devtools/shared-utils' +import { HookEvents, PluginPermission, + StateEditor, getPluginDefaultSettings, getPluginSettings, + hasPluginPermission, setPluginSettings, - StateEditor, } from '@vue-devtools/shared-utils' -import { - Hooks, - HookPayloads, +import type { App, - DevtoolsPluginApi, + ComponentDevtoolsOptions, ComponentInstance, - TimelineLayerOptions, - TimelineEventOptions, + ComponentTreeNode, CustomInspectorOptions, + DevtoolsPluginApi, EditStatePayload, + HookPayloads, + TimelineEventOptions, + TimelineLayerOptions, WithId, - ComponentTreeNode, - ComponentDevtoolsOptions, +} from '@vue/devtools-api' +import { + Hooks, now, } from '@vue/devtools-api' import { DevtoolsHookable } from './hooks' -import { BackendContext } from './backend-context' -import { Plugin } from './plugin' -import { DevtoolsBackend } from './backend' -import { AppRecord } from './app-record' +import type { BackendContext } from './backend-context' +import type { Plugin } from './plugin' +import type { DevtoolsBackend } from './backend' +import type { AppRecord } from './app-record' const pluginOn: DevtoolsHookable[] = [] @@ -38,14 +42,14 @@ export class DevtoolsApi { on: DevtoolsHookable stateEditor: StateEditor = new StateEditor() - constructor (backend: DevtoolsBackend, ctx: BackendContext) { + constructor(backend: DevtoolsBackend, ctx: BackendContext) { this.backend = backend this.ctx = ctx this.bridge = ctx.bridge this.on = new DevtoolsHookable(ctx) } - async callHook (eventType: T, payload: HookPayloads[T], ctx: BackendContext = this.ctx) { + async callHook(eventType: T, payload: HookPayloads[T], ctx: BackendContext = this.ctx) { payload = await this.on.callHandlers(eventType, payload, ctx) for (const on of pluginOn) { payload = await on.callHandlers(eventType, payload, ctx) @@ -53,7 +57,7 @@ export class DevtoolsApi { return payload } - async transformCall (callName: string, ...args) { + async transformCall(callName: string, ...args) { const payload = await this.callHook(Hooks.TRANSFORM_CALL, { callName, inArgs: args, @@ -62,19 +66,20 @@ export class DevtoolsApi { return payload.outArgs } - async getAppRecordName (app: App, defaultName: string): Promise { + async getAppRecordName(app: App, defaultName: string): Promise { const payload = await this.callHook(Hooks.GET_APP_RECORD_NAME, { app, name: null, }) if (payload.name) { return payload.name - } else { + } + else { return `App ${defaultName}` } } - async getAppRootInstance (app: App) { + async getAppRootInstance(app: App) { const payload = await this.callHook(Hooks.GET_APP_ROOT_INSTANCE, { app, root: null, @@ -82,23 +87,24 @@ export class DevtoolsApi { return payload.root } - async registerApplication (app: App) { + async registerApplication(app: App) { await this.callHook(Hooks.REGISTER_APPLICATION, { app, }) } - async walkComponentTree (instance: ComponentInstance, maxDepth = -1, filter: string = null) { + async walkComponentTree(instance: ComponentInstance, maxDepth = -1, filter: string = null, recursively = false) { const payload = await this.callHook(Hooks.WALK_COMPONENT_TREE, { componentInstance: instance, componentTreeData: null, maxDepth, filter, + recursively, }) return payload.componentTreeData } - async visitComponentTree (instance: ComponentInstance, treeNode: ComponentTreeNode, filter: string = null, app: App) { + async visitComponentTree(instance: ComponentInstance, treeNode: ComponentTreeNode, filter: string = null, app: App) { const payload = await this.callHook(Hooks.VISIT_COMPONENT_TREE, { app, componentInstance: instance, @@ -108,7 +114,7 @@ export class DevtoolsApi { return payload.treeNode } - async walkComponentParents (instance: ComponentInstance) { + async walkComponentParents(instance: ComponentInstance) { const payload = await this.callHook(Hooks.WALK_COMPONENT_PARENTS, { componentInstance: instance, parentInstances: [], @@ -116,7 +122,7 @@ export class DevtoolsApi { return payload.parentInstances } - async inspectComponent (instance: ComponentInstance, app: App) { + async inspectComponent(instance: ComponentInstance, app: App) { const payload = await this.callHook(Hooks.INSPECT_COMPONENT, { app, componentInstance: instance, @@ -125,7 +131,7 @@ export class DevtoolsApi { return payload.instanceData } - async getComponentBounds (instance: ComponentInstance) { + async getComponentBounds(instance: ComponentInstance) { const payload = await this.callHook(Hooks.GET_COMPONENT_BOUNDS, { componentInstance: instance, bounds: null, @@ -133,7 +139,7 @@ export class DevtoolsApi { return payload.bounds } - async getComponentName (instance: ComponentInstance) { + async getComponentName(instance: ComponentInstance) { const payload = await this.callHook(Hooks.GET_COMPONENT_NAME, { componentInstance: instance, name: null, @@ -141,7 +147,7 @@ export class DevtoolsApi { return payload.name } - async getComponentInstances (app: App) { + async getComponentInstances(app: App) { const payload = await this.callHook(Hooks.GET_COMPONENT_INSTANCES, { app, componentInstances: [], @@ -149,7 +155,7 @@ export class DevtoolsApi { return payload.componentInstances } - async getElementComponent (element: HTMLElement | any) { + async getElementComponent(element: HTMLElement | any) { const payload = await this.callHook(Hooks.GET_ELEMENT_COMPONENT, { element, componentInstance: null, @@ -157,7 +163,7 @@ export class DevtoolsApi { return payload.componentInstance } - async getComponentRootElements (instance: ComponentInstance) { + async getComponentRootElements(instance: ComponentInstance) { const payload = await this.callHook(Hooks.GET_COMPONENT_ROOT_ELEMENTS, { componentInstance: instance, rootElements: [], @@ -165,7 +171,7 @@ export class DevtoolsApi { return payload.rootElements } - async editComponentState (instance: ComponentInstance, dotPath: string, type: string, state: EditStatePayload, app: App) { + async editComponentState(instance: ComponentInstance, dotPath: string, type: string, state: EditStatePayload, app: App) { const arrayPath = dotPath.split('.') const payload = await this.callHook(Hooks.EDIT_COMPONENT_STATE, { app, @@ -178,7 +184,7 @@ export class DevtoolsApi { return payload.componentInstance } - async getComponentDevtoolsOptions (instance: ComponentInstance): Promise { + async getComponentDevtoolsOptions(instance: ComponentInstance): Promise { const payload = await this.callHook(Hooks.GET_COMPONENT_DEVTOOLS_OPTIONS, { componentInstance: instance, options: null, @@ -186,7 +192,7 @@ export class DevtoolsApi { return payload.options || {} } - async getComponentRenderCode (instance: ComponentInstance): Promise<{ + async getComponentRenderCode(instance: ComponentInstance): Promise<{ code: string }> { const payload = await this.callHook(Hooks.GET_COMPONENT_RENDER_CODE, { @@ -198,7 +204,7 @@ export class DevtoolsApi { } } - async inspectTimelineEvent (eventData: TimelineEventOptions & WithId, app: App) { + async inspectTimelineEvent(eventData: TimelineEventOptions & WithId, app: App) { const payload = await this.callHook(Hooks.INSPECT_TIMELINE_EVENT, { event: eventData.event, layerId: eventData.layerId, @@ -209,11 +215,11 @@ export class DevtoolsApi { return payload.data } - async clearTimeline () { + async clearTimeline() { await this.callHook(Hooks.TIMELINE_CLEARED, {}) } - async getInspectorTree (inspectorId: string, app: App, filter: string) { + async getInspectorTree(inspectorId: string, app: App, filter: string) { const payload = await this.callHook(Hooks.GET_INSPECTOR_TREE, { inspectorId, app, @@ -223,7 +229,7 @@ export class DevtoolsApi { return payload.rootNodes } - async getInspectorState (inspectorId: string, app: App, nodeId: string) { + async getInspectorState(inspectorId: string, app: App, nodeId: string) { const payload = await this.callHook(Hooks.GET_INSPECTOR_STATE, { inspectorId, app, @@ -233,7 +239,7 @@ export class DevtoolsApi { return payload.state } - async editInspectorState (inspectorId: string, app: App, nodeId: string, dotPath: string, type: string, state: EditStatePayload) { + async editInspectorState(inspectorId: string, app: App, nodeId: string, dotPath: string, type: string, state: EditStatePayload) { const arrayPath = dotPath.split('.') await this.callHook(Hooks.EDIT_INSPECTOR_STATE, { inspectorId, @@ -246,7 +252,7 @@ export class DevtoolsApi { }) } - now () { + now() { return now() } } @@ -260,7 +266,7 @@ export class DevtoolsPluginApiInstance implements DevtoolsPlugi on: DevtoolsHookable private defaultSettings: TSettings - constructor (plugin: Plugin, appRecord: AppRecord, ctx: BackendContext) { + constructor(plugin: Plugin, appRecord: AppRecord, ctx: BackendContext) { this.bridge = ctx.bridge this.ctx = ctx this.plugin = plugin @@ -273,101 +279,120 @@ export class DevtoolsPluginApiInstance implements DevtoolsPlugi // Plugin API - async notifyComponentUpdate (instance: ComponentInstance = null) { - if (!this.enabled || !this.hasPermission(PluginPermission.COMPONENTS)) return + async notifyComponentUpdate(instance: ComponentInstance = null) { + if (!this.enabled || !this.hasPermission(PluginPermission.COMPONENTS)) { + return + } if (instance) { this.ctx.hook.emit(HookEvents.COMPONENT_UPDATED, ...await this.backendApi.transformCall(HookEvents.COMPONENT_UPDATED, instance)) - } else { + } + else { this.ctx.hook.emit(HookEvents.COMPONENT_UPDATED) } } - addTimelineLayer (options: TimelineLayerOptions) { - if (!this.enabled || !this.hasPermission(PluginPermission.TIMELINE)) return false + addTimelineLayer(options: TimelineLayerOptions) { + if (!this.enabled || !this.hasPermission(PluginPermission.TIMELINE)) { + return false + } this.ctx.hook.emit(HookEvents.TIMELINE_LAYER_ADDED, options, this.plugin) return true } - addTimelineEvent (options: TimelineEventOptions) { - if (!this.enabled || !this.hasPermission(PluginPermission.TIMELINE)) return false + addTimelineEvent(options: TimelineEventOptions) { + if (!this.enabled || !this.hasPermission(PluginPermission.TIMELINE)) { + return false + } this.ctx.hook.emit(HookEvents.TIMELINE_EVENT_ADDED, options, this.plugin) return true } - addInspector (options: CustomInspectorOptions) { - if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) return false + addInspector(options: CustomInspectorOptions) { + if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) { + return false + } this.ctx.hook.emit(HookEvents.CUSTOM_INSPECTOR_ADD, options, this.plugin) return true } - sendInspectorTree (inspectorId: string) { - if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) return false + sendInspectorTree(inspectorId: string) { + if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) { + return false + } this.ctx.hook.emit(HookEvents.CUSTOM_INSPECTOR_SEND_TREE, inspectorId, this.plugin) return true } - sendInspectorState (inspectorId: string) { - if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) return false + sendInspectorState(inspectorId: string) { + if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) { + return false + } this.ctx.hook.emit(HookEvents.CUSTOM_INSPECTOR_SEND_STATE, inspectorId, this.plugin) return true } - selectInspectorNode (inspectorId: string, nodeId: string) { - if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) return false + selectInspectorNode(inspectorId: string, nodeId: string) { + if (!this.enabled || !this.hasPermission(PluginPermission.CUSTOM_INSPECTOR)) { + return false + } this.ctx.hook.emit(HookEvents.CUSTOM_INSPECTOR_SELECT_NODE, inspectorId, nodeId, this.plugin) return true } - getComponentBounds (instance: ComponentInstance) { + getComponentBounds(instance: ComponentInstance) { return this.backendApi.getComponentBounds(instance) } - getComponentName (instance: ComponentInstance) { + getComponentName(instance: ComponentInstance) { return this.backendApi.getComponentName(instance) } - getComponentInstances (app: App) { + getComponentInstances(app: App) { return this.backendApi.getComponentInstances(app) } - highlightElement (instance: ComponentInstance) { - if (!this.enabled || !this.hasPermission(PluginPermission.COMPONENTS)) return false + highlightElement(instance: ComponentInstance) { + if (!this.enabled || !this.hasPermission(PluginPermission.COMPONENTS)) { + return false + } this.ctx.hook.emit(HookEvents.COMPONENT_HIGHLIGHT, instance.__VUE_DEVTOOLS_UID__, this.plugin) return true } - unhighlightElement () { - if (!this.enabled || !this.hasPermission(PluginPermission.COMPONENTS)) return false + unhighlightElement() { + if (!this.enabled || !this.hasPermission(PluginPermission.COMPONENTS)) { + return false + } this.ctx.hook.emit(HookEvents.COMPONENT_UNHIGHLIGHT, this.plugin) return true } - getSettings (pluginId?: string) { + getSettings(pluginId?: string) { return getPluginSettings(pluginId ?? this.plugin.descriptor.id, this.defaultSettings) } - setSettings (value: TSettings, pluginId?: string) { + setSettings(value: TSettings, pluginId?: string) { setPluginSettings(pluginId ?? this.plugin.descriptor.id, value) } - now () { + now() { return now() } - private get enabled () { + private get enabled() { return hasPluginPermission(this.plugin.descriptor.id, PluginPermission.ENABLED) } - private hasPermission (permission: PluginPermission) { + private hasPermission(permission: PluginPermission) { return hasPluginPermission(this.plugin.descriptor.id, permission) } } diff --git a/packages/app-backend-api/src/app-record.ts b/packages/app-backend-api/src/app-record.ts index 9d7e9f9753..8aad57d040 100644 --- a/packages/app-backend-api/src/app-record.ts +++ b/packages/app-backend-api/src/app-record.ts @@ -1,5 +1,5 @@ -import { DevtoolsBackend } from './backend' -import { App, ComponentInstance, TimelineEventOptions, ID, WithId } from '@vue/devtools-api' +import type { App, ComponentInstance } from '@vue/devtools-api' +import type { DevtoolsBackend } from './backend' export interface AppRecordOptions { app: App @@ -20,6 +20,7 @@ export interface AppRecord { perfGroupIds: Map iframe: string meta: any + missingInstanceQueue: Set } /** diff --git a/packages/app-backend-api/src/backend-context.ts b/packages/app-backend-api/src/backend-context.ts index db9753fd46..d878ec0631 100644 --- a/packages/app-backend-api/src/backend-context.ts +++ b/packages/app-backend-api/src/backend-context.ts @@ -1,16 +1,16 @@ -import { Bridge } from '@vue-devtools/shared-utils' -import { - TimelineLayerOptions, +import type { Bridge } from '@vue-devtools/shared-utils' +import type { CustomInspectorOptions, - TimelineEventOptions, - WithId, ID, + TimelineEventOptions, + TimelineLayerOptions, TimelineMarkerOptions, + WithId, } from '@vue/devtools-api' -import { AppRecord } from './app-record' -import { Plugin } from './plugin' -import { DevtoolsHook } from './global-hook' -import { DevtoolsBackend } from './backend' +import type { AppRecord } from './app-record' +import type { Plugin } from './plugin' +import type { DevtoolsHook } from './global-hook' +import type { DevtoolsBackend } from './backend' export interface BackendContext { bridge: Bridge @@ -52,7 +52,7 @@ export interface CreateBackendContextOptions { hook: DevtoolsHook } -export function createBackendContext (options: CreateBackendContextOptions): BackendContext { +export function createBackendContext(options: CreateBackendContextOptions): BackendContext { return { bridge: options.bridge, hook: options.hook, diff --git a/packages/app-backend-api/src/backend.ts b/packages/app-backend-api/src/backend.ts index 97210a7896..67e4975afe 100644 --- a/packages/app-backend-api/src/backend.ts +++ b/packages/app-backend-api/src/backend.ts @@ -1,12 +1,12 @@ -import { AppRecord } from './app-record' +import type { AppRecord } from './app-record' import { DevtoolsApi } from './api' -import { BackendContext } from './backend-context' +import type { BackendContext } from './backend-context' export enum BuiltinBackendFeature { /** * @deprecated */ - FLUSH = 'flush' + FLUSH = 'flush', } export interface DevtoolsBackendOptions { @@ -16,7 +16,7 @@ export interface DevtoolsBackendOptions { setupApp?: (api: DevtoolsApi, app: AppRecord) => void } -export function defineBackend (options: DevtoolsBackendOptions) { +export function defineBackend(options: DevtoolsBackendOptions) { return options } @@ -25,7 +25,7 @@ export interface DevtoolsBackend { api: DevtoolsApi } -export function createBackend (options: DevtoolsBackendOptions, ctx: BackendContext): DevtoolsBackend { +export function createBackend(options: DevtoolsBackendOptions, ctx: BackendContext): DevtoolsBackend { const backend: DevtoolsBackend = { options, api: null, diff --git a/packages/app-backend-api/src/global-hook.ts b/packages/app-backend-api/src/global-hook.ts index 3596d17df2..5c07e85c55 100644 --- a/packages/app-backend-api/src/global-hook.ts +++ b/packages/app-backend-api/src/global-hook.ts @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/ban-types */ - -import { AppRecordOptions } from './app-record' +import type { AppRecordOptions } from './app-record' export interface DevtoolsHook { emit: (event: string, ...payload: any[]) => void diff --git a/packages/app-backend-api/src/hooks.ts b/packages/app-backend-api/src/hooks.ts index 41b83afc6b..febecc222a 100644 --- a/packages/app-backend-api/src/hooks.ts +++ b/packages/app-backend-api/src/hooks.ts @@ -1,7 +1,8 @@ -import { hasPluginPermission, PluginPermission } from '@vue-devtools/shared-utils' -import { Hooks, HookPayloads, Hookable, HookHandler } from '@vue/devtools-api' -import { BackendContext } from './backend-context' -import { Plugin } from './plugin' +import { PluginPermission, SharedData, hasPluginPermission } from '@vue-devtools/shared-utils' +import type { HookHandler, HookPayloads, Hookable } from '@vue/devtools-api' +import { Hooks } from '@vue/devtools-api' +import type { BackendContext } from './backend-context' +import type { Plugin } from './plugin' type Handler = HookHandler @@ -15,29 +16,29 @@ export class DevtoolsHookable implements Hookable { private ctx: BackendContext private plugin: Plugin - constructor (ctx: BackendContext, plugin: Plugin = null) { + constructor(ctx: BackendContext, plugin: Plugin = null) { this.ctx = ctx this.plugin = plugin } - private hook (eventType: T, handler: Handler, pluginPermision: PluginPermission = null) { + private hook(eventType: T, handler: Handler, pluginPermision: PluginPermission = null) { const handlers = (this.handlers[eventType] = this.handlers[eventType] || []) as HookHandlerData[] if (this.plugin) { const originalHandler = handler handler = (...args) => { // Plugin permission - if (!hasPluginPermission(this.plugin.descriptor.id, PluginPermission.ENABLED) || - (pluginPermision && !hasPluginPermission(this.plugin.descriptor.id, pluginPermision)) - ) return + if (!hasPluginPermission(this.plugin.descriptor.id, PluginPermission.ENABLED) + || (pluginPermision && !hasPluginPermission(this.plugin.descriptor.id, pluginPermision)) + ) { return } // App scope - if (!this.plugin.descriptor.disableAppScope && - this.ctx.currentAppRecord?.options.app !== this.plugin.descriptor.app) return + if (!this.plugin.descriptor.disableAppScope + && this.ctx.currentAppRecord?.options.app !== this.plugin.descriptor.app) { return } // Plugin scope - if (!this.plugin.descriptor.disablePluginScope && - (args[0] as any).pluginId != null && (args[0] as any).pluginId !== this.plugin.descriptor.id) return + if (!this.plugin.descriptor.disablePluginScope + && (args[0] as any).pluginId != null && (args[0] as any).pluginId !== this.plugin.descriptor.id) { return } return originalHandler(...args) } @@ -49,107 +50,110 @@ export class DevtoolsHookable implements Hookable { }) } - async callHandlers (eventType: T, payload: HookPayloads[T], ctx: BackendContext) { + async callHandlers(eventType: T, payload: HookPayloads[T], ctx: BackendContext) { if (this.handlers[eventType]) { const handlers = this.handlers[eventType] as HookHandlerData[] for (let i = 0; i < handlers.length; i++) { const { handler, plugin } = handlers[i] try { await handler(payload, ctx) - } catch (e) { - console.error(`An error occurred in hook '${eventType}'${plugin ? ` registered by plugin '${plugin.descriptor.id}'` : ''} with payload:`, payload) - console.error(e) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(`An error occurred in hook '${eventType}'${plugin ? ` registered by plugin '${plugin.descriptor.id}'` : ''} with payload:`, payload) + console.error(e) + } } } } return payload } - transformCall (handler: Handler) { + transformCall(handler: Handler) { this.hook(Hooks.TRANSFORM_CALL, handler) } - getAppRecordName (handler: Handler) { + getAppRecordName(handler: Handler) { this.hook(Hooks.GET_APP_RECORD_NAME, handler) } - getAppRootInstance (handler: Handler) { + getAppRootInstance(handler: Handler) { this.hook(Hooks.GET_APP_ROOT_INSTANCE, handler) } - registerApplication (handler: Handler) { + registerApplication(handler: Handler) { this.hook(Hooks.REGISTER_APPLICATION, handler) } - walkComponentTree (handler: Handler) { + walkComponentTree(handler: Handler) { this.hook(Hooks.WALK_COMPONENT_TREE, handler, PluginPermission.COMPONENTS) } - visitComponentTree (handler: Handler) { + visitComponentTree(handler: Handler) { this.hook(Hooks.VISIT_COMPONENT_TREE, handler, PluginPermission.COMPONENTS) } - walkComponentParents (handler: Handler) { + walkComponentParents(handler: Handler) { this.hook(Hooks.WALK_COMPONENT_PARENTS, handler, PluginPermission.COMPONENTS) } - inspectComponent (handler: Handler) { + inspectComponent(handler: Handler) { this.hook(Hooks.INSPECT_COMPONENT, handler, PluginPermission.COMPONENTS) } - getComponentBounds (handler: Handler) { + getComponentBounds(handler: Handler) { this.hook(Hooks.GET_COMPONENT_BOUNDS, handler, PluginPermission.COMPONENTS) } - getComponentName (handler: Handler) { + getComponentName(handler: Handler) { this.hook(Hooks.GET_COMPONENT_NAME, handler, PluginPermission.COMPONENTS) } - getComponentInstances (handler: Handler) { + getComponentInstances(handler: Handler) { this.hook(Hooks.GET_COMPONENT_INSTANCES, handler, PluginPermission.COMPONENTS) } - getElementComponent (handler: Handler) { + getElementComponent(handler: Handler) { this.hook(Hooks.GET_ELEMENT_COMPONENT, handler, PluginPermission.COMPONENTS) } - getComponentRootElements (handler: Handler) { + getComponentRootElements(handler: Handler) { this.hook(Hooks.GET_COMPONENT_ROOT_ELEMENTS, handler, PluginPermission.COMPONENTS) } - editComponentState (handler: Handler) { + editComponentState(handler: Handler) { this.hook(Hooks.EDIT_COMPONENT_STATE, handler, PluginPermission.COMPONENTS) } - getComponentDevtoolsOptions (handler: Handler) { + getComponentDevtoolsOptions(handler: Handler) { this.hook(Hooks.GET_COMPONENT_DEVTOOLS_OPTIONS, handler, PluginPermission.COMPONENTS) } - getComponentRenderCode (handler: Handler) { + getComponentRenderCode(handler: Handler) { this.hook(Hooks.GET_COMPONENT_RENDER_CODE, handler, PluginPermission.COMPONENTS) } - inspectTimelineEvent (handler: Handler) { + inspectTimelineEvent(handler: Handler) { this.hook(Hooks.INSPECT_TIMELINE_EVENT, handler, PluginPermission.TIMELINE) } - timelineCleared (handler: Handler) { + timelineCleared(handler: Handler) { this.hook(Hooks.TIMELINE_CLEARED, handler, PluginPermission.TIMELINE) } - getInspectorTree (handler: Handler) { + getInspectorTree(handler: Handler) { this.hook(Hooks.GET_INSPECTOR_TREE, handler, PluginPermission.CUSTOM_INSPECTOR) } - getInspectorState (handler: Handler) { + getInspectorState(handler: Handler) { this.hook(Hooks.GET_INSPECTOR_STATE, handler, PluginPermission.CUSTOM_INSPECTOR) } - editInspectorState (handler: Handler) { + editInspectorState(handler: Handler) { this.hook(Hooks.EDIT_INSPECTOR_STATE, handler, PluginPermission.CUSTOM_INSPECTOR) } - setPluginSettings (handler: Handler) { + setPluginSettings(handler: Handler) { this.hook(Hooks.SET_PLUGIN_SETTINGS, handler) } } diff --git a/packages/app-backend-api/src/plugin.ts b/packages/app-backend-api/src/plugin.ts index 4fde11981d..f9df976c13 100644 --- a/packages/app-backend-api/src/plugin.ts +++ b/packages/app-backend-api/src/plugin.ts @@ -1,4 +1,4 @@ -import { PluginDescriptor, SetupFunction } from '@vue/devtools-api' +import type { PluginDescriptor, SetupFunction } from '@vue/devtools-api' export interface Plugin { descriptor: PluginDescriptor diff --git a/packages/app-backend-api/tsconfig.json b/packages/app-backend-api/tsconfig.json index f565bc6603..becb763b20 100644 --- a/packages/app-backend-api/tsconfig.json +++ b/packages/app-backend-api/tsconfig.json @@ -3,25 +3,25 @@ "target": "ES2019", "module": "commonjs", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true, "types": [ "node", "webpack-env" ], - "sourceMap": true, - "preserveWatchOutput": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "alwaysStrict": true, // Strict "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true }, "include": [ - "src/**/*", + "src/**/*" ], "exclude": [ "node_modules" diff --git a/packages/app-backend-core/package.json b/packages/app-backend-core/package.json index ce1e5093af..4249524c3b 100644 --- a/packages/app-backend-core/package.json +++ b/packages/app-backend-core/package.json @@ -10,18 +10,18 @@ "ts": "tsc -d -outDir lib" }, "dependencies": { - "@vue/devtools-api": "^6.0.0-beta.1", "@vue-devtools/app-backend-api": "^0.0.0", "@vue-devtools/app-backend-vue1": "^0.0.0", "@vue-devtools/app-backend-vue2": "^0.0.0", "@vue-devtools/app-backend-vue3": "^0.0.0", "@vue-devtools/shared-utils": "^0.0.0", + "@vue/devtools-api": "^6.0.0-beta.1", "lodash": "^4.17.21", "speakingurl": "^14.0.1" }, "devDependencies": { - "@types/node": "^13.9.1", + "@types/node": "^20.11.16", "@types/webpack-env": "^1.15.1", - "typescript": "^4.5.2" + "typescript": "^5.3.3" } } diff --git a/packages/app-backend-core/src/app.ts b/packages/app-backend-core/src/app.ts index efb40a0e6a..9a43c35560 100644 --- a/packages/app-backend-core/src/app.ts +++ b/packages/app-backend-core/src/app.ts @@ -1,18 +1,19 @@ -import { +import type { AppRecord, - SimpleAppRecord, AppRecordOptions, BackendContext, DevtoolsBackend, + SimpleAppRecord, } from '@vue-devtools/app-backend-api' -import { BridgeEvents, isBrowser, SharedData } from '@vue-devtools/shared-utils' -import { App } from '@vue/devtools-api' +import { BridgeEvents, SharedData, isBrowser } from '@vue-devtools/shared-utils' +import type { App } from '@vue/devtools-api' import slug from 'speakingurl' import { JobQueue } from './util/queue' import { scan } from './legacy/scan' import { addBuiltinLayers, removeLayersForApp } from './timeline' -import { getBackend, availableBackends } from './backend' +import { availableBackends, getBackend } from './backend' import { hook } from './global-hook.js' +import { sendComponentTreeData, sendSelectedComponentData } from './component.js' const jobs = new JobQueue() @@ -21,11 +22,11 @@ let recordId = 0 type AppRecordResolver = (record: AppRecord) => void | Promise const appRecordPromises = new Map() -export async function registerApp (options: AppRecordOptions, ctx: BackendContext) { +export async function registerApp(options: AppRecordOptions, ctx: BackendContext) { return jobs.queue('regiserApp', () => registerAppJob(options, ctx)) } -async function registerAppJob (options: AppRecordOptions, ctx: BackendContext) { +async function registerAppJob(options: AppRecordOptions, ctx: BackendContext) { // Dedupe if (ctx.appRecords.find(a => a.options.app === options.app)) { return @@ -36,7 +37,7 @@ async function registerAppJob (options: AppRecordOptions, ctx: BackendContext) { } // Find correct backend - const baseFrameworkVersion = parseInt(options.version.substring(0, options.version.indexOf('.'))) + const baseFrameworkVersion = Number.parseInt(options.version.substring(0, options.version.indexOf('.'))) for (let i = 0; i < availableBackends.length; i++) { const backendOptions = availableBackends[i] if (backendOptions.frameworkVersion === baseFrameworkVersion) { @@ -50,10 +51,11 @@ async function registerAppJob (options: AppRecordOptions, ctx: BackendContext) { } } -async function createAppRecord (options: AppRecordOptions, backend: DevtoolsBackend, ctx: BackendContext) { +async function createAppRecord(options: AppRecordOptions, backend: DevtoolsBackend, ctx: BackendContext) { const rootInstance = await backend.api.getAppRootInstance(options.app) if (rootInstance) { if ((await backend.api.getComponentDevtoolsOptions(rootInstance)).hide) { + options.app._vueDevtools_hidden_ = true return } @@ -63,17 +65,39 @@ async function createAppRecord (options: AppRecordOptions, backend: DevtoolsBack const [el]: HTMLElement[] = await backend.api.getComponentRootElements(rootInstance) + const instanceMapRaw = new Map() + const record: AppRecord = { id, name, options, backend, lastInspectedComponentId: null, - instanceMap: new Map(), + instanceMap: new Proxy(instanceMapRaw, { + get(target, key: string) { + if (key === 'set') { + return (instanceId: string, instance: any) => { + target.set(instanceId, instance) + // The component was requested by the frontend before it was registered + if (record.missingInstanceQueue.has(instanceId)) { + record.missingInstanceQueue.delete(instanceId) + if (ctx.currentAppRecord === record) { + sendComponentTreeData(record, instanceId, record.componentFilter, null, false, ctx) + if (record.lastInspectedComponentId === instanceId) { + sendSelectedComponentData(record, instanceId, ctx) + } + } + } + } + } + return target[key].bind(target) + }, + }), rootInstance, perfGroupIds: new Map(), - iframe: isBrowser && document !== el.ownerDocument ? el.ownerDocument?.location?.pathname : null, + iframe: isBrowser && document !== el?.ownerDocument ? el?.ownerDocument?.location?.pathname : null, meta: options.meta ?? {}, + missingInstanceQueue: new Set(), } options.app.__VUE_DEVTOOLS_APP_RECORD__ = record @@ -96,22 +120,23 @@ async function createAppRecord (options: AppRecordOptions, backend: DevtoolsBack appRecord: mapAppRecord(record), }) + // Auto select first app + if (ctx.currentAppRecord == null) { + await selectApp(record, ctx) + } + if (appRecordPromises.has(options.app)) { for (const r of appRecordPromises.get(options.app)) { await r(record) } } - - // Auto select first app - if (ctx.currentAppRecord == null) { - await selectApp(record, ctx) - } - } else { + } + else if (SharedData.debugInfo) { console.warn('[Vue devtools] No root instance found for app, it might have been unmounted', options.app) } } -export async function selectApp (record: AppRecord, ctx: BackendContext) { +export async function selectApp(record: AppRecord, ctx: BackendContext) { ctx.currentAppRecord = record ctx.currentInspectedComponentId = record.lastInspectedComponentId ctx.bridge.send(BridgeEvents.TO_FRONT_APP_SELECTED, { @@ -120,7 +145,7 @@ export async function selectApp (record: AppRecord, ctx: BackendContext) { }) } -export function mapAppRecord (record: AppRecord): SimpleAppRecord { +export function mapAppRecord(record: AppRecord): SimpleAppRecord { return { id: record.id, name: record.name, @@ -131,7 +156,7 @@ export function mapAppRecord (record: AppRecord): SimpleAppRecord { const appIds = new Set() -export function getAppRecordId (app, defaultId?: string): string { +export function getAppRecordId(app, defaultId?: string): string { if (app.__VUE_DEVTOOLS_APP_RECORD_ID__ != null) { return app.__VUE_DEVTOOLS_APP_RECORD_ID__ } @@ -151,11 +176,14 @@ export function getAppRecordId (app, defaultId?: string): string { return id } -export async function getAppRecord (app: any, ctx: BackendContext): Promise { - const record = ctx.appRecords.find(ar => ar.options.app === app) +export async function getAppRecord(app: any, ctx: BackendContext): Promise { + const record = app.__VUE_DEVTOOLS_APP_RECORD__ ?? ctx.appRecords.find(ar => ar.options.app === app) if (record) { return record } + if (app._vueDevtools_hidden_) { + return null + } return new Promise((resolve, reject) => { let resolvers = appRecordPromises.get(app) let timedOut = false @@ -163,6 +191,7 @@ export async function getAppRecord (app: any, ctx: BackendContext): Promise { if (!timedOut) { clearTimeout(timer) @@ -170,10 +199,12 @@ export async function getAppRecord (app: any, ctx: BackendContext): Promise { + timer = setTimeout(() => { timedOut = true const index = resolvers.indexOf(fn) - if (index !== -1) resolvers.splice(index, 1) + if (index !== -1) { + resolvers.splice(index, 1) + } if (SharedData.debugInfo) { // eslint-disable-next-line no-console console.log('Timed out waiting for app record', app) @@ -183,11 +214,11 @@ export async function getAppRecord (app: any, ctx: BackendContext): Promise { /* NOOP */ }) } -export async function sendApps (ctx: BackendContext) { +export async function sendApps(ctx: BackendContext) { const appRecords = [] for (const appRecord of ctx.appRecords) { @@ -199,27 +230,31 @@ export async function sendApps (ctx: BackendContext) { }) } -function removeAppRecord (appRecord: AppRecord, ctx: BackendContext) { +function removeAppRecord(appRecord: AppRecord, ctx: BackendContext) { try { appIds.delete(appRecord.id) const index = ctx.appRecords.indexOf(appRecord) - if (index !== -1) ctx.appRecords.splice(index, 1) + if (index !== -1) { + ctx.appRecords.splice(index, 1) + } removeLayersForApp(appRecord.options.app, ctx) ctx.bridge.send(BridgeEvents.TO_FRONT_APP_REMOVE, { id: appRecord.id }) - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } } } -export async function removeApp (app: App, ctx: BackendContext) { +export async function removeApp(app: App, ctx: BackendContext) { try { const appRecord = await getAppRecord(app, ctx) if (appRecord) { removeAppRecord(appRecord, ctx) } - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -228,13 +263,12 @@ export async function removeApp (app: App, ctx: BackendContext) { let scanTimeout: any -// eslint-disable-next-line camelcase -export function _legacy_getAndRegisterApps (ctx: BackendContext, clear = false) { +export function _legacy_getAndRegisterApps(ctx: BackendContext, clear = false) { setTimeout(() => { try { if (clear) { // Remove apps that are legacy - ctx.appRecords.forEach(appRecord => { + ctx.appRecords.forEach((appRecord) => { if (appRecord.meta.Vue) { removeAppRecord(appRecord, ctx) } @@ -248,7 +282,7 @@ export function _legacy_getAndRegisterApps (ctx: BackendContext, clear = false) scanTimeout = setTimeout(() => _legacy_getAndRegisterApps(ctx), 1000) } - apps.forEach(app => { + apps.forEach((app) => { const Vue = hook.Vue registerApp({ app, @@ -259,9 +293,12 @@ export function _legacy_getAndRegisterApps (ctx: BackendContext, clear = false) }, }, ctx) }) - } catch (e) { - console.error(`Error scanning for legacy apps:`) - console.error(e) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(`Error scanning for legacy apps:`) + console.error(e) + } } }, 0) } diff --git a/packages/app-backend-core/src/backend.ts b/packages/app-backend-core/src/backend.ts index 3fd43362f5..af07e8244d 100644 --- a/packages/app-backend-core/src/backend.ts +++ b/packages/app-backend-core/src/backend.ts @@ -1,5 +1,5 @@ - -import { DevtoolsBackendOptions, DevtoolsBackend, createBackend, BackendContext } from '@vue-devtools/app-backend-api' +import type { BackendContext, DevtoolsBackend, DevtoolsBackendOptions } from '@vue-devtools/app-backend-api' +import { createBackend } from '@vue-devtools/app-backend-api' import { backend as backendVue1 } from '@vue-devtools/app-backend-vue1' import { backend as backendVue2 } from '@vue-devtools/app-backend-vue2' @@ -15,7 +15,7 @@ export const availableBackends = [ const enabledBackends: Map = new Map() -export function getBackend (backendOptions: DevtoolsBackendOptions, ctx: BackendContext) { +export function getBackend(backendOptions: DevtoolsBackendOptions, ctx: BackendContext) { let backend: DevtoolsBackend if (!enabledBackends.has(backendOptions)) { // Create backend @@ -23,7 +23,8 @@ export function getBackend (backendOptions: DevtoolsBackendOptions, ctx: Backend handleAddPerformanceTag(backend, ctx) enabledBackends.set(backendOptions, backend) ctx.backends.push(backend) - } else { + } + else { backend = enabledBackends.get(backendOptions) } return backend diff --git a/packages/app-backend-core/src/component-pick.ts b/packages/app-backend-core/src/component-pick.ts index 24233d79ca..ca23a94fac 100644 --- a/packages/app-backend-core/src/component-pick.ts +++ b/packages/app-backend-core/src/component-pick.ts @@ -1,14 +1,14 @@ -import { isBrowser, BridgeEvents } from '@vue-devtools/shared-utils' -import { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' +import { BridgeEvents, isBrowser } from '@vue-devtools/shared-utils' +import type { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' +import type { ComponentInstance } from '@vue/devtools-api' import { highlight, unHighlight } from './highlighter' -import { ComponentInstance } from '@vue/devtools-api' export default class ComponentPicker { ctx: BackendContext selectedInstance: ComponentInstance selectedBackend: DevtoolsBackend - constructor (ctx: BackendContext) { + constructor(ctx: BackendContext) { this.ctx = ctx this.bindMethods() } @@ -16,8 +16,10 @@ export default class ComponentPicker { /** * Adds event listeners for mouseover and mouseup */ - startSelecting () { - if (!isBrowser) return + startSelecting() { + if (!isBrowser) { + return + } window.addEventListener('mouseover', this.elementMouseOver, true) window.addEventListener('click', this.elementClicked, true) window.addEventListener('mouseout', this.cancelEvent, true) @@ -30,8 +32,10 @@ export default class ComponentPicker { /** * Removes event listeners */ - stopSelecting () { - if (!isBrowser) return + stopSelecting() { + if (!isBrowser) { + return + } window.removeEventListener('mouseover', this.elementMouseOver, true) window.removeEventListener('click', this.elementClicked, true) window.removeEventListener('mouseout', this.cancelEvent, true) @@ -46,7 +50,7 @@ export default class ComponentPicker { /** * Highlights a component on element mouse over */ - async elementMouseOver (e: MouseEvent) { + async elementMouseOver(e: MouseEvent) { this.cancelEvent(e) const el = e.target @@ -60,7 +64,7 @@ export default class ComponentPicker { } } - async selectElementComponent (el) { + async selectElementComponent(el) { for (const backend of this.ctx.backends) { const instance = await backend.api.getElementComponent(el) if (instance) { @@ -76,13 +80,14 @@ export default class ComponentPicker { /** * Selects an instance in the component view */ - async elementClicked (e: MouseEvent) { + async elementClicked(e: MouseEvent) { this.cancelEvent(e) if (this.selectedInstance && this.selectedBackend) { const parentInstances = await this.selectedBackend.api.walkComponentParents(this.selectedInstance) this.ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_PICK, { id: this.selectedInstance.__VUE_DEVTOOLS_UID__, parentIds: parentInstances.map(i => i.__VUE_DEVTOOLS_UID__) }) - } else { + } + else { this.ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_PICK_CANCELED, null) } @@ -92,7 +97,7 @@ export default class ComponentPicker { /** * Cancel a mouse event */ - cancelEvent (e: MouseEvent) { + cancelEvent(e: MouseEvent) { e.stopImmediatePropagation() e.preventDefault() } @@ -100,7 +105,7 @@ export default class ComponentPicker { /** * Bind class methods to the class scope to avoid rebind for event listeners */ - bindMethods () { + bindMethods() { this.startSelecting = this.startSelecting.bind(this) this.stopSelecting = this.stopSelecting.bind(this) this.elementMouseOver = this.elementMouseOver.bind(this) diff --git a/packages/app-backend-core/src/component.ts b/packages/app-backend-core/src/component.ts index 90b0aeba38..4033b792fb 100644 --- a/packages/app-backend-core/src/component.ts +++ b/packages/app-backend-core/src/component.ts @@ -1,19 +1,22 @@ -import { stringify, BridgeEvents, parse, SharedData } from '@vue-devtools/shared-utils' -import { AppRecord, BackendContext, BuiltinBackendFeature } from '@vue-devtools/app-backend-api' +import { BridgeEvents, SharedData, createThrottleQueue, parse, stringify } from '@vue-devtools/shared-utils' +import type { AppRecord, BackendContext } from '@vue-devtools/app-backend-api' +import { BuiltinBackendFeature } from '@vue-devtools/app-backend-api' +import type { App, ComponentInstance, EditStatePayload } from '@vue/devtools-api' import { getAppRecord } from './app' -import { App, ComponentInstance, EditStatePayload, now } from '@vue/devtools-api' const MAX_$VM = 10 const $vmQueue = [] -export async function sendComponentTreeData (appRecord: AppRecord, instanceId: string, filter = '', maxDepth: number = null, ctx: BackendContext) { - if (!instanceId || appRecord !== ctx.currentAppRecord) return +export async function sendComponentTreeData(appRecord: AppRecord, instanceId: string, filter = '', maxDepth: number = null, recursively = false, ctx: BackendContext) { + if (!instanceId || appRecord !== ctx.currentAppRecord) { + return + } // Flush will send all components in the tree // So we skip individiual tree updates if ( - instanceId !== '_root' && - ctx.currentAppRecord.backend.options.features.includes(BuiltinBackendFeature.FLUSH) + instanceId !== '_root' + && ctx.currentAppRecord.backend.options.features.includes(BuiltinBackendFeature.FLUSH) ) { return } @@ -25,12 +28,15 @@ export async function sendComponentTreeData (appRecord: AppRecord, instanceId: s treeData: null, notFound: true, }) - } else { - if (filter) filter = filter.toLowerCase() + } + else { + if (filter) { + filter = filter.toLowerCase() + } if (maxDepth == null) { maxDepth = instance === ctx.currentAppRecord.rootInstance ? 2 : 1 } - const data = await appRecord.backend.api.walkComponentTree(instance, maxDepth, filter) + const data = await appRecord.backend.api.walkComponentTree(instance, maxDepth, filter, recursively) const payload = { instanceId, treeData: stringify(data), @@ -39,15 +45,18 @@ export async function sendComponentTreeData (appRecord: AppRecord, instanceId: s } } -export async function sendSelectedComponentData (appRecord: AppRecord, instanceId: string, ctx: BackendContext) { - if (!instanceId || appRecord !== ctx.currentAppRecord) return +export async function sendSelectedComponentData(appRecord: AppRecord, instanceId: string, ctx: BackendContext) { + if (!instanceId || appRecord !== ctx.currentAppRecord) { + return + } const instance = getComponentInstance(appRecord, instanceId, ctx) if (!instance) { sendEmptyComponentData(instanceId, ctx) - } else { + } + else { // Expose instance on window if (typeof window !== 'undefined') { - const win = (window as any) + const win = window as any win.$vm = instance // $vm0, $vm1, $vm2, ... @@ -76,20 +85,22 @@ export async function sendSelectedComponentData (appRecord: AppRecord, instanceI } } -export function markSelectedInstance (instanceId: string, ctx: BackendContext) { +export function markSelectedInstance(instanceId: string, ctx: BackendContext) { ctx.currentInspectedComponentId = instanceId ctx.currentAppRecord.lastInspectedComponentId = instanceId } -export function sendEmptyComponentData (instanceId: string, ctx: BackendContext) { +export function sendEmptyComponentData(instanceId: string, ctx: BackendContext) { ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_SELECTED_DATA, { instanceId, data: null, }) } -export async function editComponentState (instanceId: string, dotPath: string, type: string, state: EditStatePayload, ctx: BackendContext) { - if (!instanceId) return +export async function editComponentState(instanceId: string, dotPath: string, type: string, state: EditStatePayload, ctx: BackendContext) { + if (!instanceId) { + return + } const instance = getComponentInstance(ctx.currentAppRecord, instanceId, ctx) if (instance) { if ('value' in state && state.value != null) { @@ -100,14 +111,19 @@ export async function editComponentState (instanceId: string, dotPath: string, t } } -export async function getComponentId (app: App, uid: number, instance: ComponentInstance, ctx: BackendContext) { +export async function getComponentId(app: App, uid: number, instance: ComponentInstance, ctx: BackendContext) { try { - if (instance.__VUE_DEVTOOLS_UID__) return instance.__VUE_DEVTOOLS_UID__ + if (instance.__VUE_DEVTOOLS_UID__) { + return instance.__VUE_DEVTOOLS_UID__ + } const appRecord = await getAppRecord(app, ctx) - if (!appRecord) return null + if (!appRecord) { + return null + } const isRoot = appRecord.rootInstance === instance return `${appRecord.id}:${isRoot ? 'root' : uid}` - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -115,27 +131,39 @@ export async function getComponentId (app: App, uid: number, instance: Component } } -export function getComponentInstance (appRecord: AppRecord, instanceId: string, ctx: BackendContext) { +export function getComponentInstance(appRecord: AppRecord, instanceId: string, _ctx: BackendContext) { if (instanceId === '_root') { instanceId = `${appRecord.id}:root` } const instance = appRecord.instanceMap.get(instanceId) - if (!instance && SharedData.debugInfo) { - console.warn(`Instance uid=${instanceId} not found`) + if (!instance) { + appRecord.missingInstanceQueue.add(instanceId) + if (SharedData.debugInfo) { + console.warn(`Instance uid=${instanceId} not found`) + } } return instance } -export async function refreshComponentTreeSearch (ctx: BackendContext) { - if (!ctx.currentAppRecord.componentFilter) return - await sendComponentTreeData(ctx.currentAppRecord, '_root', ctx.currentAppRecord.componentFilter, null, ctx) +export async function refreshComponentTreeSearch(ctx: BackendContext) { + if (!ctx.currentAppRecord.componentFilter) { + return + } + await sendComponentTreeData(ctx.currentAppRecord, '_root', ctx.currentAppRecord.componentFilter, null, false, ctx) } -export async function sendComponentUpdateTracking (instanceId: string, ctx: BackendContext) { - if (!instanceId) return - const payload = { - instanceId, - time: Date.now(), // Use normal date +const updateTrackingQueue = createThrottleQueue(500) + +export function sendComponentUpdateTracking(instanceId: string, time: number, ctx: BackendContext) { + if (!instanceId) { + return } - ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_UPDATED, payload) + + updateTrackingQueue.add(instanceId, () => { + const payload = { + instanceId, + time, + } + ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_UPDATED, payload) + }) } diff --git a/packages/app-backend-core/src/flash.ts b/packages/app-backend-core/src/flash.ts index 77d26bff63..8c9bdf38ab 100644 --- a/packages/app-backend-core/src/flash.ts +++ b/packages/app-backend-core/src/flash.ts @@ -1,7 +1,7 @@ -import { DevtoolsBackend } from '@vue-devtools/app-backend-api' -import { ComponentInstance } from '@vue/devtools-api' +import type { DevtoolsBackend } from '@vue-devtools/app-backend-api' +import type { ComponentInstance } from '@vue/devtools-api' -export async function flashComponent (instance: ComponentInstance, backend: DevtoolsBackend) { +export async function flashComponent(instance: ComponentInstance, backend: DevtoolsBackend) { const bounds = await backend.api.getComponentBounds(instance) if (bounds) { let overlay: HTMLDivElement = instance.__VUE_DEVTOOLS_FLASH @@ -18,10 +18,10 @@ export async function flashComponent (instance: ComponentInstance, backend: Devt } overlay.style.opacity = '1' overlay.style.transition = null - overlay.style.width = Math.round(bounds.width) + 'px' - overlay.style.height = Math.round(bounds.height) + 'px' - overlay.style.left = Math.round(bounds.left) + 'px' - overlay.style.top = Math.round(bounds.top) + 'px' + overlay.style.width = `${Math.round(bounds.width)}px` + overlay.style.height = `${Math.round(bounds.height)}px` + overlay.style.left = `${Math.round(bounds.left)}px` + overlay.style.top = `${Math.round(bounds.top)}px` requestAnimationFrame(() => { overlay.style.transition = 'opacity 1s' overlay.style.opacity = '0' diff --git a/packages/app-backend-core/src/global-hook.ts b/packages/app-backend-core/src/global-hook.ts index be2c638c5f..bbdcd3eaa7 100644 --- a/packages/app-backend-core/src/global-hook.ts +++ b/packages/app-backend-core/src/global-hook.ts @@ -1,4 +1,4 @@ -import { DevtoolsHook } from '@vue-devtools/app-backend-api' +import type { DevtoolsHook } from '@vue-devtools/app-backend-api' import { target } from '@vue-devtools/shared-utils' // hook should have been injected before this executes. diff --git a/packages/app-backend-core/src/highlighter.ts b/packages/app-backend-core/src/highlighter.ts index 76d77aeed1..6a2cb9247a 100644 --- a/packages/app-backend-core/src/highlighter.ts +++ b/packages/app-backend-core/src/highlighter.ts @@ -1,14 +1,16 @@ import { isBrowser } from '@vue-devtools/shared-utils' -import { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' -import { ComponentBounds, ComponentInstance } from '@vue/devtools-api' +import type { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' +import type { ComponentBounds, ComponentInstance } from '@vue/devtools-api' import { JobQueue } from './util/queue' let overlay: HTMLDivElement let overlayContent: HTMLDivElement let currentInstance -function createOverlay () { - if (overlay || !isBrowser) return +function createOverlay() { + if (overlay || !isBrowser) { + return + } overlay = document.createElement('div') overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)' overlay.style.position = 'fixed' @@ -34,9 +36,11 @@ function createOverlay () { // This prevents "sticky" highlights that are not removed because highlight is async const jobQueue = new JobQueue() -export async function highlight (instance: ComponentInstance, backend: DevtoolsBackend, ctx: BackendContext) { +export async function highlight(instance: ComponentInstance, backend: DevtoolsBackend, ctx: BackendContext) { await jobQueue.queue('highlight', async () => { - if (!instance) return + if (!instance) { + return + } const bounds = await backend.api.getComponentBounds(instance) if (bounds) { @@ -46,14 +50,14 @@ export async function highlight (instance: ComponentInstance, backend: DevtoolsB const name = (await backend.api.getComponentName(instance)) || 'Anonymous' const pre = document.createElement('span') pre.style.opacity = '0.6' - pre.innerText = '<' + pre.textContent = '<' const text = document.createElement('span') text.style.fontWeight = 'bold' text.style.color = '#09ab56' - text.innerText = name + text.textContent = name const post = document.createElement('span') post.style.opacity = '0.6' - post.innerText = '>' + post.textContent = '>' // Size const size = document.createElement('span') @@ -62,7 +66,7 @@ export async function highlight (instance: ComponentInstance, backend: DevtoolsB size.appendChild(document.createTextNode((Math.round(bounds.width * 100) / 100).toString())) const multiply = document.createElement('span') multiply.style.marginLeft = multiply.style.marginRight = '2px' - multiply.innerText = '×' + multiply.textContent = '×' size.appendChild(multiply) size.appendChild(document.createTextNode((Math.round(bounds.height * 100) / 100).toString())) @@ -75,7 +79,7 @@ export async function highlight (instance: ComponentInstance, backend: DevtoolsB }) } -export async function unHighlight () { +export async function unHighlight() { await jobQueue.queue('unHighlight', async () => { overlay?.parentNode?.removeChild(overlay) overlayContent?.parentNode?.removeChild(overlayContent) @@ -85,8 +89,10 @@ export async function unHighlight () { }) } -function showOverlay (bounds: ComponentBounds, children: Node[] = null) { - if (!isBrowser || !children.length) return +function showOverlay(bounds: ComponentBounds, children: Node[] = null) { + if (!isBrowser || !children.length) { + return + } positionOverlay(bounds) document.body.appendChild(overlay) @@ -98,21 +104,22 @@ function showOverlay (bounds: ComponentBounds, children: Node[] = null) { positionOverlayContent(bounds) } -function positionOverlay ({ width = 0, height = 0, top = 0, left = 0 }) { - overlay.style.width = Math.round(width) + 'px' - overlay.style.height = Math.round(height) + 'px' - overlay.style.left = Math.round(left) + 'px' - overlay.style.top = Math.round(top) + 'px' +function positionOverlay({ width = 0, height = 0, top = 0, left = 0 }) { + overlay.style.width = `${Math.round(width)}px` + overlay.style.height = `${Math.round(height)}px` + overlay.style.left = `${Math.round(left)}px` + overlay.style.top = `${Math.round(top)}px` } -function positionOverlayContent ({ height = 0, top = 0, left = 0 }) { +function positionOverlayContent({ height = 0, top = 0, left = 0 }) { // Content position (prevents overflow) const contentWidth = overlayContent.offsetWidth const contentHeight = overlayContent.offsetHeight let contentLeft = left if (contentLeft < 0) { contentLeft = 0 - } else if (contentLeft + contentWidth > window.innerWidth) { + } + else if (contentLeft + contentWidth > window.innerWidth) { contentLeft = window.innerWidth - contentWidth } let contentTop = top - contentHeight - 2 @@ -121,14 +128,15 @@ function positionOverlayContent ({ height = 0, top = 0, left = 0 }) { } if (contentTop < 0) { contentTop = 0 - } else if (contentTop + contentHeight > window.innerHeight) { + } + else if (contentTop + contentHeight > window.innerHeight) { contentTop = window.innerHeight - contentHeight } - overlayContent.style.left = ~~contentLeft + 'px' - overlayContent.style.top = ~~contentTop + 'px' + overlayContent.style.left = `${~~contentLeft}px` + overlayContent.style.top = `${~~contentTop}px` } -async function updateOverlay (backend: DevtoolsBackend, ctx: BackendContext) { +async function updateOverlay(backend: DevtoolsBackend, _ctx: BackendContext) { if (currentInstance) { const bounds = await backend.api.getComponentBounds(currentInstance) if (bounds) { @@ -146,7 +154,7 @@ async function updateOverlay (backend: DevtoolsBackend, ctx: BackendContext) { let updateTimer -function startUpdateTimer (backend: DevtoolsBackend, ctx: BackendContext) { +function startUpdateTimer(backend: DevtoolsBackend, ctx: BackendContext) { stopUpdateTimer() updateTimer = setInterval(() => { jobQueue.queue('updateOverlay', async () => { @@ -155,6 +163,6 @@ function startUpdateTimer (backend: DevtoolsBackend, ctx: BackendContext) { }, 1000 / 30) // 30fps } -function stopUpdateTimer () { +function stopUpdateTimer() { clearInterval(updateTimer) } diff --git a/packages/app-backend-core/src/hook.ts b/packages/app-backend-core/src/hook.ts index d9ea6fa11c..c13558a6f5 100644 --- a/packages/app-backend-core/src/hook.ts +++ b/packages/app-backend-core/src/hook.ts @@ -8,35 +8,41 @@ * * @param {Window|global} target */ -export function installHook (target, isIframe = false) { +export function installHook(target, isIframe = false) { const devtoolsVersion = '6.0' let listeners = {} - function injectIframeHook (iframe) { - if ((iframe as any).__vdevtools__injected) return + function injectIframeHook(iframe) { + if ((iframe as any).__vdevtools__injected) { + return + } try { (iframe as any).__vdevtools__injected = true const inject = () => { try { (iframe.contentWindow as any).__VUE_DEVTOOLS_IFRAME__ = iframe const script = iframe.contentDocument.createElement('script') - script.textContent = ';(' + installHook.toString() + ')(window, true)' + script.textContent = `;(${installHook.toString()})(window, true)` iframe.contentDocument.documentElement.appendChild(script) script.parentNode.removeChild(script) - } catch (e) { + } + catch (e) { // Ignore } } inject() iframe.addEventListener('load', () => inject()) - } catch (e) { + } + catch (e) { // Ignore } } let iframeChecks = 0 - function injectToIframes () { - if (typeof window === 'undefined') return + function injectToIframes() { + if (typeof window === 'undefined') { + return + } const iframes = document.querySelectorAll('iframe:not([data-vue-devtools-ignore])') for (const iframe of iframes) { @@ -62,15 +68,17 @@ export function installHook (target, isIframe = false) { let hook if (isIframe) { - const sendToParent = cb => { + const sendToParent = (cb) => { try { const hook = (window.parent as any).__VUE_DEVTOOLS_GLOBAL_HOOK__ if (hook) { - cb(hook) - } else { + return cb(hook) + } + else { console.warn('[Vue Devtools] No hook in parent window') } - } catch (e) { + } + catch (e) { console.warn('[Vue Devtools] Failed to send message to parent window', e) } } @@ -78,67 +86,81 @@ export function installHook (target, isIframe = false) { hook = { devtoolsVersion, // eslint-disable-next-line accessor-pairs - set Vue (value) { - sendToParent(hook => { hook.Vue = value }) + set Vue(value) { + sendToParent((hook) => { + hook.Vue = value + }) }, // eslint-disable-next-line accessor-pairs - set enabled (value) { - sendToParent(hook => { hook.enabled = value }) + set enabled(value) { + sendToParent((hook) => { + hook.enabled = value + }) }, - on (event, fn) { + on(event, fn) { sendToParent(hook => hook.on(event, fn)) }, - once (event, fn) { + once(event, fn) { sendToParent(hook => hook.once(event, fn)) }, - off (event, fn) { + off(event, fn) { sendToParent(hook => hook.off(event, fn)) }, - emit (event, ...args) { + emit(event, ...args) { sendToParent(hook => hook.emit(event, ...args)) }, + + cleanupBuffer(matchArg) { + return sendToParent(hook => hook.cleanupBuffer(matchArg)) ?? false + }, } - } else { + } + else { hook = { devtoolsVersion, Vue: null, enabled: undefined, _buffer: [], + _bufferMap: new Map(), + _bufferToRemove: new Map(), store: null, initialState: null, storeModules: null, flushStoreModules: null, apps: [], - _replayBuffer (event) { + _replayBuffer(event) { const buffer = this._buffer this._buffer = [] + this._bufferMap.clear() + this._bufferToRemove.clear() for (let i = 0, l = buffer.length; i < l; i++) { - const allArgs = buffer[i] + const allArgs = buffer[i].slice(1) allArgs[0] === event // eslint-disable-next-line prefer-spread ? this.emit.apply(this, allArgs) - : this._buffer.push(allArgs) + : this._buffer.push(buffer[i]) } }, - on (event, fn) { - const $event = '$' + event + on(event, fn) { + const $event = `$${event}` if (listeners[$event]) { listeners[$event].push(fn) - } else { + } + else { listeners[$event] = [fn] this._replayBuffer(event) } }, - once (event, fn) { + once(event, fn) { const on = (...args) => { this.off(event, on) return fn.apply(this, args) @@ -146,16 +168,18 @@ export function installHook (target, isIframe = false) { this.on(event, on) }, - off (event, fn) { - event = '$' + event + off(event, fn) { + event = `$${event}` if (!arguments.length) { listeners = {} - } else { + } + else { const cbs = listeners[event] if (cbs) { if (!fn) { listeners[event] = null - } else { + } + else { for (let i = 0, l = cbs.length; i < l; i++) { const cb = cbs[i] if (cb === fn || cb.fn === fn) { @@ -168,8 +192,8 @@ export function installHook (target, isIframe = false) { } }, - emit (event, ...args) { - const $event = '$' + event + emit(event, ...args) { + const $event = `$${event}` let cbs = listeners[$event] if (cbs) { cbs = cbs.slice() @@ -177,23 +201,59 @@ export function installHook (target, isIframe = false) { try { const result = cbs[i].apply(this, args) if (typeof result?.catch === 'function') { - result.catch(e => { + result.catch((e) => { console.error(`[Hook] Error in async event handler for ${event} with args:`, args) console.error(e) }) } - } catch (e) { + } + catch (e) { console.error(`[Hook] Error in event handler for ${event} with args:`, args) console.error(e) } } - } else { - this._buffer.push([event, ...args]) } + else { + const buffered = [Date.now(), event, ...args] + this._buffer.push(buffered) + + for (let i = 2; i < args.length; i++) { + if (typeof args[i] === 'object' && args[i]) { + // Save by component instance (3rd, 4th or 5th arg) + this._bufferMap.set(args[i], buffered) + break + } + } + } + }, + + /** + * Remove buffered events with any argument that is equal to the given value. + * @param matchArg Given value to match. + */ + cleanupBuffer(matchArg) { + const inBuffer = this._bufferMap.has(matchArg) + if (inBuffer) { + // Mark event for removal + this._bufferToRemove.set(this._bufferMap.get(matchArg), true) + } + return inBuffer + }, + + _cleanupBuffer() { + const now = Date.now() + // Clear buffer events that are older than 10 seconds or marked for removal + this._buffer = this._buffer.filter(args => !this._bufferToRemove.has(args) && now - args[0] < 10_000) + this._bufferToRemove.clear() + this._bufferMap.clear() }, } - hook.once('init', Vue => { + setInterval(() => { + hook._cleanupBuffer() + }, 10_000) + + hook.once('init', (Vue) => { hook.Vue = Vue if (Vue) { @@ -214,11 +274,11 @@ export function installHook (target, isIframe = false) { hook.emit('app:add', appRecord) }) - hook.once('vuex:init', store => { + hook.once('vuex:init', (store) => { hook.store = store hook.initialState = clone(store.state) const origReplaceState = store.replaceState.bind(store) - store.replaceState = state => { + store.replaceState = (state) => { hook.initialState = clone(state) origReplaceState(state) } @@ -228,7 +288,9 @@ export function installHook (target, isIframe = false) { hook.storeModules = [] origRegister = store.registerModule.bind(store) store.registerModule = (path, module, options) => { - if (typeof path === 'string') path = [path] + if (typeof path === 'string') { + path = [path] + } hook.storeModules.push({ path, module, options }) origRegister(path, module, options) if (process.env.NODE_ENV !== 'production') { @@ -238,10 +300,14 @@ export function installHook (target, isIframe = false) { } origUnregister = store.unregisterModule.bind(store) store.unregisterModule = (path) => { - if (typeof path === 'string') path = [path] + if (typeof path === 'string') { + path = [path] + } const key = path.join('/') const index = hook.storeModules.findIndex(m => m.path.join('/') === key) - if (index !== -1) hook.storeModules.splice(index, 1) + if (index !== -1) { + hook.storeModules.splice(index, 1) + } origUnregister(path) if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console @@ -261,7 +327,7 @@ export function installHook (target, isIframe = false) { } Object.defineProperty(target, '__VUE_DEVTOOLS_GLOBAL_HOOK__', { - get () { + get() { return hook }, }) @@ -271,7 +337,8 @@ export function installHook (target, isIframe = false) { try { target.__VUE_DEVTOOLS_HOOK_REPLAY__.forEach(cb => cb(hook)) target.__VUE_DEVTOOLS_HOOK_REPLAY__ = [] - } catch (e) { + } + catch (e) { console.error('[vue-devtools] Error during hook replay', e) } } @@ -296,7 +363,7 @@ export function installHook (target, isIframe = false) { /** * @enum * - * @const {Object} SUPPORTS + * @const {object} SUPPORTS * * @property {boolean} SYMBOL_PROPERTIES are symbol properties supported * @property {boolean} WEAKSET is WeakSet supported @@ -320,8 +387,8 @@ export function installHook (target, isIframe = false) { } const object = create({ - add: (value) => object._values.push(value), - has: (value) => !!~object._values.indexOf(value), + add: value => object._values.push(value), + has: value => !!~object._values.indexOf(value), }) object._values = [] @@ -344,7 +411,7 @@ export function installHook (target, isIframe = false) { return create(null) } - // eslint-disable-next-line no-proto + // eslint-disable-next-line no-proto, no-restricted-properties const prototype = object.__proto__ || getPrototypeOf(object) if (object.constructor === realm.Object) { @@ -354,7 +421,8 @@ export function installHook (target, isIframe = false) { if (~toStringFunction.call(object.constructor).indexOf('[native code]')) { try { return new object.constructor() - } catch (e) { + } + catch (e) { // Error } } @@ -488,7 +556,9 @@ export function installHook (target, isIframe = false) { const { isArray } = Array const GLOBAL_THIS = (() => { + // eslint-disable-next-line no-restricted-globals if (typeof self !== 'undefined') { + // eslint-disable-next-line no-restricted-globals return self } @@ -496,8 +566,8 @@ export function installHook (target, isIframe = false) { return window } - if (typeof global !== 'undefined') { - return global + if (typeof globalThis !== 'undefined') { + return globalThis } if (console && console.error) { @@ -524,7 +594,7 @@ export function installHook (target, isIframe = false) { * @param [options.realm] the realm (this) object the object is copied from * @returns the copied object */ - function clone (object, options = null) { + function clone(object, options = null) { // manually coalesced instead of default parameters for performance const isStrict = !!(options && options.isStrict) const realm = (options && options.realm) || GLOBAL_THIS @@ -654,13 +724,13 @@ export function installHook (target, isIframe = false) { // if the object cannot / should not be cloned, don't if ( // promise-like - (hasOwnProperty.call(object, 'then') && typeof object.then === 'function') || + (hasOwnProperty.call(object, 'then') && typeof object.then === 'function') // errors - object instanceof Error || + || object instanceof Error // weakmaps - (realm.WeakMap && object instanceof realm.WeakMap) || + || (realm.WeakMap && object instanceof realm.WeakMap) // weaksets - (realm.WeakSet && object instanceof realm.WeakSet) + || (realm.WeakSet && object instanceof realm.WeakSet) ) { return object } diff --git a/packages/app-backend-core/src/index.ts b/packages/app-backend-core/src/index.ts index c0bacc1a73..d10c082dc1 100644 --- a/packages/app-backend-core/src/index.ts +++ b/packages/app-backend-core/src/index.ts @@ -1,56 +1,63 @@ -import { - createBackendContext, +import type { + AppRecord, BackendContext, Plugin, - BuiltinBackendFeature, - AppRecord, } from '@vue-devtools/app-backend-api' import { + BuiltinBackendFeature, + createBackendContext, +} from '@vue-devtools/app-backend-api' +import type { Bridge, - HookEvents, +} from '@vue-devtools/shared-utils' +import { BridgeEvents, + BridgeSubscriptions, BuiltinTabs, + HookEvents, + SharedData, + createThrottleQueue, + getPluginSettings, initSharedData, - BridgeSubscriptions, + isBrowser, parse, + raf, revive, target, - getPluginSettings, - SharedData, - isBrowser, - raf, } from '@vue-devtools/shared-utils' import debounce from 'lodash/debounce' -import throttle from 'lodash/throttle' +import type { CustomInspectorOptions, PluginDescriptor, SetupFunction, TimelineEventOptions, TimelineLayerOptions } from '@vue/devtools-api' +import { Hooks, now } from '@vue/devtools-api' import { hook } from './global-hook' -import { subscribe, unsubscribe, isSubscribed } from './util/subscriptions' +import { isSubscribed, subscribe, unsubscribe } from './util/subscriptions' import { highlight, unHighlight } from './highlighter' -import { setupTimeline, sendTimelineLayers, addTimelineEvent, clearTimeline, sendTimelineEventData, sendTimelineLayerEvents } from './timeline' +import { addTimelineEvent, clearTimeline, sendTimelineEventData, sendTimelineLayerEvents, sendTimelineLayers, setupTimeline } from './timeline' import ComponentPicker from './component-pick' import { - sendComponentTreeData, - sendSelectedComponentData, - sendEmptyComponentData, - getComponentId, editComponentState, + getComponentId, getComponentInstance, refreshComponentTreeSearch, + sendComponentTreeData, sendComponentUpdateTracking, + sendEmptyComponentData, + sendSelectedComponentData, } from './component' -import { addQueuedPlugins, addPlugin, sendPluginList, addPreviouslyRegisteredPlugins } from './plugin' -import { PluginDescriptor, SetupFunction, TimelineLayerOptions, TimelineEventOptions, CustomInspectorOptions, Hooks, now } from '@vue/devtools-api' -import { registerApp, selectApp, waitForAppsRegistration, sendApps, _legacy_getAndRegisterApps, getAppRecord, removeApp } from './app' -import { sendInspectorTree, getInspector, getInspectorWithAppId, sendInspectorState, editInspectorState, sendCustomInspectors, selectInspectorNode } from './inspector' +import { addPlugin, addPreviouslyRegisteredPlugins, addQueuedPlugins, sendPluginList } from './plugin' +import { _legacy_getAndRegisterApps, getAppRecord, registerApp, removeApp, selectApp, sendApps, waitForAppsRegistration } from './app' +import { editInspectorState, getInspector, getInspectorWithAppId, selectInspectorNode, sendCustomInspectors, sendInspectorState, sendInspectorTree } from './inspector' import { showScreenshot } from './timeline-screenshot' import { performanceMarkEnd, performanceMarkStart } from './perf' import { initOnPageConfig } from './page-config' -import { sendTimelineMarkers, addTimelineMarker } from './timeline-marker' +import { addTimelineMarker, sendTimelineMarkers } from './timeline-marker' import { flashComponent } from './flash.js' let ctx: BackendContext = target.__vdevtools_ctx ?? null let connected = target.__vdevtools_connected ?? false -export async function initBackend (bridge: Bridge) { +let pageTitleObserver: MutationObserver + +export async function initBackend(bridge: Bridge) { await initSharedData({ bridge, persist: false, @@ -61,7 +68,7 @@ export async function initBackend (bridge: Bridge) { initOnPageConfig() if (!connected) { - // connected = false + // First connect ctx = target.__vdevtools_ctx = createBackendContext({ bridge, hook, @@ -78,25 +85,28 @@ export async function initBackend (bridge: Bridge) { SharedData.legacyApps = true }) - hook.on(HookEvents.APP_ADD, async app => { + hook.on(HookEvents.APP_ADD, async (app) => { await registerApp(app, ctx) connect() }) // Add apps that already sent init if (hook.apps.length) { - hook.apps.forEach(app => { + hook.apps.forEach((app) => { registerApp(app, ctx) connect() }) } - } else { + } + else { + // Reconnect ctx.bridge = bridge connectBridge() + ctx.bridge.send(BridgeEvents.TO_FRONT_RECONNECTED) } } -async function connect () { +async function connect() { if (connected) { return } @@ -110,53 +120,60 @@ async function connect () { // Apps - hook.on(HookEvents.APP_UNMOUNT, async app => { + hook.on(HookEvents.APP_UNMOUNT, async (app) => { await removeApp(app, ctx) }) // Components - const sendComponentUpdate = throttle(async (appRecord: AppRecord, id: string) => { - try { - // Update component inspector - if (id && isSubscribed(BridgeSubscriptions.SELECTED_COMPONENT_DATA, sub => sub.payload.instanceId === id)) { - await sendSelectedComponentData(appRecord, id, ctx) - } - - // Update tree (tags) - if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === id)) { - await sendComponentTreeData(appRecord, id, appRecord.componentFilter, 0, ctx) - } - } catch (e) { - if (SharedData.debugInfo) { - console.error(e) - } - } - }, 100) + const throttleQueue = createThrottleQueue(500) hook.on(HookEvents.COMPONENT_UPDATED, async (app, uid, parentUid, component) => { try { - if (!app || (typeof uid !== 'number' && !uid) || !component) return + if (!app || (typeof uid !== 'number' && !uid) || !component) { + return + } + const now = Date.now() + let id: string let appRecord: AppRecord if (app && uid != null) { id = await getComponentId(app, uid, component, ctx) appRecord = await getAppRecord(app, ctx) - } else { + } + else { id = ctx.currentInspectedComponentId appRecord = ctx.currentAppRecord } - if (SharedData.trackUpdates) { - await sendComponentUpdateTracking(id, ctx) - } + throttleQueue.add(`update:${id}`, async () => { + try { + if (SharedData.trackUpdates) { + sendComponentUpdateTracking(id, now, ctx) + } - if (SharedData.flashUpdates) { - await flashComponent(component, appRecord.backend) - } + if (SharedData.flashUpdates) { + await flashComponent(component, appRecord.backend) + } - await sendComponentUpdate(appRecord, id) - } catch (e) { + // Update component inspector + if (ctx.currentInspectedComponentId === id) { + await sendSelectedComponentData(appRecord, ctx.currentInspectedComponentId, ctx) + } + + // Update tree (tags) + if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, id)) { + await sendComponentTreeData(appRecord, id, appRecord.componentFilter, 0, false, ctx) + } + } + catch (e) { + if (SharedData.debugInfo) { + console.error(e) + } + } + }) + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -165,51 +182,67 @@ async function connect () { hook.on(HookEvents.COMPONENT_ADDED, async (app, uid, parentUid, component) => { try { - if (!app || (typeof uid !== 'number' && !uid) || !component) return - const id = await getComponentId(app, uid, component, ctx) - const appRecord = await getAppRecord(app, ctx) - if (component) { - if (component.__VUE_DEVTOOLS_UID__ == null) { - component.__VUE_DEVTOOLS_UID__ = id - } - if (!appRecord.instanceMap.has(id)) { - appRecord.instanceMap.set(id, component) - } + if (!app || (typeof uid !== 'number' && !uid) || !component) { + return } + const now = Date.now() + const id = await getComponentId(app, uid, component, ctx) - if (parentUid != null) { - const parentInstances = await appRecord.backend.api.walkComponentParents(component) - if (parentInstances.length) { - // Check two parents level to update `hasChildren - for (let i = 0; i < parentInstances.length; i++) { - const parentId = await getComponentId(app, parentUid, parentInstances[i], ctx) - if (i < 2 && isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === parentId)) { - raf(() => { - sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, ctx) - }) + throttleQueue.add(`add:${id}`, async () => { + try { + const appRecord = await getAppRecord(app, ctx) + if (component) { + if (component.__VUE_DEVTOOLS_UID__ == null) { + component.__VUE_DEVTOOLS_UID__ = id } + if (appRecord?.instanceMap) { + if (!appRecord.instanceMap.has(id)) { + appRecord.instanceMap.set(id, component) + } + } + } + + if (parentUid != null && appRecord?.instanceMap) { + const parentInstances = await appRecord.backend.api.walkComponentParents(component) + if (parentInstances.length) { + // Check two parents level to update `hasChildren + for (let i = 0; i < parentInstances.length; i++) { + const parentId = await getComponentId(app, parentUid, parentInstances[i], ctx) + if (i < 2 && isSubscribed(BridgeSubscriptions.COMPONENT_TREE, parentId)) { + raf(() => { + sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, false, ctx) + }) + } - if (SharedData.trackUpdates) { - await sendComponentUpdateTracking(parentId, ctx) + if (SharedData.trackUpdates) { + sendComponentUpdateTracking(parentId, now, ctx) + } + } } } - } - } - if (ctx.currentInspectedComponentId === id) { - await sendSelectedComponentData(appRecord, id, ctx) - } + if (ctx.currentInspectedComponentId === id) { + await sendSelectedComponentData(appRecord, id, ctx) + } - if (SharedData.trackUpdates) { - await sendComponentUpdateTracking(id, ctx) - } + if (SharedData.trackUpdates) { + sendComponentUpdateTracking(id, now, ctx) + } - if (SharedData.flashUpdates) { - await flashComponent(component, appRecord.backend) - } + if (SharedData.flashUpdates) { + await flashComponent(component, appRecord.backend) + } - await refreshComponentTreeSearch(ctx) - } catch (e) { + await refreshComponentTreeSearch(ctx) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(e) + } + } + }) + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -218,34 +251,55 @@ async function connect () { hook.on(HookEvents.COMPONENT_REMOVED, async (app, uid, parentUid, component) => { try { - if (!app || (typeof uid !== 'number' && !uid) || !component) return - const appRecord = await getAppRecord(app, ctx) - if (parentUid != null) { - const parentInstances = await appRecord.backend.api.walkComponentParents(component) - if (parentInstances.length) { - const parentId = await getComponentId(app, parentUid, parentInstances[0], ctx) - if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === parentId)) { - raf(async () => { - try { - sendComponentTreeData(await getAppRecord(app, ctx), parentId, appRecord.componentFilter, null, ctx) - } catch (e) { - if (SharedData.debugInfo) { - console.error(e) - } + if (!app || (typeof uid !== 'number' && !uid) || !component) { + return + } + const id = await getComponentId(app, uid, component, ctx) + + throttleQueue.add(`remove:${id}`, async () => { + try { + const appRecord = await getAppRecord(app, ctx) + if (parentUid != null && appRecord) { + const parentInstances = await appRecord.backend.api.walkComponentParents(component) + if (parentInstances.length) { + const parentId = await getComponentId(app, parentUid, parentInstances[0], ctx) + if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, parentId)) { + raf(async () => { + try { + const appRecord = await getAppRecord(app, ctx) + + if (appRecord) { + sendComponentTreeData(appRecord, parentId, appRecord.componentFilter, null, false, ctx) + } + } + catch (e) { + if (SharedData.debugInfo) { + console.error(e) + } + } + }) } - }) + } } - } - } - const id = await getComponentId(app, uid, component, ctx) - if (isSubscribed(BridgeSubscriptions.SELECTED_COMPONENT_DATA, sub => sub.payload.instanceId === id)) { - await sendEmptyComponentData(id, ctx) - } - appRecord.instanceMap.delete(id) + if (isSubscribed(BridgeSubscriptions.SELECTED_COMPONENT_DATA, id)) { + await sendEmptyComponentData(id, ctx) + } - await refreshComponentTreeSearch(ctx) - } catch (e) { + if (appRecord) { + appRecord.instanceMap.delete(id) + } + + await refreshComponentTreeSearch(ctx) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(e) + } + } + }) + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -253,7 +307,7 @@ async function connect () { }) hook.on(HookEvents.TRACK_UPDATE, (id, ctx) => { - sendComponentUpdateTracking(id, ctx) + sendComponentUpdateTracking(id, Date.now(), ctx) }) hook.on(HookEvents.FLASH_UPDATE, (instance, backend) => { @@ -262,22 +316,22 @@ async function connect () { // Component perf - hook.on(HookEvents.PERFORMANCE_START, async (app, uid, vm, type, time) => { - await performanceMarkStart(app, uid, vm, type, time, ctx) + hook.on(HookEvents.PERFORMANCE_START, (app, uid, vm, type, time) => { + performanceMarkStart(app, uid, vm, type, time, ctx) }) - hook.on(HookEvents.PERFORMANCE_END, async (app, uid, vm, type, time) => { - await performanceMarkEnd(app, uid, vm, type, time, ctx) + hook.on(HookEvents.PERFORMANCE_END, (app, uid, vm, type, time) => { + performanceMarkEnd(app, uid, vm, type, time, ctx) }) // Highlighter - hook.on(HookEvents.COMPONENT_HIGHLIGHT, async instanceId => { - await highlight(ctx.currentAppRecord.instanceMap.get(instanceId), ctx.currentAppRecord.backend, ctx) + hook.on(HookEvents.COMPONENT_HIGHLIGHT, (instanceId) => { + highlight(ctx.currentAppRecord.instanceMap.get(instanceId), ctx.currentAppRecord.backend, ctx) }) - hook.on(HookEvents.COMPONENT_UNHIGHLIGHT, async () => { - await unHighlight() + hook.on(HookEvents.COMPONENT_UNHIGHLIGHT, () => { + unHighlight() }) // Timeline @@ -286,13 +340,15 @@ async function connect () { hook.on(HookEvents.TIMELINE_LAYER_ADDED, async (options: TimelineLayerOptions, plugin: Plugin) => { const appRecord = await getAppRecord(plugin.descriptor.app, ctx) - ctx.timelineLayers.push({ - ...options, - appRecord, - plugin, - events: [], - }) - ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_LAYER_ADD, {}) + if (appRecord) { + ctx.timelineLayers.push({ + ...options, + appRecord, + plugin, + events: [], + }) + ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_LAYER_ADD, {}) + } }) hook.on(HookEvents.TIMELINE_EVENT_ADDED, async (options: TimelineEventOptions, plugin: Plugin) => { @@ -303,21 +359,24 @@ async function connect () { hook.on(HookEvents.CUSTOM_INSPECTOR_ADD, async (options: CustomInspectorOptions, plugin: Plugin) => { const appRecord = await getAppRecord(plugin.descriptor.app, ctx) - ctx.customInspectors.push({ - ...options, - appRecord, - plugin, - treeFilter: '', - selectedNodeId: null, - }) - ctx.bridge.send(BridgeEvents.TO_FRONT_CUSTOM_INSPECTOR_ADD, {}) + if (appRecord) { + ctx.customInspectors.push({ + ...options, + appRecord, + plugin, + treeFilter: '', + selectedNodeId: null, + }) + ctx.bridge.send(BridgeEvents.TO_FRONT_CUSTOM_INSPECTOR_ADD, {}) + } }) hook.on(HookEvents.CUSTOM_INSPECTOR_SEND_TREE, async (inspectorId: string, plugin: Plugin) => { const inspector = getInspector(inspectorId, plugin.descriptor.app, ctx) if (inspector) { await sendInspectorTree(inspector, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -326,7 +385,8 @@ async function connect () { const inspector = getInspector(inspectorId, plugin.descriptor.app, ctx) if (inspector) { await sendInspectorState(inspector, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -335,7 +395,8 @@ async function connect () { const inspector = getInspector(inspectorId, plugin.descriptor.app, ctx) if (inspector) { await selectInspectorNode(inspector, nodeId, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -344,15 +405,21 @@ async function connect () { try { await addPreviouslyRegisteredPlugins(ctx) - } catch (e) { - console.error(`Error adding previously registered plugins:`) - console.error(e) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(`Error adding previously registered plugins:`) + console.error(e) + } } try { await addQueuedPlugins(ctx) - } catch (e) { - console.error(`Error adding queued plugins:`) - console.error(e) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(`Error adding queued plugins:`) + console.error(e) + } } hook.on(HookEvents.SETUP_DEVTOOLS_PLUGIN, async (pluginDescriptor: PluginDescriptor, setupFn: SetupFunction) => { @@ -365,7 +432,7 @@ async function connect () { const handleFlush = debounce(async () => { if (ctx.currentAppRecord?.backend.options.features.includes(BuiltinBackendFeature.FLUSH)) { - await sendComponentTreeData(ctx.currentAppRecord, '_root', ctx.currentAppRecord.componentFilter, null, ctx) + await sendComponentTreeData(ctx.currentAppRecord, '_root', ctx.currentAppRecord.componentFilter, null, false, ctx) if (ctx.currentInspectedComponentId) { await sendSelectedComponentData(ctx.currentAppRecord, ctx.currentInspectedComponentId, ctx) } @@ -385,26 +452,29 @@ async function connect () { color: 0x41B883, all: true, }, ctx) - } catch (e) { - console.error(`Error while adding devtools connected timeline marker:`) - console.error(e) + } + catch (e) { + if (SharedData.debugInfo) { + console.error(`Error while adding devtools connected timeline marker:`) + console.error(e) + } } } -function connectBridge () { +function connectBridge() { // Subscriptions - ctx.bridge.on(BridgeEvents.TO_BACK_SUBSCRIBE, ({ type, payload }) => { - subscribe(type, payload) + ctx.bridge.on(BridgeEvents.TO_BACK_SUBSCRIBE, ({ type, key }) => { + subscribe(type, key) }) - ctx.bridge.on(BridgeEvents.TO_BACK_UNSUBSCRIBE, ({ type, payload }) => { - unsubscribe(type, payload) + ctx.bridge.on(BridgeEvents.TO_BACK_UNSUBSCRIBE, ({ type, key }) => { + unsubscribe(type, key) }) // Tabs - ctx.bridge.on(BridgeEvents.TO_BACK_TAB_SWITCH, async tab => { + ctx.bridge.on(BridgeEvents.TO_BACK_TAB_SWITCH, async (tab) => { ctx.currentTab = tab await unHighlight() }) @@ -415,12 +485,15 @@ function connectBridge () { await sendApps(ctx) }) - ctx.bridge.on(BridgeEvents.TO_BACK_APP_SELECT, async id => { - if (id == null) return + ctx.bridge.on(BridgeEvents.TO_BACK_APP_SELECT, async (id) => { + if (id == null) { + return + } const record = ctx.appRecords.find(r => r.id === id) if (record) { await selectApp(record, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`App with id ${id} not found`) } }) @@ -433,10 +506,10 @@ function connectBridge () { // Components - ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_TREE, async ({ instanceId, filter }) => { + ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_TREE, async ({ instanceId, filter, recursively }) => { ctx.currentAppRecord.componentFilter = filter - subscribe(BridgeSubscriptions.COMPONENT_TREE, { instanceId }) - await sendComponentTreeData(ctx.currentAppRecord, instanceId, filter, null, ctx) + subscribe(BridgeSubscriptions.COMPONENT_TREE, instanceId) + await sendComponentTreeData(ctx.currentAppRecord, instanceId, filter, null, recursively, ctx) }) ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_SELECTED_DATA, async (instanceId) => { @@ -452,7 +525,6 @@ function connectBridge () { if (instance) { const [el] = await ctx.currentAppRecord.backend.api.getComponentRootElements(instance) if (el) { - // @ts-ignore target.__VUE_DEVTOOLS_INSPECT_TARGET__ = el ctx.bridge.send(BridgeEvents.TO_FRONT_COMPONENT_INSPECT_DOM, null) } @@ -460,7 +532,9 @@ function connectBridge () { }) ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_SCROLL_TO, async ({ instanceId }) => { - if (!isBrowser) return + if (!isBrowser) { + return + } const instance = getComponentInstance(ctx.currentAppRecord, instanceId, ctx) if (instance) { const [el] = await ctx.currentAppRecord.backend.api.getComponentRootElements(instance) @@ -471,7 +545,8 @@ function connectBridge () { block: 'center', inline: 'center', }) - } else { + } + else { // Handle nodes that don't implement scrollIntoView const bounds = await ctx.currentAppRecord.backend.api.getComponentBounds(instance) const scrollTarget = document.createElement('div') @@ -499,7 +574,9 @@ function connectBridge () { }) ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_RENDER_CODE, async ({ instanceId }) => { - if (!isBrowser) return + if (!isBrowser) { + return + } const instance = getComponentInstance(ctx.currentAppRecord, instanceId, ctx) if (instance) { const { code } = await ctx.currentAppRecord.backend.api.getComponentRenderCode(instance) @@ -516,17 +593,21 @@ function connectBridge () { if (action) { try { await action() - } catch (e) { - console.error(e) } - } else { + catch (e) { + if (SharedData.debugInfo) { + console.error(e) + } + } + } + else if (SharedData.debugInfo) { console.warn(`Couldn't revive action ${actionIndex} from`, value) } }) // Highlighter - ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_MOUSE_OVER, async instanceId => { + ctx.bridge.on(BridgeEvents.TO_BACK_COMPONENT_MOUSE_OVER, async (instanceId) => { await highlight(ctx.currentAppRecord.instanceMap.get(instanceId), ctx.currentAppRecord.backend, ctx) }) @@ -583,7 +664,8 @@ function connectBridge () { if (inspector) { inspector.treeFilter = treeFilter sendInspectorTree(inspector, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -593,7 +675,8 @@ function connectBridge () { if (inspector) { inspector.selectedNodeId = nodeId sendInspectorState(inspector, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -604,7 +687,8 @@ function connectBridge () { await editInspectorState(inspector, nodeId, path, type, payload, ctx) inspector.selectedNodeId = nodeId await sendInspectorState(inspector, ctx) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -615,12 +699,14 @@ function connectBridge () { const action = inspector[actionType ?? 'actions'][actionIndex] try { await action.action(...(args ?? [])) - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } } - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Inspector ${inspectorId} not found`) } }) @@ -631,7 +717,8 @@ function connectBridge () { let value = payload.value if (payload.serialized) { value = parse(value, payload.revive) - } else if (payload.revive) { + } + else if (payload.revive) { value = revive(value) } // eslint-disable-next-line no-console @@ -656,4 +743,18 @@ function connectBridge () { settings, }) }) + + ctx.bridge.send(BridgeEvents.TO_FRONT_TITLE, { title: document.title }) + // Watch page title + const titleEl = document.querySelector('title') + if (titleEl && typeof MutationObserver !== 'undefined') { + if (pageTitleObserver) { + pageTitleObserver.disconnect() + } + pageTitleObserver = new MutationObserver((mutations) => { + const title = mutations[0].target as HTMLTitleElement + ctx.bridge.send(BridgeEvents.TO_FRONT_TITLE, { title: title.textContent }) + }) + pageTitleObserver.observe(titleEl, { subtree: true, characterData: true, childList: true }) + } } diff --git a/packages/app-backend-core/src/inspector.ts b/packages/app-backend-core/src/inspector.ts index 501c3842e4..67e79a3838 100644 --- a/packages/app-backend-core/src/inspector.ts +++ b/packages/app-backend-core/src/inspector.ts @@ -1,12 +1,12 @@ -import { App } from '@vue/devtools-api' -import { BackendContext, CustomInspector } from '@vue-devtools/app-backend-api' +import type { App } from '@vue/devtools-api' +import type { BackendContext, CustomInspector } from '@vue-devtools/app-backend-api' import { BridgeEvents, parse, stringify } from '@vue-devtools/shared-utils' -export function getInspector (inspectorId: string, app: App, ctx: BackendContext) { +export function getInspector(inspectorId: string, app: App, ctx: BackendContext) { return ctx.customInspectors.find(i => i.id === inspectorId && i.appRecord.options.app === app) } -export async function getInspectorWithAppId (inspectorId: string, appId: string, ctx: BackendContext): Promise { +export async function getInspectorWithAppId(inspectorId: string, appId: string, ctx: BackendContext): Promise { for (const i of ctx.customInspectors) { if (i.id === inspectorId && i.appRecord.id === appId) { return i @@ -15,7 +15,7 @@ export async function getInspectorWithAppId (inspectorId: string, appId: string, return null } -export async function sendInspectorTree (inspector: CustomInspector, ctx: BackendContext) { +export async function sendInspectorTree(inspector: CustomInspector, ctx: BackendContext) { const rootNodes = await inspector.appRecord.backend.api.getInspectorTree(inspector.id, inspector.appRecord.options.app, inspector.treeFilter) ctx.bridge.send(BridgeEvents.TO_FRONT_CUSTOM_INSPECTOR_TREE, { appId: inspector.appRecord.id, @@ -24,7 +24,7 @@ export async function sendInspectorTree (inspector: CustomInspector, ctx: Backen }) } -export async function sendInspectorState (inspector: CustomInspector, ctx: BackendContext) { +export async function sendInspectorState(inspector: CustomInspector, ctx: BackendContext) { const state = inspector.selectedNodeId ? await inspector.appRecord.backend.api.getInspectorState(inspector.id, inspector.appRecord.options.app, inspector.selectedNodeId) : null ctx.bridge.send(BridgeEvents.TO_FRONT_CUSTOM_INSPECTOR_STATE, { appId: inspector.appRecord.id, @@ -33,14 +33,14 @@ export async function sendInspectorState (inspector: CustomInspector, ctx: Backe }) } -export async function editInspectorState (inspector: CustomInspector, nodeId: string, dotPath: string, type: string, state: any, ctx: BackendContext) { +export async function editInspectorState(inspector: CustomInspector, nodeId: string, dotPath: string, type: string, state: any, _ctx: BackendContext) { await inspector.appRecord.backend.api.editInspectorState(inspector.id, inspector.appRecord.options.app, nodeId, dotPath, type, { ...state, value: state.value != null ? parse(state.value, true) : state.value, }) } -export async function sendCustomInspectors (ctx: BackendContext) { +export async function sendCustomInspectors(ctx: BackendContext) { const inspectors = [] for (const i of ctx.customInspectors) { inspectors.push({ @@ -67,7 +67,7 @@ export async function sendCustomInspectors (ctx: BackendContext) { }) } -export async function selectInspectorNode (inspector: CustomInspector, nodeId: string, ctx: BackendContext) { +export async function selectInspectorNode(inspector: CustomInspector, nodeId: string, ctx: BackendContext) { ctx.bridge.send(BridgeEvents.TO_FRONT_CUSTOM_INSPECTOR_SELECT_NODE, { appId: inspector.appRecord.id, inspectorId: inspector.id, diff --git a/packages/app-backend-core/src/legacy/scan.ts b/packages/app-backend-core/src/legacy/scan.ts index bdb73a7fcb..7d4428744e 100644 --- a/packages/app-backend-core/src/legacy/scan.ts +++ b/packages/app-backend-core/src/legacy/scan.ts @@ -6,16 +6,15 @@ const rootInstances = [] /** * Scan the page for root level Vue instances. */ -export function scan () { +export function scan() { rootInstances.length = 0 let inFragment = false let currentFragment = null - // eslint-disable-next-line no-inner-declarations - function processInstance (instance) { + function processInstance(instance) { if (instance) { - if (rootInstances.indexOf(instance.$root) === -1) { + if (!rootInstances.includes(instance.$root)) { instance = instance.$root } if (instance._isFragment) { @@ -37,8 +36,8 @@ export function scan () { } if (isBrowser) { - const walkDocument = document => { - walk(document, function (node) { + const walkDocument = (document) => { + walk(document, (node) => { if (inFragment) { if (node === currentFragment._fragmentEnd) { inFragment = false @@ -57,7 +56,8 @@ export function scan () { for (const iframe of iframes) { try { walkDocument(iframe.contentDocument) - } catch (e) { + } + catch (e) { // Ignore } } @@ -68,11 +68,13 @@ export function scan () { for (const customTarget of customTargets) { try { walkDocument(customTarget) - } catch (e) { + } + catch (e) { // Ignore } } - } else { + } + else { if (Array.isArray(target.__VUE_ROOT_INSTANCES__)) { target.__VUE_ROOT_INSTANCES__.map(processInstance) } @@ -88,7 +90,7 @@ export function scan () { * @param {Function} fn */ -function walk (node, fn) { +function walk(node, fn) { if (node.childNodes) { for (let i = 0, l = node.childNodes.length; i < l; i++) { const child = node.childNodes[i] diff --git a/packages/app-backend-core/src/page-config.ts b/packages/app-backend-core/src/page-config.ts index 42b627a3e9..d570ae97f8 100644 --- a/packages/app-backend-core/src/page-config.ts +++ b/packages/app-backend-core/src/page-config.ts @@ -1,4 +1,4 @@ -import { target, SharedData } from '@vue-devtools/shared-utils' +import { SharedData, target } from '@vue-devtools/shared-utils' export interface PageConfig { openInEditorHost?: string @@ -8,11 +8,11 @@ export interface PageConfig { let config: PageConfig = {} -export function getPageConfig (): PageConfig { +export function getPageConfig(): PageConfig { return config } -export function initOnPageConfig () { +export function initOnPageConfig() { // User project devtools config if (Object.hasOwnProperty.call(target, 'VUE_DEVTOOLS_CONFIG')) { config = SharedData.pageConfig = target.VUE_DEVTOOLS_CONFIG diff --git a/packages/app-backend-core/src/perf.ts b/packages/app-backend-core/src/perf.ts index db5697980f..4cfa2f03c2 100644 --- a/packages/app-backend-core/src/perf.ts +++ b/packages/app-backend-core/src/perf.ts @@ -1,12 +1,20 @@ -import { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' -import { App, ComponentInstance } from '@vue/devtools-api' -import { BridgeSubscriptions, raf, SharedData } from '@vue-devtools/shared-utils' +import type { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' +import type { App, ComponentInstance } from '@vue/devtools-api' +import { BridgeSubscriptions, SharedData, raf } from '@vue-devtools/shared-utils' import { addTimelineEvent } from './timeline' import { getAppRecord } from './app' import { getComponentId, sendComponentTreeData } from './component' import { isSubscribed } from './util/subscriptions' -export async function performanceMarkStart ( +const markEndQueue = new Map() + +export async function performanceMarkStart( app: App, uid: number, instance: ComponentInstance, @@ -15,8 +23,13 @@ export async function performanceMarkStart ( ctx: BackendContext, ) { try { - if (!SharedData.performanceMonitoringEnabled) return + if (!SharedData.performanceMonitoringEnabled) { + return + } const appRecord = await getAppRecord(app, ctx) + if (!appRecord) { + return + } const componentName = await appRecord.backend.api.getComponentName(instance) const groupId = ctx.perfUniqueGroupId++ const groupKey = `${uid}-${type}` @@ -54,22 +67,15 @@ export async function performanceMarkStart ( ctx, ) } - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } } } -const markEndQueue = new Map() - -export async function performanceMarkEnd ( +export async function performanceMarkEnd( app: App, uid: number, instance: ComponentInstance, @@ -78,8 +84,13 @@ export async function performanceMarkEnd ( ctx: BackendContext, ) { try { - if (!SharedData.performanceMonitoringEnabled) return + if (!SharedData.performanceMonitoringEnabled) { + return + } const appRecord = await getAppRecord(app, ctx) + if (!appRecord) { + return + } const componentName = await appRecord.backend.api.getComponentName(instance) const groupKey = `${uid}-${type}` const groupInfo = appRecord.perfGroupIds.get(groupKey) @@ -143,22 +154,23 @@ export async function performanceMarkEnd ( if (change) { // Update component tree const id = await getComponentId(app, uid, instance, ctx) - if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, sub => sub.payload.instanceId === id)) { + if (isSubscribed(BridgeSubscriptions.COMPONENT_TREE, id)) { raf(() => { - sendComponentTreeData(appRecord, id, ctx.currentAppRecord.componentFilter, null, ctx) + sendComponentTreeData(appRecord, id, ctx.currentAppRecord.componentFilter, null, false, ctx) }) } } } - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } } } -export function handleAddPerformanceTag (backend: DevtoolsBackend, ctx: BackendContext) { - backend.api.on.visitComponentTree(payload => { +export function handleAddPerformanceTag(backend: DevtoolsBackend, _ctx: BackendContext) { + backend.api.on.visitComponentTree((payload) => { if (payload.componentInstance.__VUE_DEVTOOLS_SLOW__) { const { duration, measures } = payload.componentInstance.__VUE_DEVTOOLS_SLOW__ diff --git a/packages/app-backend-core/src/plugin.ts b/packages/app-backend-core/src/plugin.ts index b9d5099330..cf726df926 100644 --- a/packages/app-backend-core/src/plugin.ts +++ b/packages/app-backend-core/src/plugin.ts @@ -1,9 +1,10 @@ -import { PluginQueueItem } from '@vue/devtools-api' -import { Plugin, BackendContext, DevtoolsPluginApiInstance } from '@vue-devtools/app-backend-api' -import { BridgeEvents, target } from '@vue-devtools/shared-utils' +import type { PluginQueueItem } from '@vue/devtools-api' +import type { BackendContext, Plugin } from '@vue-devtools/app-backend-api' +import { DevtoolsPluginApiInstance } from '@vue-devtools/app-backend-api' +import { BridgeEvents, SharedData, target } from '@vue-devtools/shared-utils' import { getAppRecord, getAppRecordId } from './app' -export async function addPlugin (pluginQueueItem: PluginQueueItem, ctx: BackendContext) { +export async function addPlugin(pluginQueueItem: PluginQueueItem, ctx: BackendContext) { const { pluginDescriptor, setupFn } = pluginQueueItem const plugin: Plugin = { @@ -14,15 +15,22 @@ export async function addPlugin (pluginQueueItem: PluginQueueItem, ctx: BackendC ctx.currentPlugin = plugin try { const appRecord = await getAppRecord(plugin.descriptor.app, ctx) + if (!appRecord) { + return + } const api = new DevtoolsPluginApiInstance(plugin, appRecord, ctx) if (pluginQueueItem.proxy) { await pluginQueueItem.proxy.setRealTarget(api) - } else { + } + else { setupFn(api) } - } catch (e) { + } + catch (e) { plugin.error = e - console.error(e) + if (SharedData.debugInfo) { + console.error(e) + } } ctx.currentPlugin = null ctx.plugins.push(plugin) @@ -37,7 +45,7 @@ export async function addPlugin (pluginQueueItem: PluginQueueItem, ctx: BackendC }) } -export async function addQueuedPlugins (ctx: BackendContext) { +export async function addQueuedPlugins(ctx: BackendContext) { if (target.__VUE_DEVTOOLS_PLUGINS__ && Array.isArray(target.__VUE_DEVTOOLS_PLUGINS__)) { for (const queueItem of target.__VUE_DEVTOOLS_PLUGINS__) { await addPlugin(queueItem, ctx) @@ -46,7 +54,7 @@ export async function addQueuedPlugins (ctx: BackendContext) { } } -export async function addPreviouslyRegisteredPlugins (ctx: BackendContext) { +export async function addPreviouslyRegisteredPlugins(ctx: BackendContext) { if (target.__VUE_DEVTOOLS_REGISTERED_PLUGINS__ && Array.isArray(target.__VUE_DEVTOOLS_REGISTERED_PLUGINS__)) { for (const queueItem of target.__VUE_DEVTOOLS_REGISTERED_PLUGINS__) { await addPlugin(queueItem, ctx) @@ -54,13 +62,13 @@ export async function addPreviouslyRegisteredPlugins (ctx: BackendContext) { } } -export async function sendPluginList (ctx: BackendContext) { +export async function sendPluginList(ctx: BackendContext) { ctx.bridge.send(BridgeEvents.TO_FRONT_DEVTOOLS_PLUGIN_LIST, { plugins: await Promise.all(ctx.plugins.map(p => serializePlugin(p))), }) } -export async function serializePlugin (plugin: Plugin) { +export async function serializePlugin(plugin: Plugin) { return { id: plugin.descriptor.id, label: plugin.descriptor.label, diff --git a/packages/app-backend-core/src/timeline-builtins.ts b/packages/app-backend-core/src/timeline-builtins.ts index 3322d27ed8..9b365c9c5e 100644 --- a/packages/app-backend-core/src/timeline-builtins.ts +++ b/packages/app-backend-core/src/timeline-builtins.ts @@ -1,15 +1,15 @@ -import { TimelineLayerOptions } from '@vue/devtools-api' +import type { TimelineLayerOptions } from '@vue/devtools-api' export const builtinLayers: TimelineLayerOptions[] = [ { id: 'mouse', label: 'Mouse', color: 0xA451AF, - screenshotOverlayRender (event, { events }) { + screenshotOverlayRender(event, { events }) { const samePositionEvent = events.find(e => e !== event && e.renderMeta.textEl && e.data.x === event.data.x && e.data.y === event.data.y) if (samePositionEvent) { const text = document.createElement('div') - text.innerText = event.data.type + text.textContent = event.data.type samePositionEvent.renderMeta.textEl.appendChild(text) return false } @@ -24,7 +24,7 @@ export const builtinLayers: TimelineLayerOptions[] = [ div.style.backgroundColor = 'rgba(164, 81, 175, 0.5)' const text = document.createElement('div') - text.innerText = event.data.type + text.textContent = event.data.type text.style.color = '#541e5b' text.style.fontFamily = 'monospace' text.style.fontSize = '9px' @@ -52,10 +52,10 @@ export const builtinLayers: TimelineLayerOptions[] = [ color: 0x41B883, screenshotOverlayRender: (event, { events }) => { if (!event.meta.bounds || events.some(e => e !== event && e.layerId === event.layerId && e.renderMeta.drawn && (e.meta.componentId === event.meta.componentId || ( - e.meta.bounds.left === event.meta.bounds.left && - e.meta.bounds.top === event.meta.bounds.top && - e.meta.bounds.width === event.meta.bounds.width && - e.meta.bounds.height === event.meta.bounds.height + e.meta.bounds.left === event.meta.bounds.left + && e.meta.bounds.top === event.meta.bounds.top + && e.meta.bounds.width === event.meta.bounds.width + && e.meta.bounds.height === event.meta.bounds.height )))) { return false } @@ -83,7 +83,7 @@ export const builtinLayers: TimelineLayerOptions[] = [ text.style.padding = '1px' text.style.backgroundColor = 'rgba(255, 255, 255, 0.9)' text.style.borderRadius = '3px' - text.innerText = event.data.event + text.textContent = event.data.event div.appendChild(text) event.renderMeta.drawn = true @@ -94,7 +94,7 @@ export const builtinLayers: TimelineLayerOptions[] = [ { id: 'performance', label: 'Performance', - color: 0x41b86a, + color: 0x41B86A, groupsOnly: true, skipScreenshots: true, ignoreNoDurationGroups: true, diff --git a/packages/app-backend-core/src/timeline-marker.ts b/packages/app-backend-core/src/timeline-marker.ts index 104c1b91f3..8ac048e27f 100644 --- a/packages/app-backend-core/src/timeline-marker.ts +++ b/packages/app-backend-core/src/timeline-marker.ts @@ -1,9 +1,13 @@ -import { BackendContext, TimelineMarker } from '@vue-devtools/app-backend-api' -import { BridgeEvents } from '@vue-devtools/shared-utils' -import { isPerformanceSupported, TimelineMarkerOptions } from '@vue/devtools-api' +import type { BackendContext, TimelineMarker } from '@vue-devtools/app-backend-api' +import { BridgeEvents, SharedData } from '@vue-devtools/shared-utils' +import type { TimelineMarkerOptions } from '@vue/devtools-api' +import { isPerformanceSupported } from '@vue/devtools-api' import { dateThreshold, perfTimeDiff } from './timeline' -export async function addTimelineMarker (options: TimelineMarkerOptions, ctx: BackendContext) { +export async function addTimelineMarker(options: TimelineMarkerOptions, ctx: BackendContext) { + if (!SharedData.timelineRecording) { + return + } if (!ctx.currentAppRecord) { options.all = true } @@ -18,8 +22,13 @@ export async function addTimelineMarker (options: TimelineMarkerOptions, ctx: Ba }) } -export async function sendTimelineMarkers (ctx: BackendContext) { - if (!ctx.currentAppRecord) return +export async function sendTimelineMarkers(ctx: BackendContext) { + if (!SharedData.timelineRecording) { + return + } + if (!ctx.currentAppRecord) { + return + } const markers = ctx.timelineMarkers.filter(marker => marker.all || marker.appRecord === ctx.currentAppRecord) const result = [] for (const marker of markers) { @@ -31,7 +40,7 @@ export async function sendTimelineMarkers (ctx: BackendContext) { }) } -async function serializeMarker (marker: TimelineMarker) { +async function serializeMarker(marker: TimelineMarker) { let time = marker.time if (isPerformanceSupported() && time < dateThreshold) { time += perfTimeDiff diff --git a/packages/app-backend-core/src/timeline-screenshot.ts b/packages/app-backend-core/src/timeline-screenshot.ts index 3a99763d5e..1b11a5f713 100644 --- a/packages/app-backend-core/src/timeline-screenshot.ts +++ b/packages/app-backend-core/src/timeline-screenshot.ts @@ -1,5 +1,5 @@ -import { BackendContext } from '@vue-devtools/app-backend-api' -import { ID, ScreenshotOverlayRenderContext } from '@vue/devtools-api' +import type { BackendContext } from '@vue-devtools/app-backend-api' +import type { ID, ScreenshotOverlayRenderContext } from '@vue/devtools-api' import { SharedData } from '@vue-devtools/shared-utils' import { JobQueue } from './util/queue' import { builtinLayers } from './timeline-builtins' @@ -17,7 +17,7 @@ interface Screenshot { events: ID[] } -export async function showScreenshot (screenshot: Screenshot, ctx: BackendContext) { +export async function showScreenshot(screenshot: Screenshot, ctx: BackendContext) { await jobQueue.queue('showScreenshot', async () => { if (screenshot) { if (!container) { @@ -53,11 +53,13 @@ export async function showScreenshot (screenshot: Screenshot, ctx: BackendContex if (result !== false) { if (typeof result === 'string') { container.innerHTML += result - } else { + } + else { container.appendChild(result) } } - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -66,13 +68,14 @@ export async function showScreenshot (screenshot: Screenshot, ctx: BackendContex } showElement() - } else { + } + else { hideElement() } }) } -function createElements () { +function createElements() { overlay = document.createElement('div') overlay.style.position = 'fixed' overlay.style.zIndex = '9999999999999' @@ -102,14 +105,14 @@ function createElements () { document.head.appendChild(style) } -function showElement () { +function showElement() { if (!overlay.parentNode) { document.body.appendChild(overlay) document.body.classList.add('__vuedevtools_no-scroll') } } -function hideElement () { +function hideElement() { if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay) @@ -119,7 +122,7 @@ function hideElement () { } } -function clearContent () { +function clearContent() { while (container.firstChild) { container.removeChild(container.lastChild) } diff --git a/packages/app-backend-core/src/timeline.ts b/packages/app-backend-core/src/timeline.ts index 1eff348c83..b67b58eebb 100644 --- a/packages/app-backend-core/src/timeline.ts +++ b/packages/app-backend-core/src/timeline.ts @@ -1,15 +1,16 @@ -import { BackendContext, AppRecord } from '@vue-devtools/app-backend-api' -import { BridgeEvents, HookEvents, stringify, SharedData, isBrowser } from '@vue-devtools/shared-utils' -import { App, ID, TimelineEventOptions, WithId, now, isPerformanceSupported } from '@vue/devtools-api' +import type { AppRecord, BackendContext } from '@vue-devtools/app-backend-api' +import { BridgeEvents, HookEvents, SharedData, isBrowser, stringify } from '@vue-devtools/shared-utils' +import type { App, ID, TimelineEventOptions, WithId } from '@vue/devtools-api' +import { isPerformanceSupported, now } from '@vue/devtools-api' import { hook } from './global-hook' import { getAppRecord, getAppRecordId } from './app' import { builtinLayers } from './timeline-builtins' -export function setupTimeline (ctx: BackendContext) { +export function setupTimeline(ctx: BackendContext) { setupBuiltinLayers(ctx) } -export function addBuiltinLayers (appRecord: AppRecord, ctx: BackendContext) { +export function addBuiltinLayers(appRecord: AppRecord, ctx: BackendContext) { for (const layerDef of builtinLayers) { ctx.timelineLayers.push({ ...layerDef, @@ -20,10 +21,9 @@ export function addBuiltinLayers (appRecord: AppRecord, ctx: BackendContext) { } } -function setupBuiltinLayers (ctx: BackendContext) { +function setupBuiltinLayers(ctx: BackendContext) { if (isBrowser) { - ['mousedown', 'mouseup', 'click', 'dblclick'].forEach(eventType => { - // @ts-ignore + (['mousedown', 'mouseup', 'click', 'dblclick'] as const).forEach((eventType) => { window.addEventListener(eventType, async (event: MouseEvent) => { await addTimelineEvent({ layerId: 'mouse', @@ -43,8 +43,7 @@ function setupBuiltinLayers (ctx: BackendContext) { }) }) - ;['keyup', 'keydown', 'keypress'].forEach(eventType => { - // @ts-ignore + ;(['keyup', 'keydown', 'keypress'] as const).forEach((eventType) => { window.addEventListener(eventType, async (event: KeyboardEvent) => { await addTimelineEvent({ layerId: 'keyboard', @@ -70,9 +69,14 @@ function setupBuiltinLayers (ctx: BackendContext) { hook.on(HookEvents.COMPONENT_EMIT, async (app, instance, event, params) => { try { - if (!SharedData.componentEventsEnabled) return + if (!SharedData.componentEventsEnabled) { + return + } const appRecord = await getAppRecord(app, ctx) + if (!appRecord) { + return + } const componentId = `${appRecord.id}:${instance.uid}` const componentDisplay = (await appRecord.backend.api.getComponentName(instance)) || 'Unknown Component' @@ -98,7 +102,8 @@ function setupBuiltinLayers (ctx: BackendContext) { }, }, }, app, ctx) - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -106,7 +111,7 @@ function setupBuiltinLayers (ctx: BackendContext) { }) } -export async function sendTimelineLayers (ctx: BackendContext) { +export async function sendTimelineLayers(ctx: BackendContext) { const layers = [] for (const layer of ctx.timelineLayers) { try { @@ -120,7 +125,8 @@ export async function sendTimelineLayers (ctx: BackendContext) { skipScreenshots: layer.skipScreenshots, ignoreNoDurationGroups: layer.ignoreNoDurationGroups, }) - } catch (e) { + } + catch (e) { if (SharedData.debugInfo) { console.error(e) } @@ -131,7 +137,10 @@ export async function sendTimelineLayers (ctx: BackendContext) { }) } -export async function addTimelineEvent (options: TimelineEventOptions, app: App, ctx: BackendContext) { +export async function addTimelineEvent(options: TimelineEventOptions, app: App, ctx: BackendContext) { + if (!SharedData.timelineRecording) { + return + } const appId = app ? getAppRecordId(app) : null const isAllApps = options.all || !app || appId == null @@ -153,7 +162,8 @@ export async function addTimelineEvent (options: TimelineEventOptions, app: App, const layer = ctx.timelineLayers.find(l => (isAllApps || l.appRecord?.options.app === app) && l.id === options.layerId) if (layer) { layer.events.push(eventData) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Timeline layer ${options.layerId} not found`) } } @@ -162,7 +172,7 @@ const initialTime = Date.now() export const dateThreshold = initialTime - 1_000_000 export const perfTimeDiff = initialTime - now() -function mapTimelineEvent (eventData: TimelineEventOptions & WithId) { +function mapTimelineEvent(eventData: TimelineEventOptions & WithId) { let time = eventData.event.time if (isPerformanceSupported() && time < dateThreshold) { time += perfTimeDiff @@ -177,7 +187,7 @@ function mapTimelineEvent (eventData: TimelineEventOptions & WithId) { } } -export async function clearTimeline (ctx: BackendContext) { +export async function clearTimeline(ctx: BackendContext) { ctx.timelineEventMap.clear() for (const layer of ctx.timelineLayers) { layer.events = [] @@ -187,13 +197,17 @@ export async function clearTimeline (ctx: BackendContext) { } } -export async function sendTimelineEventData (id: ID, ctx: BackendContext) { +export async function sendTimelineEventData(id: ID, ctx: BackendContext) { + if (!SharedData.timelineRecording) { + return + } let data = null const eventData = ctx.timelineEventMap.get(id) if (eventData) { data = await ctx.currentAppRecord.backend.api.inspectTimelineEvent(eventData, ctx.currentAppRecord.options.app) data = stringify(data) - } else if (SharedData.debugInfo) { + } + else if (SharedData.debugInfo) { console.warn(`Event ${id} not found`, ctx.timelineEventMap.keys()) } ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_EVENT_DATA, { @@ -202,22 +216,31 @@ export async function sendTimelineEventData (id: ID, ctx: BackendContext) { }) } -export function removeLayersForApp (app: App, ctx: BackendContext) { +export function removeLayersForApp(app: App, ctx: BackendContext) { const layers = ctx.timelineLayers.filter(l => l.appRecord?.options.app === app) for (const layer of layers) { const index = ctx.timelineLayers.indexOf(layer) - if (index !== -1) ctx.timelineLayers.splice(index, 1) + if (index !== -1) { + ctx.timelineLayers.splice(index, 1) + } for (const e of layer.events) { ctx.timelineEventMap.delete(e.id) } } } -export function sendTimelineLayerEvents (appId: string, layerId: string, ctx: BackendContext) { +export function sendTimelineLayerEvents(appId: string, layerId: string, ctx: BackendContext) { + if (!SharedData.timelineRecording) { + return + } const app = ctx.appRecords.find(ar => ar.id === appId)?.options.app - if (!app) return + if (!app) { + return + } const layer = ctx.timelineLayers.find(l => l.appRecord?.options.app === app && l.id === layerId) - if (!layer) return + if (!layer) { + return + } ctx.bridge.send(BridgeEvents.TO_FRONT_TIMELINE_LAYER_LOAD_EVENTS, { appId, layerId, diff --git a/packages/app-backend-core/src/toast.ts b/packages/app-backend-core/src/toast.ts index ff91ab404f..ff28119686 100644 --- a/packages/app-backend-core/src/toast.ts +++ b/packages/app-backend-core/src/toast.ts @@ -1,3 +1,3 @@ -export function installToast () { +export function installToast() { // @TODO } diff --git a/packages/app-backend-core/src/util/queue.ts b/packages/app-backend-core/src/util/queue.ts index 087a03b083..99932bbb4e 100644 --- a/packages/app-backend-core/src/util/queue.ts +++ b/packages/app-backend-core/src/util/queue.ts @@ -7,13 +7,13 @@ export class JobQueue { jobs: Job[] = [] currentJob: Job - queue (id: string, fn: Job['fn']) { + queue(id: string, fn: Job['fn']) { const job: Job = { id, fn, } - return new Promise(resolve => { + return new Promise((resolve) => { const onDone = () => { this.currentJob = null const nextJob = this.jobs.shift() @@ -25,7 +25,7 @@ export class JobQueue { const run = () => { this.currentJob = job - return job.fn().then(onDone).catch(e => { + return job.fn().then(onDone).catch((e) => { console.error(`Job ${job.id} failed:`) console.error(e) }) @@ -36,7 +36,8 @@ export class JobQueue { id: job.id, fn: () => run(), }) - } else { + } + else { run() } }) diff --git a/packages/app-backend-core/src/util/subscriptions.ts b/packages/app-backend-core/src/util/subscriptions.ts index 1f6f4b4cf7..d57bdbce6f 100644 --- a/packages/app-backend-core/src/util/subscriptions.ts +++ b/packages/app-backend-core/src/util/subscriptions.ts @@ -1,47 +1,26 @@ -interface Subscription { - payload: any - rawPayload: string -} - -const activeSubs: Map = new Map() +const activeSubs: Map> = new Map() -function getSubs (type: string) { +function getSubs(type: string) { let subs = activeSubs.get(type) if (!subs) { - subs = [] + subs = new Map() activeSubs.set(type, subs) } return subs } -export function subscribe (type: string, payload: any) { - const rawPayload = getRawPayload(payload) - getSubs(type).push({ - payload, - rawPayload, - }) +export function subscribe(type: string, key: string) { + getSubs(type).set(key, true) } -export function unsubscribe (type: string, payload: any) { - const rawPayload = getRawPayload(payload) +export function unsubscribe(type: string, key: string) { const subs = getSubs(type) - let index: number - while ((index = subs.findIndex(sub => sub.rawPayload === rawPayload)) !== -1) { - subs.splice(index, 1) - } -} - -function getRawPayload (payload: any) { - const data = Object.keys(payload).sort().reduce((acc, key) => { - acc[key] = payload[key] - return acc - }, {}) - return JSON.stringify(data) + subs.delete(key) } -export function isSubscribed ( +export function isSubscribed( type: string, - predicate: (sub: Subscription) => boolean = () => true, + key: string, ) { - return getSubs(type).some(predicate) + return getSubs(type).has(key) } diff --git a/packages/app-backend-core/tsconfig.json b/packages/app-backend-core/tsconfig.json index f565bc6603..becb763b20 100644 --- a/packages/app-backend-core/tsconfig.json +++ b/packages/app-backend-core/tsconfig.json @@ -3,25 +3,25 @@ "target": "ES2019", "module": "commonjs", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true, "types": [ "node", "webpack-env" ], - "sourceMap": true, - "preserveWatchOutput": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "alwaysStrict": true, // Strict "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true }, "include": [ - "src/**/*", + "src/**/*" ], "exclude": [ "node_modules" diff --git a/packages/app-backend-vue1/package.json b/packages/app-backend-vue1/package.json index e74abf9492..76cf42fe95 100644 --- a/packages/app-backend-vue1/package.json +++ b/packages/app-backend-vue1/package.json @@ -14,8 +14,8 @@ "@vue-devtools/shared-utils": "^0.0.0" }, "devDependencies": { - "@types/node": "^13.9.1", + "@types/node": "^20.11.16", "@types/webpack-env": "^1.15.1", - "typescript": "^4.5.2" + "typescript": "^5.3.3" } } diff --git a/packages/app-backend-vue1/src/index.ts b/packages/app-backend-vue1/src/index.ts index a4c7264ac5..8798cc0724 100644 --- a/packages/app-backend-vue1/src/index.ts +++ b/packages/app-backend-vue1/src/index.ts @@ -3,7 +3,7 @@ import { defineBackend } from '@vue-devtools/app-backend-api' export const backend = defineBackend({ frameworkVersion: 1, features: [], - setup (api) { + setup(_api) { // @TODO }, }) diff --git a/packages/app-backend-vue1/tsconfig.json b/packages/app-backend-vue1/tsconfig.json index f565bc6603..becb763b20 100644 --- a/packages/app-backend-vue1/tsconfig.json +++ b/packages/app-backend-vue1/tsconfig.json @@ -3,25 +3,25 @@ "target": "ES2019", "module": "commonjs", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true, "types": [ "node", "webpack-env" ], - "sourceMap": true, - "preserveWatchOutput": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "alwaysStrict": true, // Strict "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true }, "include": [ - "src/**/*", + "src/**/*" ], "exclude": [ "node_modules" diff --git a/packages/app-backend-vue2/package.json b/packages/app-backend-vue2/package.json index cacbebf25a..1a95fec7d7 100644 --- a/packages/app-backend-vue2/package.json +++ b/packages/app-backend-vue2/package.json @@ -17,9 +17,11 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@types/node": "^13.9.1", + "@types/node": "^20.11.16", "@types/webpack-env": "^1.15.1", "core-js": "^3.20.2", - "typescript": "^4.5.2" + "typescript": "^5.3.3", + "vue": "^2.7.10", + "vue-loader": "^15.7.1" } } diff --git a/packages/app-backend-vue2/src/components/data.ts b/packages/app-backend-vue2/src/components/data.ts index dcb0c5d0a0..227e55dd99 100644 --- a/packages/app-backend-vue2/src/components/data.ts +++ b/packages/app-backend-vue2/src/components/data.ts @@ -1,16 +1,19 @@ -import { camelize, getComponentName, getCustomRefDetails, StateEditor, SharedData } from '@vue-devtools/shared-utils' -import { ComponentState, CustomState, HookPayloads, Hooks, InspectedComponentData } from '@vue/devtools-api' -import { functionalVnodeMap, instanceMap } from './tree' +import type { StateEditor } from '@vue-devtools/shared-utils' +import { SharedData, camelize, getComponentName, getCustomRefDetails } from '@vue-devtools/shared-utils' +import type { ComponentState, CustomState, HookPayloads, Hooks, InspectedComponentData } from '@vue/devtools-api' +import { getFunctionalVnodeMap, getInstanceMap } from './tree' import 'core-js/modules/es.object.entries' /** * Get the detailed information of an inspected instance. */ -export function getInstanceDetails (instance): InspectedComponentData { +export function getInstanceDetails(instance): InspectedComponentData { if (instance.__VUE_DEVTOOLS_FUNCTIONAL_LEGACY__) { const vnode = findInstanceOrVnode(instance.__VUE_DEVTOOLS_UID__) - if (!vnode) return null + if (!vnode) { + return null + } const fakeInstance = { $options: vnode.fnOptions, @@ -42,15 +45,14 @@ export function getInstanceDetails (instance): InspectedComponentData { file: null, } - let i - if ((i = instance.$vnode) && (i = i.componentOptions) && (i = i.Ctor) && (i = i.options)) { - data.file = i.__file || null + if (instance.$vnode?.componentOptions?.Ctor?.options) { + data.file = instance.$vnode.componentOptions.Ctor.options.__file || null } return data } -function getInstanceState (instance): ComponentState[] { +function getInstanceState(instance): ComponentState[] { return processProps(instance).concat( processState(instance), processSetupState(instance), @@ -65,11 +67,11 @@ function getInstanceState (instance): ComponentState[] { ) } -function getFunctionalInstanceState (instance): ComponentState[] { +function getFunctionalInstanceState(instance): ComponentState[] { return processProps(instance) } -export function getCustomInstanceDetails (instance) { +export function getCustomInstanceDetails(instance) { const state = getInstanceState(instance) return { _custom: { @@ -85,7 +87,7 @@ export function getCustomInstanceDetails (instance) { } } -export function reduceStateList (list) { +export function reduceStateList(list) { if (!list.length) { return undefined } @@ -100,9 +102,11 @@ export function reduceStateList (list) { /** * Get the appropriate display name for an instance. */ -export function getInstanceName (instance): string { +export function getInstanceName(instance): string { const name = getComponentName(instance.$options || instance.fnOptions || {}) - if (name) return name + if (name) { + return name + } return instance.$root === instance ? 'Root' : 'Anonymous Component' @@ -113,7 +117,7 @@ export function getInstanceName (instance): string { * Make sure return a plain object because window.postMessage() * will throw an Error if the passed object contains Functions. */ -function processProps (instance): ComponentState[] { +function processProps(instance): ComponentState[] { const props = instance.$options.props const propsData = [] for (let key in props) { @@ -137,7 +141,7 @@ function processProps (instance): ComponentState[] { return propsData } -function processAttrs (instance): ComponentState[] { +function processAttrs(instance): ComponentState[] { return Object.entries(instance.$attrs || {}).map(([key, value]) => { return { type: '$attrs', @@ -152,7 +156,7 @@ const fnTypeRE = /^(?:function|class) (\w+)/ /** * Convert prop type constructor to string. */ -function getPropType (type) { +function getPropType(type) { if (Array.isArray(type)) { return type.map(t => getPropType(t)).join(' or ') } @@ -170,15 +174,15 @@ function getPropType (type) { * with a JSON dance. This removes functions which can cause * errors during structured clone used by window.postMessage. */ -function processState (instance): ComponentState[] { +function processState(instance): ComponentState[] { const props = instance.$options.props - const getters = - instance.$options.vuex && - instance.$options.vuex.getters + const getters + = instance.$options.vuex + && instance.$options.vuex.getters return Object.keys(instance._data) .filter(key => ( - !(props && key in props) && - !(getters && key in getters) + !(props && key in props) + && !(getters && key in getters) )) .map(key => ({ key, @@ -188,7 +192,7 @@ function processState (instance): ComponentState[] { })) } -function processSetupState (instance) { +function processSetupState(instance) { const state = instance._setupProxy || instance const raw = instance._setupState if (!raw) { @@ -197,7 +201,7 @@ function processSetupState (instance) { return Object.keys(raw) .filter(key => !key.startsWith('__')) - .map(key => { + .map((key) => { const value = returnError(() => toRaw(state[key])) const rawData = raw[key] @@ -219,7 +223,8 @@ function processSetupState (instance) { editable: isState && !info.readonly, type: isOther ? 'setup (other)' : 'setup', } - } else { + } + else { result = { type: 'setup', } @@ -233,38 +238,39 @@ function processSetupState (instance) { }) } -function returnError (cb: () => any) { +function returnError(cb: () => any) { try { return cb() - } catch (e) { + } + catch (e) { return e } } -function isRef (raw: any): boolean { +function isRef(raw: any): boolean { return !!raw.__v_isRef } -function isComputed (raw: any): boolean { +function isComputed(raw: any): boolean { return isRef(raw) && !!raw.effect } -function isReactive (raw: any): boolean { +function isReactive(raw: any): boolean { return !!raw.__ob__ } -function isReadOnly (raw: any): boolean { +function isReadOnly(raw: any): boolean { return !!raw.__v_isReadonly } -function toRaw (value: any) { +function toRaw(value: any) { if (value?.__v_raw) { return value.__v_raw } return value } -function getSetupStateInfo (raw: any) { +function getSetupStateInfo(raw: any) { return { ref: isRef(raw), computed: isComputed(raw), @@ -273,7 +279,7 @@ function getSetupStateInfo (raw: any) { } } -export function getCustomObjectDetails (object: any, proto: string): CustomState | undefined { +export function getCustomObjectDetails(object: any, _proto: string): CustomState | undefined { const info = getSetupStateInfo(object) const isState = info.ref || info.computed || info.reactive @@ -295,7 +301,7 @@ export function getCustomObjectDetails (object: any, proto: string): CustomState /** * Process refs */ -function processRefs (instance): ComponentState[] { +function processRefs(instance): ComponentState[] { return Object.keys(instance.$refs) .filter(key => instance.$refs[key]) .map(key => getCustomRefDetails(instance, key, instance.$refs[key])) @@ -304,7 +310,7 @@ function processRefs (instance): ComponentState[] { /** * Process the computed properties of an instance. */ -function processComputed (instance): ComponentState[] { +function processComputed(instance): ComponentState[] { const computed = [] const defs = instance.$options.computed || {} // use for...in here because if 'computed' is not defined @@ -325,7 +331,8 @@ function processComputed (instance): ComponentState[] { key, value: instance[key], } - } catch (e) { + } + catch (e) { computedProp = { type, key, @@ -342,18 +349,19 @@ function processComputed (instance): ComponentState[] { /** * Process Vuex getters. */ -function processInjected (instance): ComponentState[] { +function processInjected(instance): ComponentState[] { const injected = instance.$options.inject if (injected) { - return Object.keys(injected).map(key => { + return Object.keys(injected).map((key) => { return { key, type: 'injected', value: instance[key], } }) - } else { + } + else { return [] } } @@ -361,16 +369,24 @@ function processInjected (instance): ComponentState[] { /** * Process possible vue-router $route context */ -function processRouteContext (instance): ComponentState[] { +function processRouteContext(instance): ComponentState[] { try { const route = instance.$route if (route) { const { path, query, params } = route const value: any = { path, query, params } - if (route.fullPath) value.fullPath = route.fullPath - if (route.hash) value.hash = route.hash - if (route.name) value.name = route.name - if (route.meta) value.meta = route.meta + if (route.fullPath) { + value.fullPath = route.fullPath + } + if (route.hash) { + value.hash = route.hash + } + if (route.name) { + value.name = route.name + } + if (route.meta) { + value.meta = route.meta + } return [{ key: '$route', type: 'route', @@ -383,7 +399,8 @@ function processRouteContext (instance): ComponentState[] { }, }] } - } catch (e) { + } + catch (e) { // Invalid $router } return [] @@ -392,19 +409,20 @@ function processRouteContext (instance): ComponentState[] { /** * Process Vuex getters. */ -function processVuexGetters (instance): ComponentState[] { - const getters = - instance.$options.vuex && - instance.$options.vuex.getters +function processVuexGetters(instance): ComponentState[] { + const getters + = instance.$options.vuex + && instance.$options.vuex.getters if (getters) { - return Object.keys(getters).map(key => { + return Object.keys(getters).map((key) => { return { type: 'vuex getters', key, value: instance[key], } }) - } else { + } + else { return [] } } @@ -412,17 +430,18 @@ function processVuexGetters (instance): ComponentState[] { /** * Process Firebase bindings. */ -function processFirebaseBindings (instance): ComponentState[] { +function processFirebaseBindings(instance): ComponentState[] { const refs = instance.$firebaseRefs if (refs) { - return Object.keys(refs).map(key => { + return Object.keys(refs).map((key) => { return { type: 'firebase bindings', key, value: instance[key], } }) - } else { + } + else { return [] } } @@ -430,31 +449,32 @@ function processFirebaseBindings (instance): ComponentState[] { /** * Process vue-rx observable bindings. */ -function processObservables (instance): ComponentState[] { +function processObservables(instance): ComponentState[] { const obs = instance.$observables if (obs) { - return Object.keys(obs).map(key => { + return Object.keys(obs).map((key) => { return { type: 'observables', key, value: instance[key], } }) - } else { + } + else { return [] } } -export function findInstanceOrVnode (id) { +export function findInstanceOrVnode(id) { if (/:functional:/.test(id)) { const [refId] = id.split(':functional:') - const map = functionalVnodeMap.get(refId) + const map = getFunctionalVnodeMap()?.get(refId) return map && map[id] } - return instanceMap.get(id) + return getInstanceMap()?.get(id) } -export function editState ( +export function editState( { componentInstance, path, @@ -463,7 +483,9 @@ export function editState ( }: HookPayloads[Hooks.EDIT_COMPONENT_STATE], stateEditor: StateEditor, ) { - if (!['data', 'props', 'computed', 'setup'].includes(type)) return + if (!['data', 'props', 'computed', 'setup'].includes(type)) { + return + } let target: any const targetPath: string[] = path.slice() @@ -471,9 +493,10 @@ export function editState ( if (stateEditor.has(componentInstance._props, path, !!state.newKey)) { // props target = componentInstance._props - } else if ( - componentInstance._setupState && - Object.keys(componentInstance._setupState).includes(path[0]) + } + else if ( + componentInstance._setupState + && Object.keys(componentInstance._setupState).includes(path[0]) ) { // setup target = componentInstance._setupProxy @@ -481,9 +504,12 @@ export function editState ( const currentValue = stateEditor.get(target, path) if (currentValue != null) { const info = getSetupStateInfo(currentValue) - if (info.readonly) return + if (info.readonly) { + return + } } - } else { + } + else { target = componentInstance._data } diff --git a/packages/app-backend-vue2/src/components/el.ts b/packages/app-backend-vue2/src/components/el.ts index b349d8ebbd..bfd084ae60 100644 --- a/packages/app-backend-vue2/src/components/el.ts +++ b/packages/app-backend-vue2/src/components/el.ts @@ -1,18 +1,18 @@ import { inDoc, isBrowser, target } from '@vue-devtools/shared-utils' -function createRect () { +function createRect() { const rect = { top: 0, bottom: 0, left: 0, right: 0, - get width () { return rect.right - rect.left }, - get height () { return rect.bottom - rect.top }, + get width() { return rect.right - rect.left }, + get height() { return rect.bottom - rect.top }, } return rect } -function mergeRects (a, b) { +function mergeRects(a, b) { if (!a.top || b.top < a.top) { a.top = b.top } @@ -31,7 +31,7 @@ function mergeRects (a, b) { /** * Get the client rect for an instance. */ -export function getInstanceOrVnodeRect (instance) { +export function getInstanceOrVnodeRect(instance) { const el = instance.$el || instance.elm if (!isBrowser) { @@ -45,7 +45,8 @@ export function getInstanceOrVnodeRect (instance) { if (instance._isFragment) { return addIframePosition(getLegacyFragmentRect(instance), getElWindow(instance.$root.$el)) - } else if (el.nodeType === 1) { + } + else if (el.nodeType === 1) { return addIframePosition(el.getBoundingClientRect(), getElWindow(el)) } } @@ -54,13 +55,14 @@ export function getInstanceOrVnodeRect (instance) { * Highlight a fragment instance. * Loop over its node range and determine its bounding box. */ -function getLegacyFragmentRect ({ _fragmentStart, _fragmentEnd }) { +function getLegacyFragmentRect({ _fragmentStart, _fragmentEnd }) { const rect = createRect() - util().mapNodeRange(_fragmentStart, _fragmentEnd, function (node) { + util().mapNodeRange(_fragmentStart, _fragmentEnd, (node) => { let childRect if (node.nodeType === 1 || node.getBoundingClientRect) { childRect = node.getBoundingClientRect() - } else if (node.nodeType === 3 && node.data.trim()) { + } + else if (node.nodeType === 3 && node.data.trim()) { childRect = getTextRect(node) } if (childRect) { @@ -74,9 +76,13 @@ let range: Range /** * Get the bounding rect for a text node using a Range. */ -function getTextRect (node: Text) { - if (!isBrowser) return - if (!range) range = document.createRange() +function getTextRect(node: Text) { + if (!isBrowser) { + return + } + if (!range) { + range = document.createRange() + } range.selectNode(node) @@ -86,22 +92,22 @@ function getTextRect (node: Text) { /** * Get Vue's util */ -function util () { +function util() { return target.__VUE_DEVTOOLS_GLOBAL_HOOK__.Vue.util } -export function findRelatedComponent (el) { +export function findRelatedComponent(el) { while (!el.__vue__ && el.parentElement) { el = el.parentElement } return el.__vue__ } -function getElWindow (el: HTMLElement) { +function getElWindow(el: HTMLElement) { return el.ownerDocument.defaultView } -function addIframePosition (bounds, win: any) { +function addIframePosition(bounds, win: any) { if (win.__VUE_DEVTOOLS_IFRAME__) { const rect = mergeRects(createRect(), bounds) const iframeBounds = win.__VUE_DEVTOOLS_IFRAME__.getBoundingClientRect() @@ -117,11 +123,11 @@ function addIframePosition (bounds, win: any) { return bounds } -export function getRootElementsFromComponentInstance (instance) { +export function getRootElementsFromComponentInstance(instance) { if (instance._isFragment) { const list = [] const { _fragmentStart, _fragmentEnd } = instance - util().mapNodeRange(_fragmentStart, _fragmentEnd, node => { + util().mapNodeRange(_fragmentStart, _fragmentEnd, (node) => { list.push(node) }) return list diff --git a/packages/app-backend-vue2/src/components/perf.ts b/packages/app-backend-vue2/src/components/perf.ts index 73a6daee28..517cc19443 100644 --- a/packages/app-backend-vue2/src/components/perf.ts +++ b/packages/app-backend-vue2/src/components/perf.ts @@ -1,6 +1,6 @@ -import { DevtoolsApi } from '@vue-devtools/app-backend-api' +import type { DevtoolsApi } from '@vue-devtools/app-backend-api' import { HookEvents, SharedData } from '@vue-devtools/shared-utils' -import { instanceMap } from './tree' +import { getInstanceMap } from './tree' const COMPONENT_HOOKS = { beforeCreate: { start: 'create' }, @@ -13,20 +13,22 @@ const COMPONENT_HOOKS = { destroyed: { end: 'destroy' }, } -export function initPerf (api: DevtoolsApi, app, Vue) { +export function initPerf(api: DevtoolsApi, app, Vue) { // Global mixin Vue.mixin({ - beforeCreate () { + beforeCreate() { applyPerfHooks(api, this, app) }, }) // Apply to existing components - instanceMap?.forEach(vm => applyPerfHooks(api, vm, app)) + getInstanceMap()?.forEach(vm => applyPerfHooks(api, vm, app)) } -export function applyPerfHooks (api: DevtoolsApi, vm, app) { - if (vm.$options.$_devtoolsPerfHooks) return +export function applyPerfHooks(api: DevtoolsApi, vm, app) { + if (vm.$options.$_devtoolsPerfHooks) { + return + } vm.$options.$_devtoolsPerfHooks = true for (const hook in COMPONENT_HOOKS) { @@ -46,9 +48,11 @@ export function applyPerfHooks (api: DevtoolsApi, vm, app) { const currentValue = vm.$options[hook] if (Array.isArray(currentValue)) { vm.$options[hook] = [handler, ...currentValue] - } else if (typeof currentValue === 'function') { + } + else if (typeof currentValue === 'function') { vm.$options[hook] = [handler, currentValue] - } else { + } + else { vm.$options[hook] = [handler] } } diff --git a/packages/app-backend-vue2/src/components/tree.ts b/packages/app-backend-vue2/src/components/tree.ts index 435c1879ef..e73efa30af 100644 --- a/packages/app-backend-vue2/src/components/tree.ts +++ b/packages/app-backend-vue2/src/components/tree.ts @@ -1,13 +1,21 @@ -import { AppRecord, BackendContext, DevtoolsApi } from '@vue-devtools/app-backend-api' +import type { AppRecord, BackendContext, DevtoolsApi } from '@vue-devtools/app-backend-api' import { classify, kebabize } from '@vue-devtools/shared-utils' -import { ComponentTreeNode, ComponentInstance } from '@vue/devtools-api' +import type { ComponentInstance, ComponentTreeNode } from '@vue/devtools-api' import { getRootElementsFromComponentInstance } from './el' import { applyPerfHooks } from './perf.js' import { applyTrackingUpdateHook } from './update-tracking.js' import { getInstanceName, getRenderKey, getUniqueId, isBeingDestroyed } from './util' -export let instanceMap: Map -export let functionalVnodeMap: Map +let instanceMap: Map = new Map() +let functionalVnodeMap: Map = new Map() + +export function getInstanceMap() { + return instanceMap +} + +export function getFunctionalVnodeMap() { + return functionalVnodeMap +} let appRecord: AppRecord let api: DevtoolsApi @@ -15,32 +23,37 @@ let api: DevtoolsApi const consoleBoundInstances = Array(5) let filter = '' +let recursively = false const functionalIds = new Map() // Dedupe instances // Some instances may be both on a component and on a child abstract/functional component const captureIds = new Map() -export async function walkTree (instance, pFilter: string, api: DevtoolsApi, ctx: BackendContext): Promise { +export async function walkTree(instance, pFilter: string, pRecursively: boolean, api: DevtoolsApi, ctx: BackendContext): Promise { initCtx(api, ctx) filter = pFilter + recursively = pRecursively functionalIds.clear() captureIds.clear() const result: ComponentTreeNode[] = flatten(await findQualifiedChildren(instance)) return result } -export function getComponentParents (instance, api: DevtoolsApi, ctx: BackendContext) { +export function getComponentParents(instance, api: DevtoolsApi, ctx: BackendContext) { initCtx(api, ctx) const captureIds = new Map() - const captureId = vm => { + const captureId = (vm) => { const id = vm.__VUE_DEVTOOLS_UID__ = getUniqueId(vm) - if (captureIds.has(id)) return + if (captureIds.has(id)) { + return + } captureIds.set(id, undefined) if (vm.__VUE_DEVTOOLS_FUNCTIONAL_LEGACY__) { markFunctional(id, vm.vnode) - } else { + } + else { mark(vm) } } @@ -48,6 +61,7 @@ export function getComponentParents (instance, api: DevtoolsApi, ctx: BackendCon const parents = [] captureId(instance) let parent = instance + // eslint-disable-next-line no-cond-assign while ((parent = parent.$parent)) { captureId(parent) parents.push(parent) @@ -55,7 +69,7 @@ export function getComponentParents (instance, api: DevtoolsApi, ctx: BackendCon return parents } -function initCtx (_api: DevtoolsApi, ctx: BackendContext) { +function initCtx(_api: DevtoolsApi, ctx: BackendContext) { appRecord = ctx.currentAppRecord api = _api if (!appRecord.meta) { @@ -77,7 +91,7 @@ function initCtx (_api: DevtoolsApi, ctx: BackendContext) { * traversal - e.g. if an instance is not matched, we will * recursively go deeper until a qualified child is found. */ -function findQualifiedChildrenFromList (instances: any[]): Promise { +function findQualifiedChildrenFromList(instances: any[]): Promise { instances = instances .filter(child => !isBeingDestroyed(child)) return Promise.all(!filter @@ -90,10 +104,11 @@ function findQualifiedChildrenFromList (instances: any[]): Promise { +async function findQualifiedChildren(instance): Promise { if (isQualified(instance)) { return [await capture(instance)] - } else { + } + else { let children = await findQualifiedChildrenFromList(instance.$children) // Find functional components in recursively in non-functional vnodes. @@ -111,7 +126,7 @@ async function findQualifiedChildren (instance): Promise { /** * Get children from a component instance. */ -function getInternalInstanceChildren (instance): any[] { +function getInternalInstanceChildren(instance): any[] { if (instance.$children) { return instance.$children } @@ -121,25 +136,27 @@ function getInternalInstanceChildren (instance): any[] { /** * Check if an instance is qualified. */ -function isQualified (instance): boolean { +function isQualified(instance): boolean { const name = getInstanceName(instance) - return classify(name).toLowerCase().indexOf(filter) > -1 || - kebabize(name).toLowerCase().indexOf(filter) > -1 + return classify(name).toLowerCase().includes(filter) + || kebabize(name).toLowerCase().includes(filter) } -function flatten (items: any[]): T[] { +function flatten(items: any[]): T[] { const r = items.reduce((acc, item) => { if (Array.isArray(item)) { let children = [] for (const i of item) { if (Array.isArray(i)) { children = children.concat(flatten(i)) - } else { + } + else { children.push(i) } } acc.push(...children) - } else if (item) { + } + else if (item) { acc.push(item) } @@ -148,12 +165,16 @@ function flatten (items: any[]): T[] { return r } -function captureChild (child): Promise { +function captureChild(child): Promise { if (child.fnContext && !child.componentInstance) { return capture(child) - } else if (child.componentInstance) { - if (!isBeingDestroyed(child.componentInstance)) return capture(child.componentInstance) - } else if (child.children) { + } + else if (child.componentInstance) { + if (!isBeingDestroyed(child.componentInstance)) { + return capture(child.componentInstance) + } + } + else if (child.children) { return Promise.all(flatten>(child.children.map(captureChild))) } } @@ -161,7 +182,7 @@ function captureChild (child): Promise /** * Capture the meta information of an instance. (recursive) */ -async function capture (instance, index?: number, list?: any[]): Promise { +async function capture(instance, _index?: number, _list?: any[]): Promise { if (instance.__VUE_DEVTOOLS_FUNCTIONAL_LEGACY__) { instance = instance.vnode } @@ -170,7 +191,9 @@ async function capture (instance, index?: number, list?: any[]): Promise -1 ? '$vm' + consoleId : null + ret.consoleId = consoleId > -1 ? `$vm${consoleId}` : null // check router view const isRouterView2 = instance.$vnode?.data?.routerView @@ -293,15 +321,15 @@ async function capture (instance, index?: number, list?: any[]): Promise { instanceMap.delete(refId) }) applyPerfHooks(api, instance, appRecord.options.app) @@ -331,11 +359,11 @@ function mark (instance) { } } -function markFunctional (id, vnode) { +function markFunctional(id, vnode) { const refId = vnode.fnContext.__VUE_DEVTOOLS_UID__ if (!functionalVnodeMap.has(refId)) { functionalVnodeMap.set(refId, {}) - vnode.fnContext.$on('hook:beforeDestroy', function () { + vnode.fnContext.$on('hook:beforeDestroy', () => { functionalVnodeMap.delete(refId) }) } diff --git a/packages/app-backend-vue2/src/components/update-tracking.ts b/packages/app-backend-vue2/src/components/update-tracking.ts index 15549d33ca..98264ca979 100644 --- a/packages/app-backend-vue2/src/components/update-tracking.ts +++ b/packages/app-backend-vue2/src/components/update-tracking.ts @@ -1,12 +1,12 @@ -import { DevtoolsApi } from '@vue-devtools/app-backend-api' +import type { DevtoolsApi } from '@vue-devtools/app-backend-api' import { HookEvents, SharedData } from '@vue-devtools/shared-utils' import throttle from 'lodash/throttle' import { getUniqueId } from './util.js' -export function initUpdateTracking (api: DevtoolsApi, Vue) { +export function initUpdateTracking(api: DevtoolsApi, Vue) { // Global mixin Vue.mixin({ - beforeCreate () { + beforeCreate() { applyTrackingUpdateHook(api, this) }, }) @@ -17,8 +17,10 @@ const COMPONENT_HOOKS = [ 'updated', ] -export function applyTrackingUpdateHook (api: DevtoolsApi, vm) { - if (vm.$options.$_devtoolsUpdateTrackingHooks) return +export function applyTrackingUpdateHook(api: DevtoolsApi, vm) { + if (vm.$options.$_devtoolsUpdateTrackingHooks) { + return + } vm.$options.$_devtoolsUpdateTrackingHooks = true const handler = throttle(async function (this: any) { @@ -39,9 +41,11 @@ export function applyTrackingUpdateHook (api: DevtoolsApi, vm) { const currentValue = vm.$options[hook] if (Array.isArray(currentValue)) { vm.$options[hook] = [handler, ...currentValue] - } else if (typeof currentValue === 'function') { + } + else if (typeof currentValue === 'function') { vm.$options[hook] = [handler, currentValue] - } else { + } + else { vm.$options[hook] = [handler] } } diff --git a/packages/app-backend-vue2/src/components/util.ts b/packages/app-backend-vue2/src/components/util.ts index 61eaea9d2a..5d3cd6e9cf 100644 --- a/packages/app-backend-vue2/src/components/util.ts +++ b/packages/app-backend-vue2/src/components/util.ts @@ -1,31 +1,38 @@ import { getComponentName } from '@vue-devtools/shared-utils' -import { AppRecord } from '@vue-devtools/app-backend-api' +import type { AppRecord } from '@vue-devtools/app-backend-api' -export function isBeingDestroyed (instance) { +export function isBeingDestroyed(instance) { return instance._isBeingDestroyed } /** * Get the appropriate display name for an instance. */ -export function getInstanceName (instance) { +export function getInstanceName(instance) { const name = getComponentName(instance.$options || instance.fnOptions || {}) - if (name) return name + if (name) { + return name + } return instance.$root === instance ? 'Root' : 'Anonymous Component' } -export function getRenderKey (value): string { - if (value == null) return +export function getRenderKey(value): string { + if (value == null) { + return + } const type = typeof value if (type === 'number') { return value.toString() - } else if (type === 'string') { + } + else if (type === 'string') { return `'${value}'` - } else if (Array.isArray(value)) { + } + else if (Array.isArray(value)) { return 'Array' - } else { + } + else { return 'Object' } } @@ -33,8 +40,10 @@ export function getRenderKey (value): string { /** * Returns a devtools unique id for instance. */ -export function getUniqueId (instance, appRecord?: AppRecord): string { - if (instance.__VUE_DEVTOOLS_UID__ != null) return instance.__VUE_DEVTOOLS_UID__ +export function getUniqueId(instance, appRecord?: AppRecord): string { + if (instance.__VUE_DEVTOOLS_UID__ != null) { + return instance.__VUE_DEVTOOLS_UID__ + } let rootVueId = instance.$root.__VUE_DEVTOOLS_APP_RECORD_ID__ if (!rootVueId && appRecord) { rootVueId = appRecord.id diff --git a/packages/app-backend-vue2/src/events.ts b/packages/app-backend-vue2/src/events.ts index aa6764a75d..41d1c7d574 100644 --- a/packages/app-backend-vue2/src/events.ts +++ b/packages/app-backend-vue2/src/events.ts @@ -1,9 +1,9 @@ -import { BackendContext } from '@vue-devtools/app-backend-api' +import type { BackendContext } from '@vue-devtools/app-backend-api' import { HookEvents } from '@vue-devtools/shared-utils' const internalRE = /^(?:pre-)?hook:/ -function wrap (app, Vue, method, ctx: BackendContext) { +function wrap(app, Vue, method, ctx: BackendContext) { const original = Vue.prototype[method] if (original) { Vue.prototype[method] = function (...args) { @@ -13,7 +13,7 @@ function wrap (app, Vue, method, ctx: BackendContext) { } } - function logEvent (vm, type, eventName, payload) { + function logEvent(vm, type, eventName, payload) { // The string check is important for compat with 1.x where the first // argument may be an object instead of a string. // this also ensures the event is only logged for direct $emit (source) @@ -25,8 +25,8 @@ function wrap (app, Vue, method, ctx: BackendContext) { } } -export function wrapVueForEvents (app, Vue, ctx: BackendContext) { - ['$emit', '$broadcast', '$dispatch'].forEach(method => { +export function wrapVueForEvents(app, Vue, ctx: BackendContext) { + ['$emit', '$broadcast', '$dispatch'].forEach((method) => { wrap(app, Vue, method, ctx) }) } diff --git a/packages/app-backend-vue2/src/index.ts b/packages/app-backend-vue2/src/index.ts index 599b3d8e9c..32aba118e1 100644 --- a/packages/app-backend-vue2/src/index.ts +++ b/packages/app-backend-vue2/src/index.ts @@ -1,10 +1,10 @@ -import { defineBackend, BuiltinBackendFeature } from '@vue-devtools/app-backend-api' +import { BuiltinBackendFeature, defineBackend } from '@vue-devtools/app-backend-api' import { backendInjections, getComponentName } from '@vue-devtools/shared-utils' -import { ComponentInstance } from '@vue/devtools-api' +import type { ComponentInstance } from '@vue/devtools-api' import { editState, getCustomInstanceDetails, getInstanceDetails } from './components/data' -import { getInstanceOrVnodeRect, findRelatedComponent, getRootElementsFromComponentInstance } from './components/el' +import { findRelatedComponent, getInstanceOrVnodeRect, getRootElementsFromComponentInstance } from './components/el' import { initPerf } from './components/perf.js' -import { getComponentParents, instanceMap, walkTree } from './components/tree' +import { getComponentParents, getInstanceMap, walkTree } from './components/tree' import { initUpdateTracking } from './components/update-tracking.js' import { getInstanceName } from './components/util' import { wrapVueForEvents } from './events' @@ -15,58 +15,59 @@ export const backend = defineBackend({ features: [ BuiltinBackendFeature.FLUSH, ], - setup (api) { - api.on.getAppRecordName(payload => { + setup(api) { + api.on.getAppRecordName((payload) => { if (payload.app.name) { payload.name = payload.app.name - } else if (payload.app.$options.name) { + } + else if (payload.app.$options.name) { payload.name = payload.app.$options.name } }) - api.on.getAppRootInstance(payload => { + api.on.getAppRootInstance((payload) => { payload.root = payload.app as unknown as ComponentInstance }) api.on.walkComponentTree(async (payload, ctx) => { - payload.componentTreeData = await walkTree(payload.componentInstance, payload.filter, api, ctx) + payload.componentTreeData = await walkTree(payload.componentInstance, payload.filter, payload.recursively, api, ctx) }) api.on.walkComponentParents((payload, ctx) => { payload.parentInstances = getComponentParents(payload.componentInstance, api, ctx) }) - api.on.inspectComponent(payload => { + api.on.inspectComponent((payload) => { injectToUtils() payload.instanceData = getInstanceDetails(payload.componentInstance) }) - api.on.getComponentBounds(payload => { + api.on.getComponentBounds((payload) => { payload.bounds = getInstanceOrVnodeRect(payload.componentInstance) }) - api.on.getComponentName(payload => { + api.on.getComponentName((payload) => { const instance = payload.componentInstance payload.name = instance.fnContext ? getComponentName(instance.fnOptions) : getInstanceName(instance) }) - api.on.getElementComponent(payload => { + api.on.getElementComponent((payload) => { payload.componentInstance = findRelatedComponent(payload.element) }) - api.on.editComponentState(payload => { + api.on.editComponentState((payload) => { editState(payload, api.stateEditor) }) - api.on.getComponentRootElements(payload => { + api.on.getComponentRootElements((payload) => { payload.rootElements = getRootElementsFromComponentInstance(payload.componentInstance) }) - api.on.getComponentDevtoolsOptions(payload => { + api.on.getComponentDevtoolsOptions((payload) => { payload.options = payload.componentInstance.$options.devtools }) - api.on.getComponentRenderCode(payload => { + api.on.getComponentRenderCode((payload) => { payload.code = payload.componentInstance.$options.render.toString() }) @@ -75,15 +76,19 @@ export const backend = defineBackend({ }) }, - setupApp (api, appRecord) { + setupApp(api, appRecord) { const { Vue } = appRecord.options.meta const app = appRecord.options.app // State editor overrides - api.stateEditor.createDefaultSetCallback = state => { + api.stateEditor.createDefaultSetCallback = (state) => { return (obj, field, value) => { - if (state.remove || state.newKey) Vue.delete(obj, field) - if (!state.remove) Vue.set(obj, state.newKey || field, value) + if (state.remove || state.newKey) { + Vue.delete(obj, field) + } + if (!state.remove) { + Vue.set(obj, state.newKey || field, value) + } } } @@ -102,9 +107,9 @@ export const backend = defineBackend({ }) // @TODO refactor -function injectToUtils () { +function injectToUtils() { backendInjections.getCustomInstanceDetails = getCustomInstanceDetails backendInjections.getCustomObjectDetails = () => undefined - backendInjections.instanceMap = instanceMap + backendInjections.instanceMap = getInstanceMap() backendInjections.isVueInstance = val => val._isVue } diff --git a/packages/app-backend-vue2/src/plugin.ts b/packages/app-backend-vue2/src/plugin.ts index 6601e33d22..40a4b24445 100644 --- a/packages/app-backend-vue2/src/plugin.ts +++ b/packages/app-backend-vue2/src/plugin.ts @@ -1,11 +1,26 @@ -import { DevtoolsApi } from '@vue-devtools/app-backend-api' -import { App, ComponentState, CustomInspectorNode, CustomInspectorState, setupDevtoolsPlugin } from '@vue/devtools-api' +import type { DevtoolsApi } from '@vue-devtools/app-backend-api' +import type { App, ComponentState, CustomInspectorNode, CustomInspectorState } from '@vue/devtools-api' +import { setupDevtoolsPlugin } from '@vue/devtools-api' import { isEmptyObject, target } from '@vue-devtools/shared-utils' import copy from 'clone-deep' let actionId = 0 -export function setupPlugin (api: DevtoolsApi, app: App, Vue) { +const VUEX_ROOT_PATH = '__vdt_root' +const VUEX_MODULE_PATH_SEPARATOR = '[vdt]' +const VUEX_MODULE_PATH_SEPARATOR_REG = /\[vdt\]/g + +/** + * Extracted from tailwind palette + */ +const BLUE_600 = 0x2563EB +const LIME_500 = 0x84CC16 +const CYAN_400 = 0x22D3EE +const ORANGE_400 = 0xFB923C +const WHITE = 0xFFFFFF +const DARK = 0x666666 + +export function setupPlugin(api: DevtoolsApi, app: App, Vue) { const ROUTER_INSPECTOR_ID = 'vue2-router-inspector' const ROUTER_CHANGES_LAYER_ID = 'vue2-router-changes' @@ -27,7 +42,7 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { defaultValue: false, }, }, - }, api => { + }, (api) => { const hook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__ // Vue Router @@ -43,17 +58,18 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { treeFilterPlaceholder: 'Search routes', }) - api.on.getInspectorTree(payload => { + api.on.getInspectorTree((payload) => { if (payload.inspectorId === ROUTER_INSPECTOR_ID) { if (router.options.routes) { payload.rootNodes = router.options.routes.map(route => formatRouteNode(router, route, '', payload.filter)).filter(Boolean) - } else { + } + else { console.warn(`[Vue Devtools] No routes found in router`, router.options) } } }) - api.on.getInspectorState(payload => { + api.on.getInspectorState((payload) => { if (payload.inspectorId === ROUTER_INSPECTOR_ID) { const route = router.matcher.getRoutes().find(r => getPathId(r) === payload.nodeId) if (route) { @@ -69,7 +85,7 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { api.addTimelineLayer({ id: ROUTER_CHANGES_LAYER_ID, label: 'Router Navigations', - color: 0x40a8c4, + color: 0x40A8C4, }) router.afterEach((to, from) => { @@ -105,7 +121,8 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { const nodes = [] flattenStoreForInspectorTree(nodes, store._modules.root, payload.filter, '') payload.rootNodes = nodes - } else { + } + else { payload.rootNodes = [ formatStoreForInspectorTree(store._modules.root, 'Root', ''), ] @@ -116,14 +133,17 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { api.on.getInspectorState((payload) => { if (payload.inspectorId === VUEX_INSPECTOR_ID) { const modulePath = payload.nodeId - const module = getStoreModule(store._modules, modulePath) + const { module, getterPath } = getStoreModule(store._modules, modulePath) + if (!module) { + return + } // Access the getters prop to init getters cache (which is lazy) // eslint-disable-next-line no-unused-expressions module.context.getters payload.state = formatStoreForInspectorState( module, store._makeLocalGettersCache, - modulePath, + getterPath, ) } }) @@ -176,7 +196,7 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { }) }) - function legacySingleActionSub (action, state) { + function legacySingleActionSub(action, state) { const data: any = {} if (action.payload) { data.payload = action.payload @@ -247,7 +267,7 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { }, { prepend: true }) // Inspect getters on mutations - api.on.inspectTimelineEvent(payload => { + api.on.inspectTimelineEvent((payload) => { if (payload.layerId === VUEX_MUTATIONS_ID) { const getterKeys = Object.keys(store.getters) if (getterKeys.length) { @@ -272,17 +292,7 @@ export function setupPlugin (api: DevtoolsApi, app: App, Vue) { }) } -/** - * Extracted from tailwind palette - */ -const BLUE_600 = 0x2563eb -const LIME_500 = 0x84cc16 -const CYAN_400 = 0x22d3ee -const ORANGE_400 = 0xfb923c -const WHITE = 0xffffff -const DARK = 0x666666 - -function formatRouteNode (router, route, parentPath: string, filter: string): CustomInspectorNode { +function formatRouteNode(router, route, parentPath: string, filter: string): CustomInspectorNode { const node: CustomInspectorNode = { id: route.path.startsWith('/') ? route.path : `${parentPath}/${route.path}`, label: route.path, @@ -290,7 +300,9 @@ function formatRouteNode (router, route, parentPath: string, filter: string): Cu tags: [], } - if (filter && !node.id.includes(filter) && !node.children?.length) return null + if (filter && !node.id.includes(filter) && !node.children?.length) { + return null + } if (route.name != null) { node.tags.push({ @@ -319,8 +331,8 @@ function formatRouteNode (router, route, parentPath: string, filter: string): Cu if (route.redirect) { node.tags.push({ label: - 'redirect: ' + - (typeof route.redirect === 'string' ? route.redirect : 'Object'), + `redirect: ${ + typeof route.redirect === 'string' ? route.redirect : 'Object'}`, textColor: WHITE, backgroundColor: DARK, }) @@ -329,7 +341,7 @@ function formatRouteNode (router, route, parentPath: string, filter: string): Cu return node } -function formatRouteData (route) { +function formatRouteData(route) { const data: Omit[] = [] data.push({ key: 'path', value: route.path }) @@ -369,7 +381,7 @@ function formatRouteData (route) { return data } -function getPathId (routeMatcher) { +function getPathId(routeMatcher) { let path = routeMatcher.path if (routeMatcher.parent) { path = getPathId(routeMatcher.parent) + path @@ -383,11 +395,7 @@ const TAG_NAMESPACED = { backgroundColor: DARK, } -const VUEX_ROOT_PATH = '__vdt_root' -const VUEX_MODULE_PATH_SEPARATOR = '[vdt]' -const VUEX_MODULE_PATH_SEPARATOR_REG = /\[vdt\]/g - -function formatStoreForInspectorTree (module, moduleName: string, path: string): CustomInspectorNode { +function formatStoreForInspectorTree(module, moduleName: string, path: string): CustomInspectorNode { return { id: path || VUEX_ROOT_PATH, // all modules end with a `/`, we want the last segment only @@ -395,7 +403,7 @@ function formatStoreForInspectorTree (module, moduleName: string, path: string): // nested/cart/ -> cart label: moduleName, tags: module.namespaced ? [TAG_NAMESPACED] : [], - children: Object.keys(module._children ?? {}).map((key) => + children: Object.keys(module._children ?? {}).map(key => formatStoreForInspectorTree( module._children[key], key, @@ -405,7 +413,7 @@ function formatStoreForInspectorTree (module, moduleName: string, path: string): } } -function flattenStoreForInspectorTree (result: CustomInspectorNode[], module, filter: string, path: string) { +function flattenStoreForInspectorTree(result: CustomInspectorNode[], module, filter: string, path: string) { if (path.includes(filter)) { result.push({ id: path || VUEX_ROOT_PATH, @@ -413,18 +421,18 @@ function flattenStoreForInspectorTree (result: CustomInspectorNode[], module, fi tags: module.namespaced ? [TAG_NAMESPACED] : [], }) } - Object.keys(module._children).forEach(moduleName => { + Object.keys(module._children).forEach((moduleName) => { flattenStoreForInspectorTree(result, module._children[moduleName], filter, path + moduleName + VUEX_MODULE_PATH_SEPARATOR) }) } -function extractNameFromPath (path: string) { +function extractNameFromPath(path: string) { return path && path !== VUEX_ROOT_PATH ? path.split(VUEX_MODULE_PATH_SEPARATOR).slice(-2, -1)[0] : 'Root' } -function formatStoreForInspectorState (module, getters, path): CustomInspectorState { +function formatStoreForInspectorState(module, getters, path): CustomInspectorState { const storeState: CustomInspectorState = { - state: Object.keys(module.context.state ?? {}).map((key) => ({ + state: Object.keys(module.context.state ?? {}).map(key => ({ key, editable: true, value: module.context.state[key], @@ -449,11 +457,12 @@ function formatStoreForInspectorState (module, getters, path): CustomInspectorSt for (const key of gettersKeys) { moduleGetters[key] = canThrow(() => getters[key]) } - } else { + } + else { moduleGetters = getters } const tree = transformPathsToObjectTree(moduleGetters) - storeState.getters = Object.keys(tree).map((key) => ({ + storeState.getters = Object.keys(tree).map(key => ({ key: key.endsWith('/') ? extractNameFromPath(key) : key, editable: false, value: canThrow(() => tree[key]), @@ -464,9 +473,9 @@ function formatStoreForInspectorState (module, getters, path): CustomInspectorSt return storeState } -function transformPathsToObjectTree (getters) { +function transformPathsToObjectTree(getters) { const result = {} - Object.keys(getters).forEach(key => { + Object.keys(getters).forEach((key) => { const path = key.split('/') if (path.length > 1) { let target = result @@ -485,31 +494,41 @@ function transformPathsToObjectTree (getters) { target = target[p]._custom.value } target[leafKey] = canThrow(() => getters[key]) - } else { + } + else { result[key] = canThrow(() => getters[key]) } }) return result } -function getStoreModule (moduleMap, path) { - const names = path.split(VUEX_MODULE_PATH_SEPARATOR).filter((n) => n) +function getStoreModule(moduleMap, path) { + const names = path.split(VUEX_MODULE_PATH_SEPARATOR).filter(n => n) return names.reduce( - (module, moduleName, i) => { + ({ module, getterPath }, moduleName, i) => { const child = module[moduleName === VUEX_ROOT_PATH ? 'root' : moduleName] if (!child) { - throw new Error(`Missing module "${moduleName}" for path "${path}".`) + return null } - return i === names.length - 1 ? child : child._children + return { + module: i === names.length - 1 ? child : child._children, + getterPath: child._rawModule.namespaced + ? getterPath + : getterPath.replace(`${moduleName}${VUEX_MODULE_PATH_SEPARATOR}`, ''), + } + }, + { + module: path === VUEX_ROOT_PATH ? moduleMap : moduleMap.root._children, + getterPath: path, }, - path === VUEX_ROOT_PATH ? moduleMap : moduleMap.root._children, ) } -function canThrow (cb: () => any) { +function canThrow(cb: () => any) { try { return cb() - } catch (e) { + } + catch (e) { return e } } diff --git a/packages/app-backend-vue2/tsconfig.json b/packages/app-backend-vue2/tsconfig.json index f565bc6603..becb763b20 100644 --- a/packages/app-backend-vue2/tsconfig.json +++ b/packages/app-backend-vue2/tsconfig.json @@ -3,25 +3,25 @@ "target": "ES2019", "module": "commonjs", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true, "types": [ "node", "webpack-env" ], - "sourceMap": true, - "preserveWatchOutput": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "alwaysStrict": true, // Strict "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true }, "include": [ - "src/**/*", + "src/**/*" ], "exclude": [ "node_modules" diff --git a/packages/app-backend-vue3/package.json b/packages/app-backend-vue3/package.json index 279544d59a..10e2e8a037 100644 --- a/packages/app-backend-vue3/package.json +++ b/packages/app-backend-vue3/package.json @@ -10,13 +10,13 @@ "ts": "tsc -d -outDir lib" }, "dependencies": { - "@vue/devtools-api": "^6.0.0-beta.1", "@vue-devtools/app-backend-api": "^0.0.0", - "@vue-devtools/shared-utils": "^0.0.0" + "@vue-devtools/shared-utils": "^0.0.0", + "@vue/devtools-api": "^6.0.0-beta.1" }, "devDependencies": { - "@types/node": "^13.9.1", + "@types/node": "^20.11.16", "@types/webpack-env": "^1.15.1", - "typescript": "^4.5.2" + "typescript": "^5.3.3" } } diff --git a/packages/app-backend-vue3/src/components/data.ts b/packages/app-backend-vue3/src/components/data.ts index 0025977af1..73edb75466 100644 --- a/packages/app-backend-vue3/src/components/data.ts +++ b/packages/app-backend-vue3/src/components/data.ts @@ -1,13 +1,68 @@ -import { BackendContext } from '@vue-devtools/app-backend-api' -import { getInstanceName, getUniqueComponentId } from './util' -import { camelize, StateEditor, SharedData } from '@vue-devtools/shared-utils' -import { ComponentInstance, CustomState, HookPayloads, Hooks, InspectedComponentData } from '@vue/devtools-api' +import type { BackendContext } from '@vue-devtools/app-backend-api' +import type { StateEditor } from '@vue-devtools/shared-utils' +import { SharedData, camelize, kebabize } from '@vue-devtools/shared-utils' +import type { ComponentInstance, CustomState, HookPayloads, Hooks, InspectedComponentData } from '@vue/devtools-api' import { returnError } from '../util' +import { getInstanceName, getUniqueComponentId } from './util' + +const vueBuiltins = [ + 'nextTick', + 'defineComponent', + 'defineAsyncComponent', + 'defineCustomElement', + 'ref', + 'computed', + 'reactive', + 'readonly', + 'watchEffect', + 'watchPostEffect', + 'watchSyncEffect', + 'watch', + 'isRef', + 'unref', + 'toRef', + 'toRefs', + 'isProxy', + 'isReactive', + 'isReadonly', + 'shallowRef', + 'triggerRef', + 'customRef', + 'shallowReactive', + 'shallowReadonly', + 'toRaw', + 'markRaw', + 'effectScope', + 'getCurrentScope', + 'onScopeDispose', + 'onMounted', + 'onUpdated', + 'onUnmounted', + 'onBeforeMount', + 'onBeforeUpdate', + 'onBeforeUnmount', + 'onErrorCaptured', + 'onRenderTracked', + 'onRenderTriggered', + 'onActivated', + 'onDeactivated', + 'onServerPrefetch', + 'provide', + 'inject', + 'h', + 'mergeProps', + 'cloneVNode', + 'isVNode', + 'resolveComponent', + 'resolveDirective', + 'withDirectives', + 'withModifiers', +] /** * Get the detailed information of an inspected instance. */ -export function getInstanceDetails (instance: any, ctx: BackendContext): InspectedComponentData { +export function getInstanceDetails(instance: any, ctx: BackendContext): InspectedComponentData { return { id: getUniqueComponentId(instance, ctx), name: getInstanceName(instance), @@ -16,7 +71,7 @@ export function getInstanceDetails (instance: any, ctx: BackendContext): Inspect } } -function getInstanceState (instance) { +function getInstanceState(instance) { const mergedType = resolveMergedOptions(instance) return processProps(instance).concat( processState(instance), @@ -26,6 +81,7 @@ function getInstanceState (instance) { processProvide(instance), processInject(instance, mergedType), processRefs(instance), + processEventListeners(instance), ) } @@ -37,7 +93,7 @@ function getInstanceState (instance) { * @param {Vue} instance * @return {Array} */ -function processProps (instance) { +function processProps(instance) { const propsData = [] const propDefinitions = instance.type.props @@ -71,7 +127,7 @@ const fnTypeRE = /^(?:function|class) (\w+)/ /** * Convert prop type constructor to string. */ -function getPropType (type) { +function getPropType(type) { if (Array.isArray(type)) { return type.map(t => getPropType(t)).join(' or ') } @@ -93,12 +149,12 @@ function getPropType (type) { * @return {Array} */ -function processState (instance) { +function processState(instance) { const type = instance.type const props = type.props - const getters = - type.vuex && - type.vuex.getters + const getters + = type.vuex + && type.vuex.getters const computedDefs = type.computed const data = { @@ -108,9 +164,9 @@ function processState (instance) { return Object.keys(data) .filter(key => ( - !(props && key in props) && - !(getters && key in getters) && - !(computedDefs && key in computedDefs) + !(props && key in props) + && !(getters && key in getters) + && !(computedDefs && key in computedDefs) )) .map(key => ({ key, @@ -120,68 +176,81 @@ function processState (instance) { })) } -function processSetupState (instance) { - const raw = instance.devtoolsRawSetupState || {} - return Object.keys(instance.setupState) - .map(key => { - const value = returnError(() => toRaw(instance.setupState[key])) +function processSetupState(instance) { + const raw = instance.devtoolsRawSetupState + const combinedSetupState = (Object.keys(instance.setupState).length + ? instance.setupState + : instance.exposed + ) || {} + + return Object.keys(combinedSetupState) + .filter(key => !vueBuiltins.includes(key) && key.split(/(?=[A-Z])/)[0] !== 'use') + .map((key) => { + const value = returnError(() => toRaw(combinedSetupState[key])) const rawData = raw[key] let result: any + let isOther = typeof value === 'function' + || typeof value?.render === 'function' // Components + || typeof value?.__asyncLoader === 'function' // Components + || (typeof value === 'object' && value && ('setup' in value || 'props' in value)) // Components + || /^v[A-Z]/.test(key) // Directives + if (rawData) { const info = getSetupStateInfo(rawData) const objectType = info.computed ? 'Computed' : info.ref ? 'Ref' : info.reactive ? 'Reactive' : null const isState = info.ref || info.computed || info.reactive - const isOther = typeof value === 'function' || typeof value?.render === 'function' const raw = rawData.effect?.raw?.toString() || rawData.effect?.fn?.toString() + if (objectType) { + isOther = false + } + result = { ...objectType ? { objectType } : {}, ...raw ? { raw } : {}, editable: isState && !info.readonly, - type: isOther ? 'setup (other)' : 'setup', - } - } else { - result = { - type: 'setup', } } + const type = isOther ? 'setup (other)' : 'setup' + return { key, value, + type, ...result, } }) } -function isRef (raw: any): boolean { +function isRef(raw: any): boolean { return !!raw.__v_isRef } -function isComputed (raw: any): boolean { +function isComputed(raw: any): boolean { return isRef(raw) && !!raw.effect } -function isReactive (raw: any): boolean { +function isReactive(raw: any): boolean { return !!raw.__v_isReactive } -function isReadOnly (raw: any): boolean { +function isReadOnly(raw: any): boolean { return !!raw.__v_isReadonly } -function toRaw (value: any) { +function toRaw(value: any) { if (value?.__v_raw) { return value.__v_raw } return value } -function getSetupStateInfo (raw: any) { +function getSetupStateInfo(raw: any) { return { ref: isRef(raw), computed: isComputed(raw), @@ -190,7 +259,7 @@ function getSetupStateInfo (raw: any) { } } -export function getCustomObjectDetails (object: any, proto: string): CustomState | undefined { +export function getCustomObjectDetails(object: any, _proto: string): CustomState | undefined { const info = getSetupStateInfo(object) const isState = info.ref || info.computed || info.reactive @@ -207,6 +276,15 @@ export function getCustomObjectDetails (object: any, proto: string): CustomState }, } } + + if (typeof object.__asyncLoader === 'function') { + return { + _custom: { + type: 'component-definition', + display: 'Async component definition', + }, + } + } } /** @@ -215,7 +293,7 @@ export function getCustomObjectDetails (object: any, proto: string): CustomState * @param {Vue} instance * @return {Array} */ -function processComputed (instance, mergedType) { +function processComputed(instance, mergedType) { const type = mergedType const computed = [] const defs = type.computed || {} @@ -239,7 +317,7 @@ function processComputed (instance, mergedType) { return computed } -function processAttrs (instance) { +function processAttrs(instance) { return Object.keys(instance.attrs) .map(key => ({ type: 'attrs', @@ -248,7 +326,7 @@ function processAttrs (instance) { })) } -function processProvide (instance) { +function processProvide(instance) { return Reflect.ownKeys(instance.provides) .map(key => ({ type: 'provided', @@ -257,8 +335,10 @@ function processProvide (instance) { })) } -function processInject (instance, mergedType) { - if (!mergedType?.inject) return [] +function processInject(instance, mergedType) { + if (!mergedType?.inject) { + return [] + } let keys = [] let defaultValue if (Array.isArray(mergedType.inject)) { @@ -266,13 +346,15 @@ function processInject (instance, mergedType) { key, originalKey: key, })) - } else { - keys = Reflect.ownKeys(mergedType.inject).map(key => { + } + else { + keys = Reflect.ownKeys(mergedType.inject).map((key) => { const value = mergedType.inject[key] let originalKey if (typeof value === 'string' || typeof value === 'symbol') { originalKey = value - } else { + } + else { originalKey = value.from defaultValue = value.default } @@ -285,11 +367,11 @@ function processInject (instance, mergedType) { return keys.map(({ key, originalKey }) => ({ type: 'injected', key: originalKey && key !== originalKey ? `${originalKey.toString()} ➞ ${key.toString()}` : key.toString(), - value: returnError(() => instance.ctx[key] || instance.provides[originalKey] || defaultValue), + value: returnError(() => Object.prototype.hasOwnProperty.call(instance.ctx, key) ? instance.ctx[key] : Object.prototype.hasOwnProperty.call(instance.provides, originalKey) ? instance.provides[originalKey] : defaultValue), })) } -function processRefs (instance) { +function processRefs(instance) { return Object.keys(instance.refs) .map(key => ({ type: 'refs', @@ -298,24 +380,59 @@ function processRefs (instance) { })) } -export function editState ({ componentInstance, path, state, type }: HookPayloads[Hooks.EDIT_COMPONENT_STATE], stateEditor: StateEditor, ctx: BackendContext) { - if (!['data', 'props', 'computed', 'setup'].includes(type)) return +function processEventListeners(instance) { + const emitsDefinition = instance.type.emits + const declaredEmits = Array.isArray(emitsDefinition) ? emitsDefinition : Object.keys(emitsDefinition ?? {}) + const declaredEmitsMap = declaredEmits.reduce((emitsMap, key) => { + emitsMap[kebabize(key)] = key + return emitsMap + }, {}) + const keys = Object.keys(instance.vnode.props ?? {}) + const result = [] + for (const key of keys) { + const [prefix, ...eventNameParts] = key.split(/(?=[A-Z])/) + if (prefix === 'on') { + const eventName = eventNameParts.join('-').toLowerCase() + const normalizedEventName = declaredEmitsMap[eventName] + result.push({ + type: 'event listeners', + key: normalizedEventName || eventName, + value: { + _custom: { + display: normalizedEventName ? '✅ Declared' : '⚠️ Not declared', + tooltip: !normalizedEventName ? `The event ${eventName} is not declared in the emits option. It will leak into the component's attributes ($attrs).` : null, + }, + }, + }) + } + } + return result +} + +export function editState({ componentInstance, path, state, type }: HookPayloads[Hooks.EDIT_COMPONENT_STATE], stateEditor: StateEditor, _ctx: BackendContext) { + if (!['data', 'props', 'computed', 'setup'].includes(type)) { + return + } let target: any const targetPath: string[] = path.slice() if (Object.keys(componentInstance.props).includes(path[0])) { // Props target = componentInstance.props - } else if (componentInstance.devtoolsRawSetupState && Object.keys(componentInstance.devtoolsRawSetupState).includes(path[0])) { + } + else if (componentInstance.devtoolsRawSetupState && Object.keys(componentInstance.devtoolsRawSetupState).includes(path[0])) { // Setup target = componentInstance.devtoolsRawSetupState const currentValue = stateEditor.get(componentInstance.devtoolsRawSetupState, path) if (currentValue != null) { const info = getSetupStateInfo(currentValue) - if (info.readonly) return + if (info.readonly) { + return + } } - } else { + } + else { target = componentInstance.proxy } @@ -324,7 +441,7 @@ export function editState ({ componentInstance, path, state, type }: HookPayload } } -function reduceStateList (list) { +function reduceStateList(list) { if (!list.length) { return undefined } @@ -336,8 +453,10 @@ function reduceStateList (list) { }, {}) } -export function getCustomInstanceDetails (instance) { - if (instance._) instance = instance._ +export function getCustomInstanceDetails(instance) { + if (instance._) { + instance = instance._ + } const state = getInstanceState(instance) return { _custom: { @@ -353,20 +472,22 @@ export function getCustomInstanceDetails (instance) { } } -function resolveMergedOptions ( +function resolveMergedOptions( instance: ComponentInstance, ) { const raw = instance.type const { mixins, extends: extendsOptions } = raw const globalMixins = instance.appContext.mixins - if (!globalMixins.length && !mixins && !extendsOptions) return raw + if (!globalMixins.length && !mixins && !extendsOptions) { + return raw + } const options = {} globalMixins.forEach(m => mergeOptions(options, m, instance)) mergeOptions(options, raw, instance) return options } -function mergeOptions ( +function mergeOptions( to: any, from: any, instance: ComponentInstance, @@ -375,22 +496,25 @@ function mergeOptions ( from = from.options } - if (!from) return to + if (!from) { + return to + } const { mixins, extends: extendsOptions } = from extendsOptions && mergeOptions(to, extendsOptions, instance) - mixins && - mixins.forEach((m) => - mergeOptions(to, m, instance), - ) + mixins + && mixins.forEach(m => + mergeOptions(to, m, instance), + ) for (const key of ['computed', 'inject']) { if (Object.prototype.hasOwnProperty.call(from, key)) { if (!to[key]) { to[key] = from[key] - } else { - Object.assign(to[key], from[key]) + } + else { + to[key] = Object.assign(Object.create(null), to[key], from[key]) } } } diff --git a/packages/app-backend-vue3/src/components/el.ts b/packages/app-backend-vue3/src/components/el.ts index ef87958dc8..442a26e533 100644 --- a/packages/app-backend-vue3/src/components/el.ts +++ b/packages/app-backend-vue3/src/components/el.ts @@ -1,19 +1,24 @@ import { inDoc, isBrowser } from '@vue-devtools/shared-utils' import { isFragment } from './util' -export function getComponentInstanceFromElement (element) { +export function getComponentInstanceFromElement(element) { return element.__vueParentComponent } -export function getRootElementsFromComponentInstance (instance) { +export function getRootElementsFromComponentInstance(instance) { if (isFragment(instance)) { return getFragmentRootElements(instance.subTree) } + if (!instance.subTree) { + return [] + } return [instance.subTree.el] } -function getFragmentRootElements (vnode): any[] { - if (!vnode.children) return [] +function getFragmentRootElements(vnode): any[] { + if (!vnode.children) { + return [] + } const list = [] @@ -21,7 +26,8 @@ function getFragmentRootElements (vnode): any[] { const childVnode = vnode.children[i] if (childVnode.component) { list.push(...getRootElementsFromComponentInstance(childVnode.component)) - } else if (childVnode.el) { + } + else if (childVnode.el) { list.push(childVnode.el) } } @@ -33,9 +39,9 @@ function getFragmentRootElements (vnode): any[] { * Get the client rect for an instance. * * @param {Vue|Vnode} instance - * @return {Object} + * @return {object} */ -export function getInstanceOrVnodeRect (instance) { +export function getInstanceOrVnodeRect(instance) { const el = instance.subTree.el if (!isBrowser) { @@ -48,26 +54,28 @@ export function getInstanceOrVnodeRect (instance) { if (isFragment(instance)) { return addIframePosition(getFragmentRect(instance.subTree), getElWindow(el)) - } else if (el.nodeType === 1) { + } + else if (el.nodeType === 1) { return addIframePosition(el.getBoundingClientRect(), getElWindow(el)) - } else if (instance.subTree.component) { + } + else if (instance.subTree.component) { return getInstanceOrVnodeRect(instance.subTree.component) } } -function createRect () { +function createRect() { const rect = { top: 0, bottom: 0, left: 0, right: 0, - get width () { return rect.right - rect.left }, - get height () { return rect.bottom - rect.top }, + get width() { return rect.right - rect.left }, + get height() { return rect.bottom - rect.top }, } return rect } -function mergeRects (a, b) { +function mergeRects(a, b) { if (!a.top || b.top < a.top) { a.top = b.top } @@ -90,29 +98,37 @@ let range * @param {Text} node * @return {Rect} */ -function getTextRect (node) { - if (!isBrowser) return - if (!range) range = document.createRange() +function getTextRect(node) { + if (!isBrowser) { + return + } + if (!range) { + range = document.createRange() + } range.selectNode(node) return range.getBoundingClientRect() } -function getFragmentRect (vnode) { +function getFragmentRect(vnode) { const rect = createRect() - if (!vnode.children) return rect + if (!vnode.children) { + return rect + } for (let i = 0, l = vnode.children.length; i < l; i++) { const childVnode = vnode.children[i] let childRect if (childVnode.component) { childRect = getInstanceOrVnodeRect(childVnode.component) - } else if (childVnode.el) { + } + else if (childVnode.el) { const el = childVnode.el if (el.nodeType === 1 || el.getBoundingClientRect) { childRect = el.getBoundingClientRect() - } else if (el.nodeType === 3 && el.data.trim()) { + } + else if (el.nodeType === 3 && el.data.trim()) { childRect = getTextRect(el) } } @@ -124,11 +140,11 @@ function getFragmentRect (vnode) { return rect } -function getElWindow (el: HTMLElement) { +function getElWindow(el: HTMLElement) { return el.ownerDocument.defaultView } -function addIframePosition (bounds, win: any) { +function addIframePosition(bounds, win: any) { if (win.__VUE_DEVTOOLS_IFRAME__) { const rect = mergeRects(createRect(), bounds) const iframeBounds = win.__VUE_DEVTOOLS_IFRAME__.getBoundingClientRect() diff --git a/packages/app-backend-vue3/src/components/filter.ts b/packages/app-backend-vue3/src/components/filter.ts index 563fc595ea..b0fb5c118f 100644 --- a/packages/app-backend-vue3/src/components/filter.ts +++ b/packages/app-backend-vue3/src/components/filter.ts @@ -4,7 +4,7 @@ import { getInstanceName } from './util' export class ComponentFilter { filter: string - constructor (filter: string) { + constructor(filter: string) { this.filter = filter || '' } @@ -12,11 +12,11 @@ export class ComponentFilter { * Check if an instance is qualified. * * @param {Vue|Vnode} instance - * @return {Boolean} + * @return {boolean} */ - isQualified (instance) { + isQualified(instance) { const name = getInstanceName(instance) - return classify(name).toLowerCase().indexOf(this.filter) > -1 || - kebabize(name).toLowerCase().indexOf(this.filter) > -1 + return classify(name).toLowerCase().includes(this.filter) + || kebabize(name).toLowerCase().includes(this.filter) } } diff --git a/packages/app-backend-vue3/src/components/tree.ts b/packages/app-backend-vue3/src/components/tree.ts index a60e114ce1..283bd010eb 100644 --- a/packages/app-backend-vue3/src/components/tree.ts +++ b/packages/app-backend-vue3/src/components/tree.ts @@ -1,35 +1,38 @@ -import { isBeingDestroyed, getUniqueComponentId, getInstanceName, getRenderKey, isFragment } from './util' +import type { BackendContext, DevtoolsApi } from '@vue-devtools/app-backend-api' +import type { ComponentTreeNode } from '@vue/devtools-api' +import { getInstanceName, getRenderKey, getUniqueComponentId, isBeingDestroyed, isFragment } from './util' import { ComponentFilter } from './filter' -import { BackendContext, DevtoolsApi } from '@vue-devtools/app-backend-api' -import { ComponentTreeNode } from '@vue/devtools-api' import { getRootElementsFromComponentInstance } from './el' export class ComponentWalker { ctx: BackendContext api: DevtoolsApi maxDepth: number + recursively: boolean componentFilter: ComponentFilter // Dedupe instances // Some instances may be both on a component and on a child abstract/functional component captureIds: Map - constructor (maxDepth: number, filter: string, api: DevtoolsApi, ctx: BackendContext) { + constructor(maxDepth: number, filter: string, recursively: boolean, api: DevtoolsApi, ctx: BackendContext) { this.ctx = ctx this.api = api this.maxDepth = maxDepth + this.recursively = recursively this.componentFilter = new ComponentFilter(filter) } - getComponentTree (instance: any): Promise { + getComponentTree(instance: any): Promise { this.captureIds = new Map() return this.findQualifiedChildren(instance, 0) } - getComponentParents (instance: any) { + getComponentParents(instance: any) { this.captureIds = new Map() const parents = [] this.captureId(instance) let parent = instance + // eslint-disable-next-line no-cond-assign while ((parent = parent.parent)) { this.captureId(parent) parents.push(parent) @@ -45,16 +48,18 @@ export class ComponentWalker { * @param {Vue|Vnode} instance * @return {Vue|Array} */ - private async findQualifiedChildren (instance: any, depth: number): Promise { + private async findQualifiedChildren(instance: any, depth: number): Promise { if (this.componentFilter.isQualified(instance) && !instance.type.devtools?.hide) { return [await this.capture(instance, null, depth)] - } else if (instance.subTree) { + } + else if (instance.subTree) { // TODO functional components const list = this.isKeepAlive(instance) ? this.getKeepAliveCachedInstances(instance) : this.getInternalInstanceChildren(instance.subTree) return this.findQualifiedChildrenFromList(list, depth) - } else { + } + else { return [] } } @@ -68,12 +73,13 @@ export class ComponentWalker { * @param {Array} instances * @return {Array} */ - private async findQualifiedChildrenFromList (instances, depth: number): Promise { + private async findQualifiedChildrenFromList(instances, depth: number): Promise { instances = instances .filter(child => !isBeingDestroyed(child) && !child.type.devtools?.hide) if (!this.componentFilter.filter) { return Promise.all(instances.map((child, index, list) => this.capture(child, list, depth))) - } else { + } + else { return Array.prototype.concat.apply([], await Promise.all(instances.map(i => this.findQualifiedChildren(i, depth)))) } } @@ -81,19 +87,22 @@ export class ComponentWalker { /** * Get children from a component instance. */ - private getInternalInstanceChildren (subTree, suspense = null) { + private getInternalInstanceChildren(subTree, suspense = null) { const list = [] if (subTree) { if (subTree.component) { !suspense ? list.push(subTree.component) : list.push({ ...subTree.component, suspense }) - } else if (subTree.suspense) { + } + else if (subTree.suspense) { const suspenseKey = !subTree.suspense.isInFallback ? 'suspense default' : 'suspense fallback' list.push(...this.getInternalInstanceChildren(subTree.suspense.activeBranch, { ...subTree.suspense, suspenseKey })) - } else if (Array.isArray(subTree.children)) { - subTree.children.forEach(childSubTree => { + } + else if (Array.isArray(subTree.children)) { + subTree.children.forEach((childSubTree) => { if (childSubTree.component) { !suspense ? list.push(childSubTree.component) : list.push({ ...childSubTree.component, suspense }) - } else { + } + else { list.push(...this.getInternalInstanceChildren(childSubTree, suspense)) } }) @@ -102,8 +111,10 @@ export class ComponentWalker { return list.filter(child => !isBeingDestroyed(child) && !child.type.devtools?.hide) } - private captureId (instance): string { - if (!instance) return null + private captureId(instance): string { + if (!instance) { + return null + } // instance.uid is not reliable in devtools as there // may be 2 roots with same uid which causes unexpected @@ -114,7 +125,8 @@ export class ComponentWalker { // Dedupe if (this.captureIds.has(id)) { return - } else { + } + else { this.captureIds.set(id, undefined) } @@ -127,10 +139,12 @@ export class ComponentWalker { * Capture the meta information of an instance. (recursive) * * @param {Vue} instance - * @return {Object} + * @return {object} */ - private async capture (instance: any, list: any[], depth: number): Promise { - if (!instance) return null + private async capture(instance: any, list: any[], depth: number): Promise { + if (!instance) { + return null + } const id = this.captureId(instance) @@ -158,9 +172,10 @@ export class ComponentWalker { { label: 'functional', textColor: 0x555555, - backgroundColor: 0xeeeeee, + backgroundColor: 0xEEEEEE, }, ], + autoOpen: this.recursively, } // capture children @@ -197,15 +212,16 @@ export class ComponentWalker { el = el.parentElement } while (el.parentElement && parentRootElements.length && !parentRootElements.includes(el)) treeNode.domOrder = indexList.reverse() - } else { + } + else { treeNode.domOrder = [-1] } if (instance.suspense?.suspenseKey) { treeNode.tags.push({ label: instance.suspense.suspenseKey, - backgroundColor: 0xe492e4, - textColor: 0xffffff, + backgroundColor: 0xE492E4, + textColor: 0xFFFFFF, }) // update instanceMap this.mark(instance, true) @@ -219,18 +235,18 @@ export class ComponentWalker { * * @param {Vue} instance */ - private mark (instance, force = false) { + private mark(instance, force = false) { const instanceMap = this.ctx.currentAppRecord.instanceMap if (force || !instanceMap.has(instance.__VUE_DEVTOOLS_UID__)) { instanceMap.set(instance.__VUE_DEVTOOLS_UID__, instance) } } - private isKeepAlive (instance) { + private isKeepAlive(instance) { return instance.type.__isKeepAlive && instance.__v_cache } - private getKeepAliveCachedInstances (instance) { + private getKeepAliveCachedInstances(instance) { return Array.from(instance.__v_cache.values()).map((vnode: any) => vnode.component).filter(Boolean) } } diff --git a/packages/app-backend-vue3/src/components/util.ts b/packages/app-backend-vue3/src/components/util.ts index 162724a54c..4fb1b0d3c7 100644 --- a/packages/app-backend-vue3/src/components/util.ts +++ b/packages/app-backend-vue3/src/components/util.ts @@ -1,19 +1,18 @@ -import { classify } from '@vue-devtools/shared-utils' -import { basename } from '../util' -import { ComponentInstance, App } from '@vue/devtools-api' -import { BackendContext } from '@vue-devtools/app-backend-api' +import { basename, classify } from '@vue-devtools/shared-utils' +import type { App, ComponentInstance } from '@vue/devtools-api' +import type { BackendContext } from '@vue-devtools/app-backend-api' -export function isBeingDestroyed (instance) { +export function isBeingDestroyed(instance) { return instance._isBeingDestroyed || instance.isUnmounted } -export function getAppRecord (instance) { +export function getAppRecord(instance) { if (instance.root) { return instance.appContext.app.__VUE_DEVTOOLS_APP_RECORD__ } } -export function isFragment (instance) { +export function isFragment(instance) { const appRecord = getAppRecord(instance) if (appRecord) { return appRecord.options.types.Fragment === instance.subTree?.type @@ -24,31 +23,43 @@ export function isFragment (instance) { * Get the appropriate display name for an instance. * * @param {Vue} instance - * @return {String} + * @return {string} */ -export function getInstanceName (instance) { +export function getInstanceName(instance) { const name = getComponentTypeName(instance.type || {}) - if (name) return name - if (instance.root === instance) return 'Root' + if (name) { + return name + } + if (instance.root === instance) { + return 'Root' + } for (const key in instance.parent?.type?.components) { - if (instance.parent.type.components[key] === instance.type) return saveComponentName(instance, key) + if (instance.parent.type.components[key] === instance.type) { + return saveComponentName(instance, key) + } } for (const key in instance.appContext?.components) { - if (instance.appContext.components[key] === instance.type) return saveComponentName(instance, key) + if (instance.appContext.components[key] === instance.type) { + return saveComponentName(instance, key) + } + } + const fileName = getComponentFileName(instance.type || {}) + if (fileName) { + return fileName } return 'Anonymous Component' } -function saveComponentName (instance, key) { +function saveComponentName(instance, key) { instance.type.__vdevtools_guessedName = key return key } -function getComponentTypeName (options) { - const name = options.name || options._componentTag || options.__vdevtools_guessedName - if (name) { - return name - } +function getComponentTypeName(options) { + return options.name || options._componentTag || options.__vdevtools_guessedName || options.__name +} + +function getComponentFileName(options) { const file = options.__file // injected by vue-loader if (file) { return classify(basename(file, '.vue')) @@ -59,30 +70,35 @@ function getComponentTypeName (options) { * Returns a devtools unique id for instance. * @param {Vue} instance */ -export function getUniqueComponentId (instance, ctx: BackendContext) { +export function getUniqueComponentId(instance, _ctx: BackendContext) { const appId = instance.appContext.app.__VUE_DEVTOOLS_APP_RECORD_ID__ const instanceId = instance === instance.root ? 'root' : instance.uid return `${appId}:${instanceId}` } -export function getRenderKey (value): string { - if (value == null) return +export function getRenderKey(value): string { + if (value == null) { + return + } const type = typeof value if (type === 'number') { return value - } else if (type === 'string') { + } + else if (type === 'string') { return `'${value}'` - } else if (Array.isArray(value)) { + } + else if (Array.isArray(value)) { return 'Array' - } else { + } + else { return 'Object' } } -export function getComponentInstances (app: App): ComponentInstance[] { +export function getComponentInstances(app: App): ComponentInstance[] { const appRecord = app.__VUE_DEVTOOLS_APP_RECORD__ const appId = appRecord.id.toString() return [...appRecord.instanceMap] .filter(([key]) => key.split(':')[0] === appId) - .map(([,instance]) => instance) // eslint-disable-line comma-spacing + .map(([,instance]) => instance) } diff --git a/packages/app-backend-vue3/src/index.ts b/packages/app-backend-vue3/src/index.ts index 727cc0e1b5..b86cf5e220 100644 --- a/packages/app-backend-vue3/src/index.ts +++ b/packages/app-backend-vue3/src/index.ts @@ -1,37 +1,38 @@ import { defineBackend } from '@vue-devtools/app-backend-api' +import { HookEvents, backendInjections } from '@vue-devtools/shared-utils' import { ComponentWalker } from './components/tree' -import { editState, getInstanceDetails, getCustomInstanceDetails, getCustomObjectDetails } from './components/data' -import { getInstanceName, getComponentInstances } from './components/util' +import { editState, getCustomInstanceDetails, getCustomObjectDetails, getInstanceDetails } from './components/data' +import { getComponentInstances, getInstanceName } from './components/util' import { getComponentInstanceFromElement, getInstanceOrVnodeRect, getRootElementsFromComponentInstance } from './components/el' -import { backendInjections, HookEvents } from '@vue-devtools/shared-utils' export const backend = defineBackend({ frameworkVersion: 3, features: [], - setup (api) { - api.on.getAppRecordName(payload => { + setup(api) { + api.on.getAppRecordName((payload) => { if (payload.app._component) { payload.name = payload.app._component.name } }) - api.on.getAppRootInstance(payload => { + api.on.getAppRootInstance((payload) => { if (payload.app._instance) { payload.root = payload.app._instance - } else if (payload.app._container?._vnode?.component) { + } + else if (payload.app._container?._vnode?.component) { payload.root = payload.app._container?._vnode?.component } }) api.on.walkComponentTree(async (payload, ctx) => { - const walker = new ComponentWalker(payload.maxDepth, payload.filter, api, ctx) + const walker = new ComponentWalker(payload.maxDepth, payload.filter, payload.recursively, api, ctx) payload.componentTreeData = await walker.getComponentTree(payload.componentInstance) }) api.on.walkComponentParents((payload, ctx) => { - const walker = new ComponentWalker(0, null, api, ctx) + const walker = new ComponentWalker(0, null, false, api, ctx) payload.parentInstances = walker.getComponentParents(payload.componentInstance) }) @@ -44,23 +45,23 @@ export const backend = defineBackend({ payload.instanceData = getInstanceDetails(payload.componentInstance, ctx) }) - api.on.getComponentName(payload => { + api.on.getComponentName((payload) => { payload.name = getInstanceName(payload.componentInstance) }) - api.on.getComponentBounds(payload => { + api.on.getComponentBounds((payload) => { payload.bounds = getInstanceOrVnodeRect(payload.componentInstance) }) - api.on.getElementComponent(payload => { + api.on.getElementComponent((payload) => { payload.componentInstance = getComponentInstanceFromElement(payload.element) }) - api.on.getComponentInstances(payload => { + api.on.getComponentInstances((payload) => { payload.componentInstances = getComponentInstances(payload.app) }) - api.on.getComponentRootElements(payload => { + api.on.getComponentRootElements((payload) => { payload.rootElements = getRootElementsFromComponentInstance(payload.componentInstance) }) @@ -68,15 +69,15 @@ export const backend = defineBackend({ editState(payload, api.stateEditor, ctx) }) - api.on.getComponentDevtoolsOptions(payload => { + api.on.getComponentDevtoolsOptions((payload) => { payload.options = payload.componentInstance.type.devtools }) - api.on.getComponentRenderCode(payload => { + api.on.getComponentRenderCode((payload) => { payload.code = !(payload.componentInstance.type instanceof Function) ? payload.componentInstance.render.toString() : payload.componentInstance.type.toString() }) - api.on.transformCall(payload => { + api.on.transformCall((payload) => { if (payload.callName === HookEvents.COMPONENT_UPDATED) { const component = payload.inArgs[0] payload.outArgs = [ diff --git a/packages/app-backend-vue3/src/util.ts b/packages/app-backend-vue3/src/util.ts index 1e8efa9f3a..ebf171be03 100644 --- a/packages/app-backend-vue3/src/util.ts +++ b/packages/app-backend-vue3/src/util.ts @@ -1,27 +1,21 @@ -import path from 'path' - -export function flatten (items) { +export function flatten(items) { return items.reduce((acc, item) => { - if (item instanceof Array) acc.push(...flatten(item)) - else if (item) acc.push(item) + if (Array.isArray(item)) { + acc.push(...flatten(item)) + } + else if (item) { + acc.push(item) + } return acc }, []) } -// Use a custom basename functions instead of the shimed version -// because it doesn't work on Windows -export function basename (filename, ext) { - return path.basename( - filename.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/'), - ext, - ) -} - -export function returnError (cb: () => any) { +export function returnError(cb: () => any) { try { return cb() - } catch (e) { + } + catch (e) { return e } } diff --git a/packages/app-backend-vue3/tsconfig.json b/packages/app-backend-vue3/tsconfig.json index f565bc6603..becb763b20 100644 --- a/packages/app-backend-vue3/tsconfig.json +++ b/packages/app-backend-vue3/tsconfig.json @@ -3,25 +3,25 @@ "target": "ES2019", "module": "commonjs", "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true, "types": [ "node", "webpack-env" ], - "sourceMap": true, - "preserveWatchOutput": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "alwaysStrict": true, // Strict "noImplicitAny": false, "noImplicitThis": true, - "alwaysStrict": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "preserveWatchOutput": true }, "include": [ - "src/**/*", + "src/**/*" ], "exclude": [ "node_modules" diff --git a/packages/app-frontend/package.json b/packages/app-frontend/package.json index 07533a8118..971ab06823 100644 --- a/packages/app-frontend/package.json +++ b/packages/app-frontend/package.json @@ -6,23 +6,28 @@ "@pixi/events": "^6.2.0", "@pixi/unsafe-eval": "^6.2.0", "@vue-devtools/shared-utils": "^0.0.0", - "@vue/composition-api": "^1.4.6", "@vue/devtools-api": "^6.0.0-beta.9", "@vue/ui": "^0.12.5", + "@vueuse/core": "^10.7.2", "circular-json-es6": "^2.0.2", "d3": "^5.16.0", + "floating-vue": "^5.2.2", "lodash": "^4.17.15", "lru-cache": "^5.1.1", "monaco-editor": "^0.24.0", "pixi.js-legacy": "^6.2.0", - "portal-vue": "^2.1.7", "scroll-into-view-if-needed": "^2.2.28", "semver": "^7.3.5", "stylus": "^0.54.7", "stylus-loader": "^3.0.2", "tinycolor2": "^1.4.2", - "vue": "^2.6.11", - "vue-router": "^3.3.4", - "vue-virtual-scroller": "^1.0.10" + "vue": "^3.3.4", + "vue-resize": "^2.0.0-alpha.1", + "vue-router": "^4.2.5", + "vue-safe-teleport": "^0.1.2", + "vue-virtual-scroller": "^2.0.0-alpha.1" + }, + "devDependencies": { + "@akryum/md-icons-svg": "^1.0.1" } } diff --git a/packages/app-frontend/src/app.ts b/packages/app-frontend/src/app.ts index d62da4f866..15da8110bc 100644 --- a/packages/app-frontend/src/app.ts +++ b/packages/app-frontend/src/app.ts @@ -1,8 +1,8 @@ +import type { App as VueApp } from 'vue' +import { createApp as createVueApp } from 'vue' +import { BridgeEvents, SharedData, destroySharedData, initEnv, initSharedData, isChrome } from '@vue-devtools/shared-utils' import App from './features/App.vue' - -import Vue from 'vue' -import { isChrome, initEnv, SharedData, initSharedData, destroySharedData } from '@vue-devtools/shared-utils' -import { createRouter } from './router' +import { createRouterInstance } from './router' import { getBridge, setBridge } from './features/bridge' import { setAppConnected, setAppInitializing } from './features/connection' import { setupAppsBridgeEvents } from './features/apps' @@ -12,37 +12,25 @@ import { setupCustomInspectorBridgeEvents } from './features/inspector/custom/co import { setupPluginsBridgeEvents } from './features/plugin' import { setupPlugins } from './plugins' -setupPlugins() - // Capture and log devtool errors when running as actual extension // so that we can debug it by inspecting the background page. // We do want the errors to be thrown in the dev shell though. -if (isChrome) { - Vue.config.errorHandler = (e, vm) => { - getBridge()?.send('ERROR', { - message: e.message, - stack: e.stack, - component: vm.$options.name || (vm.$options as any)._componentTag || 'anonymous', - }) +export function createApp() { + const router = createRouterInstance() + + const app = createVueApp(App) + app.use(router) + setupPlugins(app) + + if (isChrome) { + app.config.errorHandler = (e, vm) => { + getBridge()?.send('ERROR', { + message: (e as Error).message, + stack: (e as Error).stack, + component: vm?.$options.name || (vm?.$options as any)._componentTag || 'anonymous', + }) + } } -} - -// @ts-ignore -Vue.options.renderError = (h, e) => { - return h('pre', { - class: 'text-white bg-red-500 p-2 rounded text-xs overflow-auto', - }, e.stack) -} - -export function createApp () { - const router = createRouter() - - const app = new Vue({ - router, - render: h => h(App as any), - }) - - // @TODO [Vue 3] Setup plugins return app } @@ -51,27 +39,31 @@ export function createApp () { * Connect then init the app. We need to reconnect on every reload, because a * new backend will be injected. */ -export function connectApp (app, shell) { - shell.connect(async bridge => { +export function connectApp(app: VueApp, shell) { + shell.connect(async (bridge) => { setBridge(bridge) // @TODO remove - // @ts-ignore + // @ts-expect-error custom prop on window window.bridge = bridge - if (Object.prototype.hasOwnProperty.call(Vue.prototype, '$shared')) { + if (app.config.globalProperties.$shared) { destroySharedData() - } else { - Object.defineProperty(Vue.prototype, '$shared', { + } + else { + Object.defineProperty(app.config.globalProperties, '$shared', { get: () => SharedData, }) } - initEnv(Vue) + initEnv(app) + + bridge.on(BridgeEvents.TO_FRONT_TITLE, ({ title }: { title: string }) => { + document.title = `${title} - Vue devtools` + }) await initSharedData({ bridge, persist: true, - Vue, }) if (SharedData.logDetected) { diff --git a/packages/app-frontend/src/assets/github-theme/dark.json b/packages/app-frontend/src/assets/github-theme/dark.json index c409cabde6..84bf8ad012 100644 --- a/packages/app-frontend/src/assets/github-theme/dark.json +++ b/packages/app-frontend/src/assets/github-theme/dark.json @@ -532,4 +532,4 @@ } ], "encodedTokensColors": [] -} \ No newline at end of file +} diff --git a/packages/app-frontend/src/assets/github-theme/light.json b/packages/app-frontend/src/assets/github-theme/light.json index 4a23a96b9a..42da7a39b0 100644 --- a/packages/app-frontend/src/assets/github-theme/light.json +++ b/packages/app-frontend/src/assets/github-theme/light.json @@ -528,4 +528,4 @@ } ], "encodedTokensColors": [] -} \ No newline at end of file +} diff --git a/packages/app-frontend/src/assets/style/index.postcss b/packages/app-frontend/src/assets/style/index.postcss new file mode 100644 index 0000000000..3d71cf5bb3 --- /dev/null +++ b/packages/app-frontend/src/assets/style/index.postcss @@ -0,0 +1,126 @@ +html, body, #app { + @apply dark:!bg-gray-800; +} + +.vue-ui-high-contrast { + #app { + @apply !bg-black; + } +} + +/* Poppers */ + +.v-popper__popper.v-popper--theme-tooltip code { + @apply bg-gray-500/50 rounded px-1 text-[11px] font-mono; +} + +.v-popper--theme-dropdown { + .vue-ui-dark-mode & { + .v-popper__inner, + .v-popper__arrow-outer { + @apply border-gray-900; + } + + .v-popper__inner { + @apply bg-gray-800; + } + + .v-popper__arrow-inner { + @apply border-gray-800; + } + } +} + +/* Scrollbars */ + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track-piece { + @apply bg-transparent; +} + +::-webkit-scrollbar-track:hover { + @apply bg-gray-600/5 dark:bg-gray-600/10; +} + +::-webkit-scrollbar-thumb { + @apply bg-gray-300 hover:bg-gray-600 border-[3px] border-transparent bg-clip-padding rounded dark:bg-gray-700 dark:hover:bg-gray-500; +} + +.vue-ui-dark-mode { + scrollbar-color: theme('colors.gray.800') theme('colors.black'); + + .selectable-item { + @apply bg-gray-800 hover:bg-gray-900; + + &.selected { + @apply hover:bg-green-600; + } + } +} + +/* Buttons */ + +.vue-ui-button:not(.flat):not(.vue-ui-dropdown-button):not(.primary):not(.secondary):not(.danger) { + @apply dark:bg-gray-700 dark:hover:!bg-gray-600; +} + +.vue-ui-button.flat, +.vue-ui-dropdown-button { + @apply hover:!bg-green-500/30; +} + +.vue-ui-dark-mode { + .vue-ui-group { + .vue-ui-button:not(.flat) { + @apply !bg-gray-700 hover:!bg-gray-600; + + &.selected { + @apply !bg-green-700; + } + } + } +} + +/* Switch */ + +.vue-ui-dark-mode { + .vue-ui-switch { + > .content > .wrapper { + @apply bg-gray-700; + } + &.selected { + > .content > .wrapper { + @apply bg-green-600; + } + } + } +} + +/* Tab */ + +.vue-ui-dark-mode { + .vue-ui-group { + .indicator .content { + @apply !border-b-green-500; + } + .vue-ui-button.selected { + @apply text-green-500; + } + } +} + +/* Arrows */ + +.arrow { + @apply inline-block w-0 h-0 transition-transform duration-150 ease-out text-gray-500; + + &.right { + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 6px solid currentColor; + } +} diff --git a/packages/app-frontend/src/assets/style/index.styl b/packages/app-frontend/src/assets/style/index.styl index 37c3b9b875..98183d731a 100644 --- a/packages/app-frontend/src/assets/style/index.styl +++ b/packages/app-frontend/src/assets/style/index.styl @@ -30,6 +30,10 @@ html, body body overflow hidden +#app + width: 100% + height: 100% + button:focus outline none @@ -41,9 +45,8 @@ button:focus &.active background-color $active-color color #fff + .item-name, .arrow - border-left-color #fff - .item-name color #fff .vue-ui-dark-mode & @@ -51,7 +54,7 @@ button:focus &:hover background-color $dark-hover-color .arrow - border-left-color #666 + color theme('colors.gray.600') &.selected, &.active color #fff @@ -60,55 +63,6 @@ button:focus .vue-ui-icon svg fill currentColor -.preferences - display flex - flex-wrap wrap - padding 12px 4px - - > * - margin 16px 24px - - .vue-ui-form-field - > .wrapper > .content - min-height 32px - justify-content center - -// Popover - -$arrow-color = $vue-ui-color-dark - -.arrow - display inline-block - width 0 - height 0 - &.up - border-left 4px solid transparent - border-right 4px solid transparent - border-bottom 6px solid $arrow-color - &.down - border-left 4px solid transparent - border-right 4px solid transparent - border-top 6px solid $arrow-color - &.right - border-top 4px solid transparent - border-bottom 4px solid transparent - border-left 6px solid $arrow-color - &.left - border-top 4px solid transparent - border-bottom 4px solid transparent - border-right 6px solid $arrow-color - - .vue-ui-dark-mode & - $arrow-color = rgba($vue-ui-color-light-neutral, .4) - &.up - border-bottom-color $arrow-color - &.down - border-top-color $arrow-color - &.right - border-left-color $arrow-color - &.left - border-right-color $arrow-color - // Tooltips .keyboard @@ -139,7 +93,6 @@ $arrow-color = $vue-ui-color-dark .vue-ui-dark-mode .v-popper__popper.v-popper--theme-tooltip .vue-ui-icon svg fill #666 - .v-popper__popper.v-popper--theme-dropdown .v-popper__inner max-height calc(100vh - 32px - 8px - 4px) overflow-y auto @@ -153,3 +106,9 @@ $arrow-color = $vue-ui-color-dark .right-icon-reveal:not(:hover) .vue-ui-icon.right opacity 0 + +.v-popper--theme-tooltip + .vue-ui-dark-mode & + .v-popper__arrow-inner, + .v-popper__arrow-outer + border-color $vue-ui-white diff --git a/packages/app-frontend/src/composition.ts b/packages/app-frontend/src/composition.ts deleted file mode 100644 index 59828038d0..0000000000 --- a/packages/app-frontend/src/composition.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @TODO remove with Vue 3 -import Vue from 'vue' -import VueCompositionAPI from '@vue/composition-api' - -Vue.use(VueCompositionAPI) diff --git a/packages/app-frontend/src/features/App.vue b/packages/app-frontend/src/features/App.vue index 10eadfb689..f091b9bf1e 100644 --- a/packages/app-frontend/src/features/App.vue +++ b/packages/app-frontend/src/features/App.vue @@ -1,29 +1,28 @@ diff --git a/packages/app-frontend/src/features/components/ComponentTreeNode.vue b/packages/app-frontend/src/features/components/ComponentTreeNode.vue index 6c5ca7eae3..414ff87928 100644 --- a/packages/app-frontend/src/features/components/ComponentTreeNode.vue +++ b/packages/app-frontend/src/features/components/ComponentTreeNode.vue @@ -1,11 +1,12 @@ + + diff --git a/packages/app-frontend/src/features/header/AppHeaderLogo.vue b/packages/app-frontend/src/features/header/AppHeaderLogo.vue deleted file mode 100644 index ac7e8daab8..0000000000 --- a/packages/app-frontend/src/features/header/AppHeaderLogo.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/packages/app-frontend/src/features/header/AppHeaderSelect.vue b/packages/app-frontend/src/features/header/AppHeaderSelect.vue index cb57d55b43..a7470cfcd1 100644 --- a/packages/app-frontend/src/features/header/AppHeaderSelect.vue +++ b/packages/app-frontend/src/features/header/AppHeaderSelect.vue @@ -1,5 +1,5 @@ - - diff --git a/packages/app-frontend/src/features/header/header.ts b/packages/app-frontend/src/features/header/header.ts index b92375b0e3..b41cdd1969 100644 --- a/packages/app-frontend/src/features/header/header.ts +++ b/packages/app-frontend/src/features/header/header.ts @@ -1,3 +1,3 @@ -import { ref } from '@vue/composition-api' +import { ref } from 'vue' export const showAppsSelector = ref(true) diff --git a/packages/app-frontend/src/features/header/tabs.ts b/packages/app-frontend/src/features/header/tabs.ts index 2dced1c6a2..c1003f8e44 100644 --- a/packages/app-frontend/src/features/header/tabs.ts +++ b/packages/app-frontend/src/features/header/tabs.ts @@ -1,14 +1,14 @@ -import { computed } from '@vue/composition-api' -import { useRoute } from '@front/util/router' +import { computed } from 'vue' +import { useRoute } from 'vue-router' -export function useTabs () { +export function useTabs() { const route = useRoute() const currentTab = computed(() => { - let fromMeta = route.value.meta.tab + let fromMeta = route.meta.tab if (typeof fromMeta === 'function') { - fromMeta = fromMeta(route.value) + fromMeta = fromMeta(route) } - return fromMeta || route.value.name + return (fromMeta || route.name) as string }) return { diff --git a/packages/app-frontend/src/features/inspector/DataField.vue b/packages/app-frontend/src/features/inspector/DataField.vue index a9e7117573..d824c4c3df 100644 --- a/packages/app-frontend/src/features/inspector/DataField.vue +++ b/packages/app-frontend/src/features/inspector/DataField.vue @@ -1,22 +1,26 @@ + + diff --git a/packages/app-frontend/src/features/plugin/PluginSourceIcon.vue b/packages/app-frontend/src/features/plugin/PluginSourceIcon.vue index c37232aa86..85a3045e74 100644 --- a/packages/app-frontend/src/features/plugin/PluginSourceIcon.vue +++ b/packages/app-frontend/src/features/plugin/PluginSourceIcon.vue @@ -1,9 +1,13 @@ + + + Enable + + + + Clear storage + + + + +
+ + + + + + + + @@ -73,24 +155,93 @@ export default defineComponent({}) - + + Enable + + + + + + Enable +
- - + - Plugin settings - - + + Hide timeline canvas + + + Hide events explorer + + + + + + Enable + + + + + + Enable + + + + + +
+

+ Are you sure you want to clear all storage? +

+
+ + Cancel + + + Clear + +
+
+
+ + diff --git a/packages/app-frontend/src/features/settings/NewTag.vue b/packages/app-frontend/src/features/settings/NewTag.vue index a48d3ef4c1..b9ea5061fd 100644 --- a/packages/app-frontend/src/features/settings/NewTag.vue +++ b/packages/app-frontend/src/features/settings/NewTag.vue @@ -1,6 +1,5 @@ - + + diff --git a/packages/app-frontend/src/features/ui/components/VueDisable.vue b/packages/app-frontend/src/features/ui/components/VueDisable.vue new file mode 100644 index 0000000000..e367c81383 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueDisable.vue @@ -0,0 +1,48 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueDropdown.vue b/packages/app-frontend/src/features/ui/components/VueDropdown.vue new file mode 100644 index 0000000000..7645136328 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueDropdown.vue @@ -0,0 +1,203 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueDropdownButton.vue b/packages/app-frontend/src/features/ui/components/VueDropdownButton.vue new file mode 100644 index 0000000000..b0c3291851 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueDropdownButton.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueFormField.vue b/packages/app-frontend/src/features/ui/components/VueFormField.vue new file mode 100644 index 0000000000..313a3bd3ea --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueFormField.vue @@ -0,0 +1,94 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueGroup.vue b/packages/app-frontend/src/features/ui/components/VueGroup.vue new file mode 100644 index 0000000000..f003feae4d --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueGroup.vue @@ -0,0 +1,131 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueGroupButton.vue b/packages/app-frontend/src/features/ui/components/VueGroupButton.vue new file mode 100644 index 0000000000..0cbee14d37 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueGroupButton.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueIcon.vue b/packages/app-frontend/src/features/ui/components/VueIcon.vue new file mode 100644 index 0000000000..4255ac63f5 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueIcon.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueInput.vue b/packages/app-frontend/src/features/ui/components/VueInput.vue new file mode 100644 index 0000000000..8ddf8153d3 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueInput.vue @@ -0,0 +1,445 @@ + + + + + diff --git a/packages/app-frontend/src/features/ui/components/VueLoadingBar.vue b/packages/app-frontend/src/features/ui/components/VueLoadingBar.vue new file mode 100644 index 0000000000..5d1bfdff0c --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueLoadingBar.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueLoadingIndicator.vue b/packages/app-frontend/src/features/ui/components/VueLoadingIndicator.vue new file mode 100644 index 0000000000..751b252325 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueLoadingIndicator.vue @@ -0,0 +1,6 @@ + diff --git a/packages/app-frontend/src/features/ui/components/VueModal.vue b/packages/app-frontend/src/features/ui/components/VueModal.vue new file mode 100644 index 0000000000..ce1bd2d1de --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueModal.vue @@ -0,0 +1,100 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueSelect.vue b/packages/app-frontend/src/features/ui/components/VueSelect.vue new file mode 100644 index 0000000000..0d31a97b32 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueSelect.vue @@ -0,0 +1,86 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueSelectButton.vue b/packages/app-frontend/src/features/ui/components/VueSelectButton.vue new file mode 100644 index 0000000000..c17af73a66 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueSelectButton.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/VueSwitch.vue b/packages/app-frontend/src/features/ui/components/VueSwitch.vue new file mode 100644 index 0000000000..1f0da92d98 --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/VueSwitch.vue @@ -0,0 +1,88 @@ + + + diff --git a/packages/app-frontend/src/features/ui/components/icons.ts b/packages/app-frontend/src/features/ui/components/icons.ts new file mode 100644 index 0000000000..1dc9c5675c --- /dev/null +++ b/packages/app-frontend/src/features/ui/components/icons.ts @@ -0,0 +1,35 @@ +const icons = require.context( + '@akryum/md-icons-svg/svg/', + true, + /materialicons\/24px\.svg$/, +) + +export default { + install() { + const sprites = [''] + let spriteIndex = 0 + // Load all the SVG symbols + icons.keys().forEach((key, index) => { + let result = icons(key) + const [, iconName] = /(\w+)\/materialicons/.exec(key) + // eslint-disable-next-line regexp/no-super-linear-backtracking + const [, content] = /(.*)<\/svg>/.exec(result) + result = `${content}` + sprites[spriteIndex] += result + if ((index + 1) % 40 === 0) { + sprites.push('') + spriteIndex++ + } + }) + for (const html of sprites) { + const iconsWrapper = document.createElement('div') + iconsWrapper.style.display = 'none' + iconsWrapper.innerHTML = html + document.body.insertBefore(iconsWrapper, document.body.firstChild) + } + }, +} + +export function generateHtmlIcon(icon: string) { + return `
` +} diff --git a/packages/app-frontend/src/features/ui/composables/useDisableScroll.ts b/packages/app-frontend/src/features/ui/composables/useDisableScroll.ts new file mode 100644 index 0000000000..0304a6cbd0 --- /dev/null +++ b/packages/app-frontend/src/features/ui/composables/useDisableScroll.ts @@ -0,0 +1,30 @@ +import { onBeforeUnmount, onMounted } from 'vue' + +let count = 0 + +function getScrollingElements() { + return document.querySelectorAll('.vue-ui-disable-scroll, body') +} + +function updateScroll() { + if (count === 0) { + getScrollingElements().forEach(el => + el.classList.remove('vue-ui-no-scroll'), + ) + } + else if (count === 1) { + getScrollingElements().forEach(el => el.classList.add('vue-ui-no-scroll')) + } +} + +export function useDisableScroll() { + onMounted(() => { + count++ + updateScroll() + }) + + onBeforeUnmount(() => { + count-- + updateScroll() + }) +} diff --git a/packages/app-frontend/src/features/ui/composables/useDisabled.ts b/packages/app-frontend/src/features/ui/composables/useDisabled.ts new file mode 100644 index 0000000000..072e935394 --- /dev/null +++ b/packages/app-frontend/src/features/ui/composables/useDisabled.ts @@ -0,0 +1,36 @@ +/** + * (Use with the DisabledChild mixin) + * Allow disabling an entire tree of components implementing the DisabledChild mixin. + */ + +import { computed, inject, provide, reactive, watch } from 'vue' + +export function useDisabledParent(props: { disabled?: boolean }) { + const injectedDisableData = reactive({ + value: props.disabled || false, + }) + + provide('VueDisableMixin', { + data: injectedDisableData, + }) + + watch( + () => props.disabled, + (value, oldValue) => { + if (value !== oldValue) { + injectedDisableData.value = value + } + }, + ) +} + +export function useDisabledChild(props: { disabled?: boolean }) { + const injectDisable = inject<{ data: { value: boolean } } | undefined>( + 'VueDisableMixin', + null, + ) + + return { + finalDisabled: computed(() => props.disabled || (injectDisable && injectDisable.data.value)), + } +} diff --git a/packages/app-frontend/src/features/ui/index.ts b/packages/app-frontend/src/features/ui/index.ts new file mode 100644 index 0000000000..e791e14d15 --- /dev/null +++ b/packages/app-frontend/src/features/ui/index.ts @@ -0,0 +1,60 @@ +import type { Plugin } from 'vue' +import FloatingVue from 'floating-vue' +import VueIcons from './components/icons' +import VueDisable from './components/VueDisable.vue' +import VueButton from './components/VueButton.vue' +import VueDropdown from './components/VueDropdown.vue' +import VueDropdownButton from './components/VueDropdownButton.vue' +import VueFormField from './components/VueFormField.vue' +import VueLoadingIndicator from './components/VueLoadingIndicator.vue' +import VueGroup from './components/VueGroup.vue' +import VueGroupButton from './components/VueGroupButton.vue' +import VueIcon from './components/VueIcon.vue' +import VueInput from './components/VueInput.vue' +import VueLoadingBar from './components/VueLoadingBar.vue' +import VueSwitch from './components/VueSwitch.vue' +import VueSelect from './components/VueSelect.vue' +import VueSelectButton from './components/VueSelectButton.vue' +import VueModal from './components/VueModal.vue' +import 'floating-vue/dist/style.css' + +export { generateHtmlIcon } from './components/icons' + +const ui: Plugin = { + install(app) { + app.use(VueIcons) + app.component('VueButton', VueButton) + app.component('VueDisable', VueDisable) + app.component('VueDropdown', VueDropdown) + app.component('VueFormField', VueFormField) + app.component('VueDropdownButton', VueDropdownButton) + app.component('VueLoadingIndicator', VueLoadingIndicator) + app.component('VueGroup', VueGroup) + app.component('VueGroupButton', VueGroupButton) + app.component('VueIcon', VueIcon) + app.component('VueInput', VueInput) + app.component('VueLoadingBar', VueLoadingBar) + app.component('VueSwitch', VueSwitch) + app.component('VueSelect', VueSelect) + app.component('VueSelectButton', VueSelectButton) + app.component('VueModal', VueModal) + + app.use(FloatingVue, { + container: 'body', + instantMove: true, + themes: { + tooltip: { + delay: { + show: 1000, + hide: 800, + }, + }, + dropdown: { + handleResize: false, + }, + }, + }) + }, +} + +export default ui diff --git a/packages/app-frontend/src/features/welcome/WelcomeSlideshow.vue b/packages/app-frontend/src/features/welcome/WelcomeSlideshow.vue index aebbaf07ed..06e6005320 100644 --- a/packages/app-frontend/src/features/welcome/WelcomeSlideshow.vue +++ b/packages/app-frontend/src/features/welcome/WelcomeSlideshow.vue @@ -1,12 +1,12 @@ diff --git a/packages/shell-dev-vue3/src/Form.vue b/packages/shell-dev-vue3/src/Form.vue index ef32afc589..5063d91c88 100644 --- a/packages/shell-dev-vue3/src/Form.vue +++ b/packages/shell-dev-vue3/src/Form.vue @@ -1,18 +1,3 @@ - - + + diff --git a/packages/shell-dev-vue3/src/FormSection.vue b/packages/shell-dev-vue3/src/FormSection.vue index 104b031ba5..c9fa476676 100644 --- a/packages/shell-dev-vue3/src/FormSection.vue +++ b/packages/shell-dev-vue3/src/FormSection.vue @@ -1,7 +1,6 @@ diff --git a/packages/shell-dev-vue3/src/Functional.vue b/packages/shell-dev-vue3/src/Functional.vue index 7f01e1f815..9310b6e0dc 100644 --- a/packages/shell-dev-vue3/src/Functional.vue +++ b/packages/shell-dev-vue3/src/Functional.vue @@ -1,7 +1,7 @@ + + -
+
Not Vue
diff --git a/packages/shell-host/src/DevIframe.vue b/packages/shell-host/src/DevIframe.vue index 3fa3ce1d44..d3c892b016 100644 --- a/packages/shell-host/src/DevIframe.vue +++ b/packages/shell-host/src/DevIframe.vue @@ -1,5 +1,5 @@