Repository: leonardoventurini/meteor-devtools-evolved Branch: development Commit: fda491248b56 Files: 210 Total size: 234.5 KB Directory structure: gitextract_md1jik7s/ ├── .babelrc ├── .claude/ │ └── settings.json ├── .editorconfig ├── .envrc ├── .eslintrc.js ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── .serena/ │ ├── .gitignore │ ├── memories/ │ │ ├── architecture_patterns.md │ │ ├── code_style_and_conventions.md │ │ ├── codebase_structure.md │ │ ├── project_overview.md │ │ ├── security_and_auditing.md │ │ ├── suggested_commands.md │ │ ├── task_completion_checklist.md │ │ └── tech_stack.md │ └── project.yml ├── .yarnrc.yml ├── CHANGELOG.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── devapp-2.0.0/ │ ├── .gitignore │ ├── .meteor/ │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client/ │ │ ├── main.css │ │ ├── main.html │ │ └── main.jsx │ ├── imports/ │ │ ├── api/ │ │ │ ├── links.js │ │ │ └── random.js │ │ └── ui/ │ │ ├── App.jsx │ │ ├── Hello.jsx │ │ └── Info.jsx │ ├── package.json │ ├── server/ │ │ └── main.js │ └── tests/ │ └── main.js ├── devapp-2.2.0/ │ ├── .gitignore │ ├── .meteor/ │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client/ │ │ ├── main.css │ │ ├── main.html │ │ └── main.jsx │ ├── imports/ │ │ ├── api/ │ │ │ ├── links.js │ │ │ └── random.js │ │ └── ui/ │ │ ├── App.jsx │ │ ├── Hello.jsx │ │ └── Info.jsx │ ├── package.json │ ├── server/ │ │ └── main.js │ └── tests/ │ └── main.js ├── devapp-2.2.4/ │ ├── .gitignore │ ├── .meteor/ │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client/ │ │ ├── main.css │ │ ├── main.html │ │ └── main.jsx │ ├── imports/ │ │ ├── api/ │ │ │ ├── links.js │ │ │ └── random.js │ │ └── ui/ │ │ ├── App.jsx │ │ ├── Hello.jsx │ │ └── Info.jsx │ ├── package.json │ ├── server/ │ │ └── main.js │ └── tests/ │ └── main.js ├── devapp-3.4/ │ ├── .gitignore │ ├── .meteor/ │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── .swcrc │ ├── client/ │ │ ├── main.css │ │ ├── main.html │ │ └── main.jsx │ ├── imports/ │ │ ├── api/ │ │ │ └── links.js │ │ └── ui/ │ │ ├── App.jsx │ │ ├── Counter.jsx │ │ ├── Header.jsx │ │ ├── Info.jsx │ │ └── styles.css │ ├── package.json │ ├── rspack.config.js │ ├── server/ │ │ └── main.js │ └── tests/ │ └── main.js ├── eslint.config.mjs ├── extension/ │ ├── devtools-panel.html │ ├── devtools.html │ ├── manifest-v2.json │ ├── manifest-v3.json │ ├── options.html │ └── popup.html ├── lint-staged.js ├── package.json ├── postcss.config.js ├── src/ │ ├── Analytics.ts │ ├── App.tsx │ ├── AppToaster.jsx │ ├── Bridge.ts │ ├── Browser/ │ │ ├── Background.ts │ │ ├── Content.ts │ │ ├── DevTools.ts │ │ ├── Inject.ts │ │ └── MeteorLibrary.ts │ ├── Components/ │ │ ├── Button.tsx │ │ ├── Field.tsx │ │ ├── PopoverButton.tsx │ │ ├── Separator.tsx │ │ ├── StatusBar.tsx │ │ ├── TabBar.tsx │ │ └── TextInput.tsx │ ├── Constants.ts │ ├── Database/ │ │ └── PanelDatabase.ts │ ├── Injectors/ │ │ ├── DDPInjector.ts │ │ ├── MeteorAdapter.ts │ │ └── MinimongoInjector.ts │ ├── Log.ts │ ├── Pages/ │ │ ├── Options.tsx │ │ ├── Panel/ │ │ │ ├── Bookmarks/ │ │ │ │ ├── Bookmarks.tsx │ │ │ │ └── BookmarksStatus.tsx │ │ │ ├── DDP/ │ │ │ │ ├── DDP.tsx │ │ │ │ ├── DDPContainer.tsx │ │ │ │ ├── DDPFilterMenu.tsx │ │ │ │ ├── DDPLog.tsx │ │ │ │ ├── DDPLogDirection.tsx │ │ │ │ ├── DDPLogMenu.tsx │ │ │ │ ├── DDPLogPreview.tsx │ │ │ │ ├── DDPStatus.tsx │ │ │ │ └── FilterConstants.ts │ │ │ ├── DrawerJSON.tsx │ │ │ ├── DrawerStackTrace.tsx │ │ │ ├── HelpDrawer.tsx │ │ │ ├── Minimongo/ │ │ │ │ ├── Minimongo.tsx │ │ │ │ ├── MinimongoContainer.tsx │ │ │ │ ├── MinimongoNavigator.tsx │ │ │ │ ├── MinimongoRow.tsx │ │ │ │ └── MinimongoStatus.tsx │ │ │ ├── Navigation.tsx │ │ │ ├── PartnersGrid.tsx │ │ │ ├── Performance/ │ │ │ │ └── Performance.tsx │ │ │ └── Subscriptions/ │ │ │ └── Subscriptions.tsx │ │ ├── Panel.tsx │ │ └── Popup.tsx │ ├── Stores/ │ │ ├── Common/ │ │ │ └── Searchable.ts │ │ ├── Panel/ │ │ │ ├── BookmarkStore.ts │ │ │ ├── DDPStore.ts │ │ │ ├── MinimongoStore/ │ │ │ │ ├── CollectionStore.ts │ │ │ │ └── index.ts │ │ │ ├── PerformanceStore.ts │ │ │ ├── SettingStore.ts │ │ │ └── SubscriptionStore.ts │ │ └── PanelStore.tsx │ ├── Styles/ │ │ ├── App.scss │ │ ├── Breakpoints.ts │ │ ├── Constants.ts │ │ ├── Mixins.ts │ │ ├── Tailwind.css │ │ └── _Utils.scss │ ├── Utils/ │ │ ├── BackgroundEvents.ts │ │ ├── Hideable.tsx │ │ ├── Hooks/ │ │ │ ├── useAnalytics.ts │ │ │ ├── useBreakpoints.ts │ │ │ ├── useDimensions.ts │ │ │ ├── useInterval.ts │ │ │ └── useResize.ts │ │ ├── JSONUtils.ts │ │ ├── MessageFormatter.ts │ │ ├── Numbers.ts │ │ ├── ObjectTreerinator/ │ │ │ ├── ArrayNodeRenderer.tsx │ │ │ ├── ArrayRenderer.tsx │ │ │ ├── BooleanRenderer.tsx │ │ │ ├── Collapsible.tsx │ │ │ ├── NullRenderer.tsx │ │ │ ├── NumberRenderer.tsx │ │ │ ├── ObjectRenderer.tsx │ │ │ ├── StringRenderer.tsx │ │ │ └── index.tsx │ │ ├── Objects.ts │ │ ├── Pagination.ts │ │ ├── StringUtils.ts │ │ └── index.ts │ └── index.d.ts ├── tailwind.config.js ├── tsconfig.json └── webpack/ ├── base.js ├── chrome.dev.js ├── chrome.prod.js ├── firefox.dev.js ├── firefox.prod.js └── utils.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["@babel/env", "@babel/react"] } ================================================ FILE: .claude/settings.json ================================================ { "enabledPlugins": { "frontend-design@claude-plugins-official": true, "context7@claude-plugins-official": true, "code-review@claude-plugins-official": true, "feature-dev@claude-plugins-official": true, "code-simplifier@claude-plugins-official": true, "typescript-lsp@claude-plugins-official": true, "superpowers@claude-plugins-official": true }, "defaultMode": "bypassPermissions", "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" } ] } ], "Notification": [ { "hooks": [ { "type": "command", "command": "afplay /System/Library/Sounds/Funk.aiff" } ] } ], "PostToolUse": [ { "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "yarn prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" && yarn eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\" || exit 2" } ] } ] } } ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true [*.{js, jsx, ts, tsx, groovy}] indent_size = 2 indent_style = space ij_visual_guides = 80 ================================================ FILE: .envrc ================================================ #!/usr/bin/env bash export DEVTOOLS_HOME="$(git rev-parse --show-toplevel)" export MAC_CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" function mpm { meteor npm $@ } function start { yarn devapp } function develop { local BROWSER=${1-"chrome"} echo "Starting Development mode for => ${BROWSER}" yarn concurrently -n ext,app "webpack --config webpack/${BROWSER}.dev.js" "cd devapp-3.4 && npm start" } function watch { local BROWSER=${1-"chrome"} yarn webpack --config webpack/${BROWSER}.dev.js } function setup { yarn cd devapp-3.4 || exit npm install } function update-meteor { cd devapp-3.4 || exit meteor update cd .. } function package-version { grep version git commit -m "message" # Pre-commit hook runs lint-staged automatically git push ``` ## Useful System Commands (macOS/Darwin) ```bash # List files ls -la # Search for files find . -name "*.tsx" # Search in files grep -r "pattern" src/ # View file contents cat head -n 20 tail -n 20 # Navigate directories cd pwd ``` ## Testing Extension Manually After running `yarn dev:chrome` or `yarn dev:firefox`, the extension will be loaded in a browser instance. Navigate to `http://localhost:2100` to see the devapp and test the extension's DevTools panel. ## Node/Yarn Version Management The project uses Volta to manage Node and Yarn versions: - Node.js: 14.19.3 - Yarn: 1.22.18 Volta will automatically use the correct versions if installed. ## Troubleshooting ```bash # If dependencies are out of sync, reinstall rm -rf node_modules yarn.lock yarn install # If devapp has issues cd devapp rm -rf node_modules yarn.lock yarn install cd .. # Reset Meteor (if devapp is broken) cd devapp meteor reset cd .. ``` ================================================ FILE: .serena/memories/task_completion_checklist.md ================================================ # Task Completion Checklist When completing a coding task in this project, follow these steps: ## 1. Pre-Commit Automatic Checks The project uses Husky + lint-staged for pre-commit hooks. When you commit, the following runs automatically: - ✓ ESLint on all staged .js, .jsx, .ts, .tsx files - ✓ TypeScript type checking (tsc --noEmit) - ✓ Prettier formatting on staged files - ✓ React Scripts tests (if applicable, with --passWithNoTests) **If the pre-commit hook fails**, fix the issues before committing: ```bash # Run lint manually to see all issues yarn lint # TypeScript errors need to be fixed in code # ESLint and Prettier issues are often auto-fixed by lint-staged ``` ## 2. Manual Verification After making changes, verify: ### Linting ```bash yarn lint ``` Ensure no ESLint errors or warnings. ### Security Audit ```bash yarn run audit ``` Ensure no high or critical security vulnerabilities are introduced. ### Building ```bash # Test Chrome build yarn build:chrome # Test Firefox build yarn build:firefox ``` Ensure both builds complete successfully without errors. ### Manual Testing ```bash # Start dev environment yarn dev:chrome # or yarn dev:firefox ``` - Open the browser instance that launches - Navigate to http://localhost:2100 (devapp) - Open DevTools and find the "Meteor" panel - Test your changes manually in the extension UI ## 3. Code Quality Checks Before considering the task done: - [ ] TypeScript compiles without errors - [ ] ESLint shows no errors or warnings - [ ] Code follows project conventions (2-space indent, LF line endings, etc.) - [ ] No unused imports or variables - [ ] MobX stores updated if state changes - [ ] Components are properly typed - [ ] Path imports use `@/` alias where appropriate ## 4. Cross-Browser Compatibility If the change affects browser-specific code: - [ ] Test in Chrome build (`yarn dev:chrome`) - [ ] Test in Firefox build (`yarn dev:firefox`) - [ ] Check for webextension-polyfill usage for cross-browser APIs ## 5. Git Commit ```bash git add git commit -m "descriptive message" # Pre-commit hooks run automatically ``` ## 6. Common Issues ### Pre-commit hook fails - Check `yarn lint` output - Run prettier manually if needed - Fix TypeScript errors shown by tsc ### Build fails - Check webpack output for specific errors - Verify all imports exist and are correct - Check for TypeScript compilation errors ### Extension doesn't load - Check browser console for errors - Verify manifest.json was generated correctly - Check extension/chrome or extension/firefox directories exist ## Notes - The devapp must be running for the extension to work properly - IndexedDB data persists between sessions (for bookmarks) - Check Chrome DevTools console in the extension context for runtime errors ================================================ FILE: .serena/memories/tech_stack.md ================================================ # Tech Stack ## Core Technologies - **TypeScript** 4.4.3 - Main programming language with ES6 target - **React** 17.0.2 - UI framework (functional components with hooks) - **MobX** 6.4.0 - State management library - **Webpack** 5 - Module bundler and build tool ## UI & Styling - **Blueprint** 4.14.1 - Core UI components library by Palantir - **Styled Components** 5.3.3 - CSS-in-JS styling - **SASS/SCSS** - Additional styling with sass-loader - **Tailwind CSS** 3.0.24 - Utility-first CSS framework - **DaisyUI** 2.15.2 - Tailwind CSS component library - **Normalize.css** - CSS reset - **Heroicons React** - Icon library ## Data & State - **Dexie** 3.2.2 - IndexedDB wrapper for bookmarks storage - **mobx-react-lite** 3.3.0 - React bindings for MobX ## Utilities - **Luxon** 2.5.2 - Date/time manipulation - **Lodash** (selective imports: debounce, memoize, sortby, throttle) - **D3** (collection, hierarchy, selection, shape) - Data visualization - **pretty-bytes** - Byte formatting - **uuid** - Unique ID generation ## Build Tools & Dev Environment - **Babel** 7 - JavaScript transpiler - **ts-loader** - TypeScript loader for Webpack - **PostCSS** - CSS processing - **Terser** - JavaScript minification - **web-ext** - Browser extension development tool - **concurrently** / **npm-run-all** - Run multiple commands - **wait-on** - Wait for resources before starting ## Code Quality - **ESLint** - JavaScript/TypeScript linter (extends @tstt/eslint-config) - **Prettier** - Code formatter (from @tstt/eslint-config) - **Husky** - Git hooks - **lint-staged** - Run linters on staged files ## Runtime Environment - **Node.js** 14.19.3 (managed by Volta) - **Yarn** 1.22.18 (managed by Volta) ## Browser APIs - **@types/chrome** - Chrome extension API types - **webextension-polyfill** - Cross-browser extension API ================================================ FILE: .serena/project.yml ================================================ # the name by which the project can be referenced within Serena project_name: 'meteor-devtools-evolved' # list of languages for which language servers are started; choose from: # al bash clojure cpp csharp # csharp_omnisharp dart elixir elm erlang # fortran fsharp go groovy haskell # java julia kotlin lua markdown # matlab nix pascal perl php # powershell python python_jedi r rego # ruby ruby_solargraph rust scala swift # terraform toml typescript typescript_vts vue # yaml zig # (This list may be outdated. For the current list, see values of Language enum here: # https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py # For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) # Note: # - For C, use cpp # - For JavaScript, use typescript # - For Free Pascal/Lazarus, use pascal # Special requirements: # Some languages require additional setup/installations. # See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers # When using multiple languages, the first language server that supports a given file will be used for that file. # The first language is the default language and the respective language server will be used as a fallback. # Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. languages: - typescript # the encoding used by text files in the project # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings encoding: 'utf-8' # whether to use project's .gitignore files to ignore files ignore_all_files_in_gitignore: true # list of additional paths to ignore in all projects # same syntax as gitignore, so you can use * and ** ignored_paths: [] # whether the project is in read-only mode # If set to true, all editing tools will be disabled and attempts to use them will result in an error # Added on 2025-04-18 read_only: false # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. # Below is the complete list of tools for convenience. # To make sure you have the latest list of tools, and to view their descriptions, # execute `uv run scripts/print_tool_overview.py`. # # * `activate_project`: Activates a project by name. # * `check_onboarding_performed`: Checks whether project onboarding was already performed. # * `create_text_file`: Creates/overwrites a file in the project directory. # * `delete_lines`: Deletes a range of lines within a file. # * `delete_memory`: Deletes a memory from Serena's project-specific memory store. # * `execute_shell_command`: Executes a shell command. # * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. # * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). # * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). # * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. # * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. # * `initial_instructions`: Gets the initial instructions for the current project. # Should only be used in settings where the system prompt cannot be set, # e.g. in clients you have no control over, like Claude Desktop. # * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. # * `insert_at_line`: Inserts content at a given line in a file. # * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. # * `list_dir`: Lists files and directories in the given directory (optionally with recursion). # * `list_memories`: Lists memories in Serena's project-specific memory store. # * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). # * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). # * `read_file`: Reads a file within the project directory. # * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. # * `remove_project`: Removes a project from the Serena configuration. # * `replace_lines`: Replaces a range of lines within a file with new content. # * `replace_symbol_body`: Replaces the full definition of a symbol. # * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. # * `search_for_pattern`: Performs a search for a pattern in the project. # * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. # * `switch_modes`: Activates modes by providing a list of their names # * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. # * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. # * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. # * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. excluded_tools: [] # list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) included_optional_tools: [] # fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. # This cannot be combined with non-empty excluded_tools or included_optional_tools. fixed_tools: [] # list of mode names to that are always to be included in the set of active modes # The full set of modes to be activated is base_modes + default_modes. # If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. # Otherwise, this setting overrides the global configuration. # Set this to [] to disable base modes for this project. # Set this to a list of mode names to always include the respective modes for this project. base_modes: # list of mode names that are to be activated by default. # The full set of modes to be activated is base_modes + default_modes. # If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. # Otherwise, this overrides the setting from the global configuration (serena_config.yml). # This setting can, in turn, be overridden by CLI parameters (--mode). default_modes: # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: '' ================================================ FILE: .yarnrc.yml ================================================ nodeLinker: node-modules ================================================ FILE: CHANGELOG.md ================================================ # Change Log All notable changes to this project will be documented in this file. > The dates refer to when it was made available in the Chrome platform. ## [1.8] - 2023-01-17 ## Fixed - Fixed a bug where the extension crashes on custom Minimongo types specifically on MongoDecimal type. - Issue where the lodash package would break the extension randomly on some pages and reduced bundle size. ## Added - Help drawer containing contact information for the author and for partners. Moved the content from the About drawer to the Help drawer. - Added sponsor button so users can support the project. ## [1.7] - 2022-07-05 ## Added - The Firefox browser is now supported. Thanks to Niloy. ## Changed (development only) - Now we can build extensions for both Chrome and Firefox using the same codebase. - New commands added to support the workflow. ## [1.6] - 2022-06-08 ## Added - Now logs are intercepted and stored in the background and loaded when you open the devtools panel. - Add Meteor emoji to devtool tab. - Add emojis to some actions in the top bar. - Add Galaxy Sponsor content. ## Removed - Removed CRC32 hashes, they did not have much use besides looking cool. Improves performance a bit. ## [1.5] - 2022-04-14 ## Added - Google Analytics for improving the extension. - The browser action of the extension opens Galaxy. - Add subscription duration, so we know how long specific subscriptions take. - Performance tab which measures Minimongo calls. ## Changed - Upgrade dependencies to latest. - Add copy JSON button to minimongo drawer. - Improved minimongo tracking performance and responsiveness. - By default JSON documents are expanded up to 5 levels now. - Remove Iosevka font, the default monospaced font from OS. - Now the extension is loaded slightly earlier, so we don't miss initial Meteor activity. ## Fixed - Fix stack trace error issue caused by a third party library affecting some users. ## [1.4] - 2020-07-21 ## Changed - Make stack trace and bookmark buttons more accessible. - Make right menu more responsive. - Estimated collection size is always visible for all collections. ## Fixed - Fix `error-stack-parser` global pollution interacting badly with some websites. ## [1.3] - 2020-06-17 ## Added - Meteor `gitCommitHash` is now shown in the status bar. - Community Slack button (with VFX!!) - Added subscription search. - Estimate Minimongo collection byte size. ## Changed - Subscriptions are clickable and open the params object viewer. - Improved naming for the extension global variables to avoid collisions. - Removed horizontal scroll constraint, but making it more responsive is too much work for now. ## Fixed - Fixed horizontal scroll showing when resizing. ## [1.2] - 2020-04-29 In this release I am focusing on some quality of life changes and addressing issues reported by the community. I tried to make the design simpler and more efficient as well. ### Added - Minimongo sidebar navigation ordered alphabetically and with counts. - Add the Iosevka font as it is more space efficient in certain scenarios. - Subscriptions tab listing all current subscriptions in real-time-ish. ### Changed - The DDP log is now a virtualized list with INFINITE scrolling and new logs come at the top. - Moved extension logs from top frame to background. NO MORE ANNOYING CONSOLE LOGS!!! - Logs now have their interaction menu as a popover in order to be more space efficient. - More space-efficient tabs and status bars. ### Fixed - Small fixes and improvements in Treerinator (JSON Viewer). - Show subscription name when ready as well. - Fixed GitHub stats not persisting as they should. ## [1.1] - 2020-04-02 I had to take a small hiatus from development after the initial release, but now I am back with a few quality of life changes and additions. Also, I am attempting to fix an issue where some installations do not initialize and thus don't log the DDP messages, which also happens to be at least a quality of life change. Hope it works, as I could not reproduce the issue, but I added a bunch of logs just in case, I promise. ### Added - [Issue #1](https://github.com/leonardoventurini/meteor-devtools-evolved/issues/1) Added ability to replay methods either from the logs or bookmarks. - Added document count to collection navigator. - Added Minimongo active collection clear button. - Added GitHub buttons to make receiving feedback easier. - Added long timestamp format on hover for logs which is useful for bookmarks. - Added setting persistence, which means the filters will persist between sessions. - Added about page with some basics and license information. ### Changed - Adjusted the layout, so it is responsive to screens with less horizontal real-estate. - Collection tags are now clickable. ### Fixed - [Issue #2](https://github.com/leonardoventurini/meteor-devtools-evolved/issues/2) The extension now initializes from the content script, which means that we don't need the devtools panel open for initialization -- but we do need it for DDP logging. ## [1.0] - 2020-03-05 Initial release. ### Added - Added DDP logging. - Added DDP bookmarking. - Added Minimongo browsing. - Added search and pagination. - Added a bunch of stuff really. ================================================ FILE: CLAUDE.md ================================================ IMPORTANT: The first thing you ever do is to start the project `meteor-devtools-evolved` in the Serena MCP server ================================================ FILE: CONTRIBUTING.md ================================================ ## Setting the Environment Up 1. Install dependencies for `devapp-3.4` & `root` with `yarn`. ```shell yarn setup ``` > As of now we use Node.js `v14.19.3`. 2. Run the extension locally ```shell yarn dev # default chrome ``` ```shell yarn dev:chrome # for chrome ``` ```shell yarn dev:firefox # for firefox ``` > This command will build and watch the extension and run the `devapp-3.4` in parallel mode and when they are ready it will launch the chrome/firefox private instance with extension installed 5. Hack away! > Open a Pull Request from your fork to our repo once it is done or need a review. ## Environment Commands If you use Linux you can run `source .envrc` for some useful commands > -c: for chrome, -f: firefox, (chrome is default) - Setup extension and test project Dependencies ```shell setup ``` ## Build - Chrome ```shell npm run build:chrome ``` - Firefox ```shell npm run build:firefox ``` ## Guidelines & Objectives 1. The code must be linted and properly formatted, that can be easily done with the right IDE -- I use JetBrains WebStorm. Perhaps some git hooks would come in handy in the future. 2. Every feature needs to take into account the Meteor community as a whole and not the interest of a few in detriment of others. 3. Be friendly and supportive, no one is perfect, and we all have limited time, especially in these difficult times. ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2020 Leonardo Venturini Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Meteor Devtool Evolved Gif

Meteor Devtools Extension

Behold, the evolution of Meteor DevTools.

Meteor Devtools Evolved is currently available for Google Chrome and Mozilla Firefox.

Download for Chrome Download for Firefox

[Harder, Better, Faster, Stronger](https://www.youtube.com/watch?v=gAjR4_CbPpQ) :rocket: Are you beginning with Meteor? Do you want to get a sense of "what is going on" or even to optimize your Meteor app? This is the tool for you. :point_right: [Changelog](CHANGELOG.md) ### Distributed Data Protocol (DDP) Everything you need to track and understand what is going on under the hood of your Meteor application. The extension allows you to filter and search for any DDP message, being able to handle thousands and thousands of messages without a hiccup. ### Bookmarks The DDP inspection is ephemeral, but you can save as many DDP messages you want for later search and retrieval, from any host. Be careful though, it is saved on IndexedDB. ### Minimongo You don't know what data belongs to where? You can rapidly search for anything in your Minimongo data and easily visualize the documents with our blazing fast custom-made Object Treerinator. --- ## Development > DISCLAIMER: This work is based in part on the [Meteor DevTools](https://github.com/bakery/meteor-devtools) extension by The Bakery. Which sadly is not maintained anymore. While it is not necessarily a fork, I did use some useful knowledge and architectural decisions, and some things naturally converged into the same most practical solution. Hence the "evolved". The extension is almost entirely written in TypeScript, while some Chrome specific code being left out for practical reasons. It uses MobX to manage state, and SASS its styles. We also use components from the [Blueprint](https://github.com/palantir/blueprint) library by Palantir. Everything is glued together with Webpack. > Anyone is welcome to contribute, more info [here](CONTRIBUTING.md). ## Firefox The Firefox port of the extension was a contribution made by [@nilooy](https://github.com/nilooy). Thank you! ================================================ FILE: devapp-2.0.0/.gitignore ================================================ node_modules/ ================================================ FILE: devapp-2.0.0/.meteor/.finished-upgraders ================================================ # This file contains information which helps Meteor properly upgrade your # app when you run 'meteor update'. You should check it into version control # with your project. notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file notices-for-facebook-graph-api-2 1.2.0-standard-minifiers-package 1.2.0-meteor-platform-split 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package 1.4.0-remove-old-dev-bundle-link 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package 1.7-split-underscore-from-meteor-base 1.8.3-split-jquery-from-blaze ================================================ FILE: devapp-2.0.0/.meteor/.gitignore ================================================ local ================================================ FILE: devapp-2.0.0/.meteor/.id ================================================ # This file contains a token that is unique to your project. # Check it into your repository along with the rest of this directory. # It can be used for purposes such as: # - ensuring you don't accidentally deploy one app on top of another # - providing package authors with aggregated statistics x58f9z1u8cbj.d63i3vx3cjzb ================================================ FILE: devapp-2.0.0/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # Check this file (and the other files in this directory) into your repository. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. meteor-base@1.4.0 # Packages every Meteor app needs to have mobile-experience@1.1.0 # Packages for a great mobile UX mongo@1.10.1 # The database Meteor supports right now reactive-var@1.0.11 # Reactive variable for tracker standard-minifier-css@1.7.2 # CSS minifier run for production mode standard-minifier-js@2.6.0 # JS minifier run for production mode es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers ecmascript@0.15.0 # Enable ECMAScript2015+ syntax in app code typescript@4.1.2 # Enable TypeScript syntax in .ts and .tsx modules shell-server@0.5.0 # Server-side component of the `meteor shell` command hot-module-replacement@0.2.0 # Update client in development without reloading the page insecure@1.0.7 # Allow all DB writes from clients (for prototyping) static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data ================================================ FILE: devapp-2.0.0/.meteor/platforms ================================================ server browser ================================================ FILE: devapp-2.0.0/.meteor/release ================================================ METEOR@2.0 ================================================ FILE: devapp-2.0.0/.meteor/versions ================================================ allow-deny@1.1.0 autoupdate@1.7.0 babel-compiler@7.6.2 babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 callback-hook@1.3.1 check@1.3.1 ddp@1.4.0 ddp-client@2.4.1 ddp-common@1.4.0 ddp-server@2.3.3 diff-sequence@1.1.1 dynamic-import@0.6.0 ecmascript@0.15.1 ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.11.1 ecmascript-runtime-server@0.10.1 ejson@1.1.1 es5-shim@4.8.0 fetch@0.1.1 geojson-utils@1.0.10 hot-code-push@1.0.4 hot-module-replacement@0.2.1 html-tools@1.1.3 htmljs@1.1.1 id-map@1.1.1 insecure@1.0.7 inter-process-messaging@0.1.1 launch-screen@1.2.1 livedata@1.0.18 logging@1.2.0 meteor@1.9.3 meteor-base@1.4.0 minifier-css@1.5.4 minifier-js@2.6.1 minimongo@1.6.2 mobile-experience@1.1.0 mobile-status-bar@1.1.0 modern-browsers@0.1.7 modules@0.16.0 modules-runtime@0.12.0 modules-runtime-hot@0.13.0 mongo@1.10.1 mongo-decimal@0.1.2 mongo-dev-server@1.1.0 mongo-id@1.0.8 npm-mongo@3.8.1 ordered-dict@1.1.0 promise@0.11.2 random@1.2.0 react-fast-refresh@0.1.1 react-meteor-data@2.5.1 reactive-var@1.0.11 reload@1.3.1 retry@1.1.0 routepolicy@1.1.0 shell-server@0.5.0 socket-stream-client@0.3.3 spacebars-compiler@1.3.1 standard-minifier-css@1.7.3 standard-minifier-js@2.6.1 static-html@1.3.2 templating-tools@1.2.2 tracker@1.2.0 typescript@4.1.2 underscore@1.0.10 webapp@1.10.1 webapp-hashing@1.1.0 ================================================ FILE: devapp-2.0.0/client/main.css ================================================ body { padding: 10px; font-family: sans-serif; } ================================================ FILE: devapp-2.0.0/client/main.html ================================================ devapp
================================================ FILE: devapp-2.0.0/client/main.jsx ================================================ import React from 'react' import { Meteor } from 'meteor/meteor' import { render } from 'react-dom' import { App } from '../imports/ui/App' import '../imports/api/links' import '../imports/api/random' Meteor.startup(() => { render(, document.getElementById('react-target')) }) ================================================ FILE: devapp-2.0.0/imports/api/links.js ================================================ import { Mongo } from 'meteor/mongo' export const LinksCollection = new Mongo.Collection('links') ================================================ FILE: devapp-2.0.0/imports/api/random.js ================================================ import { Mongo } from 'meteor/mongo' export const RandomCollection = new Mongo.Collection('random') ================================================ FILE: devapp-2.0.0/imports/ui/App.jsx ================================================ import React, { useEffect, useRef, useState } from 'react' import { useTracker } from 'meteor/react-meteor-data' import { RandomCollection } from '../api/random' export const App = () => { const [isSpamming, setSpamming] = useState(false) const spammerRef = useRef(0) const r1to100 = useTracker(() => { const handle = Meteor.subscribe('random1to100') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r101to200 = useTracker(() => { const handle = Meteor.subscribe('random101to200') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r201to300 = useTracker(() => { const handle = Meteor.subscribe('random201to300') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r301to400 = useTracker(() => { const handle = Meteor.subscribe('random301to400') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r401to500 = useTracker(() => { const handle = Meteor.subscribe('random401to500') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r501to600 = useTracker(() => { const handle = Meteor.subscribe('random501to600') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r601to700 = useTracker(() => { const handle = Meteor.subscribe('random601to700') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r701to800 = useTracker(() => { const handle = Meteor.subscribe('random701to800') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r801to900 = useTracker(() => { const handle = Meteor.subscribe('random801to900') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r901to1000 = useTracker(() => { const handle = Meteor.subscribe('random901to1000') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) useEffect(() => { if (isSpamming && !spammerRef.current) { spammerRef.current = setInterval(() => { Meteor.call('echo', 'Echo') }, 100) } else { if (spammerRef.current) { clearInterval(spammerRef.current) spammerRef.current = 0 } } }, [isSpamming]) return (

Welcome to Meteor!

) } ================================================ FILE: devapp-2.0.0/imports/ui/Hello.jsx ================================================ import React, { useState } from 'react' export const Hello = () => { const [counter, setCounter] = useState(0) const increment = () => { setCounter(counter + 1) } return (

You've pressed the button {counter} times.

) } ================================================ FILE: devapp-2.0.0/imports/ui/Info.jsx ================================================ import React from 'react' import { useTracker } from 'meteor/react-meteor-data' import { LinksCollection } from '../api/links' export const Info = () => { const links = useTracker(() => { return LinksCollection.find().fetch() }) return (

Learn Meteor!

) } ================================================ FILE: devapp-2.0.0/package.json ================================================ { "name": "devapp-2.0.0", "private": true, "scripts": { "start": "meteor run", "test": "meteor test --once --driver-package meteortesting:mocha", "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", "visualize": "meteor --production --extra-packages bundle-visualizer" }, "dependencies": { "@babel/runtime": "^7.11.2", "meteor-node-stubs": "^1.0.1", "react": "^16.13.1", "react-dom": "^16.13.1" }, "meteor": { "mainModule": { "client": "client/main.jsx", "server": "server/main.js" }, "testModule": "tests/main.js" } } ================================================ FILE: devapp-2.0.0/server/main.js ================================================ import { Meteor } from 'meteor/meteor' import { LinksCollection } from '../imports/api/links' import { RandomCollection } from '../imports/api/random' function insertLink(title, url) { LinksCollection.insert({ title, url, createdAt: new Date() }) } Meteor.methods({ echo(echo) { return echo }, }) Meteor.startup(() => { if (LinksCollection.find().count() === 0) { insertLink( 'Do the Tutorial', 'https://www.meteor.com/tutorials/react/creating-an-app', ) insertLink('Follow the Guide', 'http://guide.meteor.com') insertLink('Read the Docs', 'https://docs.meteor.com') insertLink('Discussions', 'https://forums.meteor.com') } RandomCollection.remove({}) let counter = 1 new Array(1000) .fill(null) .map(() => ({ name: 'Lorem Ipsum '.concat(String(counter)), number: counter++, })) .forEach(item => { RandomCollection.insert(item) }) }) Meteor.publish('random1to100', function () { return RandomCollection.find({ number: { $gte: 1, $lte: 100 }, }) }) Meteor.publish('random101to200', function () { return RandomCollection.find({ number: { $gte: 101, $lte: 200 }, }) }) Meteor.publish('random201to300', function () { return RandomCollection.find({ number: { $gte: 201, $lte: 300 }, }) }) Meteor.publish('random301to400', function () { return RandomCollection.find({ number: { $gte: 301, $lte: 400 }, }) }) Meteor.publish('random401to500', function () { return RandomCollection.find({ number: { $gte: 401, $lte: 500 }, }) }) Meteor.publish('random501to600', function () { return RandomCollection.find({ number: { $gte: 501, $lte: 600 }, }) }) Meteor.publish('random601to700', function () { return RandomCollection.find({ number: { $gte: 601, $lte: 700 }, }) }) Meteor.publish('random701to800', function () { return RandomCollection.find({ number: { $gte: 701, $lte: 800 }, }) }) Meteor.publish('random801to900', function () { return RandomCollection.find({ number: { $gte: 801, $lte: 900 }, }) }) Meteor.publish('random901to1000', function () { return RandomCollection.find({ number: { $gte: 901, $lte: 1000 }, }) }) ================================================ FILE: devapp-2.0.0/tests/main.js ================================================ import assert from 'assert' describe('devapp-2.0.0', function () { it('package.json has correct name', async function () { const { name } = await import('../package.json') assert.strictEqual(name, 'devapp-2.0.0') }) if (Meteor.isClient) { it('client is not server', function () { assert.strictEqual(Meteor.isServer, false) }) } if (Meteor.isServer) { it('server is not client', function () { assert.strictEqual(Meteor.isClient, false) }) } }) ================================================ FILE: devapp-2.2.0/.gitignore ================================================ node_modules/ ================================================ FILE: devapp-2.2.0/.meteor/.finished-upgraders ================================================ # This file contains information which helps Meteor properly upgrade your # app when you run 'meteor update'. You should check it into version control # with your project. notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file notices-for-facebook-graph-api-2 1.2.0-standard-minifiers-package 1.2.0-meteor-platform-split 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package 1.4.0-remove-old-dev-bundle-link 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package 1.7-split-underscore-from-meteor-base 1.8.3-split-jquery-from-blaze ================================================ FILE: devapp-2.2.0/.meteor/.gitignore ================================================ local ================================================ FILE: devapp-2.2.0/.meteor/.id ================================================ # This file contains a token that is unique to your project. # Check it into your repository along with the rest of this directory. # It can be used for purposes such as: # - ensuring you don't accidentally deploy one app on top of another # - providing package authors with aggregated statistics kckjbl9hqpog.ffkb1f09s7ns ================================================ FILE: devapp-2.2.0/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # Check this file (and the other files in this directory) into your repository. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. meteor-base@1.4.0 # Packages every Meteor app needs to have mobile-experience@1.1.0 # Packages for a great mobile UX mongo@1.11.0 # The database Meteor supports right now reactive-var@1.0.11 # Reactive variable for tracker standard-minifier-css@1.7.2 # CSS minifier run for production mode standard-minifier-js@2.6.0 # JS minifier run for production mode es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers ecmascript@0.15.1 # Enable ECMAScript2015+ syntax in app code typescript@4.2.2 # Enable TypeScript syntax in .ts and .tsx modules shell-server@0.5.0 # Server-side component of the `meteor shell` command hot-module-replacement@0.2.0 # Update client in development without reloading the page insecure@1.0.7 # Allow all DB writes from clients (for prototyping) static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data ================================================ FILE: devapp-2.2.0/.meteor/platforms ================================================ server browser ================================================ FILE: devapp-2.2.0/.meteor/release ================================================ METEOR@2.2 ================================================ FILE: devapp-2.2.0/.meteor/versions ================================================ allow-deny@1.1.0 autoupdate@1.7.0 babel-compiler@7.6.2 babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 callback-hook@1.3.1 check@1.3.1 ddp@1.4.0 ddp-client@2.4.1 ddp-common@1.4.0 ddp-server@2.3.3 diff-sequence@1.1.1 dynamic-import@0.6.0 ecmascript@0.15.1 ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.11.1 ecmascript-runtime-server@0.10.1 ejson@1.1.1 es5-shim@4.8.0 fetch@0.1.1 geojson-utils@1.0.10 hot-code-push@1.0.4 hot-module-replacement@0.2.1 html-tools@1.1.3 htmljs@1.1.1 id-map@1.1.1 insecure@1.0.7 inter-process-messaging@0.1.1 launch-screen@1.2.1 livedata@1.0.18 logging@1.2.0 meteor@1.9.3 meteor-base@1.4.0 minifier-css@1.5.4 minifier-js@2.6.1 minimongo@1.6.2 mobile-experience@1.1.0 mobile-status-bar@1.1.0 modern-browsers@0.1.7 modules@0.16.0 modules-runtime@0.12.0 modules-runtime-hot@0.13.0 mongo@1.11.1 mongo-decimal@0.1.2 mongo-dev-server@1.1.0 mongo-id@1.0.8 npm-mongo@3.9.1 ordered-dict@1.1.0 promise@0.11.2 random@1.2.0 react-fast-refresh@0.1.1 react-meteor-data@2.5.1 reactive-var@1.0.11 reload@1.3.1 retry@1.1.0 routepolicy@1.1.0 shell-server@0.5.0 socket-stream-client@0.3.3 spacebars-compiler@1.3.1 standard-minifier-css@1.7.3 standard-minifier-js@2.6.1 static-html@1.3.2 templating-tools@1.2.2 tracker@1.2.0 typescript@4.2.2 underscore@1.0.10 webapp@1.10.1 webapp-hashing@1.1.0 ================================================ FILE: devapp-2.2.0/client/main.css ================================================ body { padding: 10px; font-family: sans-serif; } ================================================ FILE: devapp-2.2.0/client/main.html ================================================ devapp
================================================ FILE: devapp-2.2.0/client/main.jsx ================================================ import React from 'react' import { Meteor } from 'meteor/meteor' import { render } from 'react-dom' import { App } from '../imports/ui/App' import '../imports/api/links' import '../imports/api/random' Meteor.startup(() => { render(, document.getElementById('react-target')) }) ================================================ FILE: devapp-2.2.0/imports/api/links.js ================================================ import { Mongo } from 'meteor/mongo' export const LinksCollection = new Mongo.Collection('links') ================================================ FILE: devapp-2.2.0/imports/api/random.js ================================================ import { Mongo } from 'meteor/mongo' export const RandomCollection = new Mongo.Collection('random') ================================================ FILE: devapp-2.2.0/imports/ui/App.jsx ================================================ import React, { useEffect, useRef, useState } from 'react' import { useTracker } from 'meteor/react-meteor-data' import { RandomCollection } from '../api/random' export const App = () => { const [isSpamming, setSpamming] = useState(false) const spammerRef = useRef(0) const r1to100 = useTracker(() => { const handle = Meteor.subscribe('random1to100') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r101to200 = useTracker(() => { const handle = Meteor.subscribe('random101to200') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r201to300 = useTracker(() => { const handle = Meteor.subscribe('random201to300') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r301to400 = useTracker(() => { const handle = Meteor.subscribe('random301to400') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r401to500 = useTracker(() => { const handle = Meteor.subscribe('random401to500') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r501to600 = useTracker(() => { const handle = Meteor.subscribe('random501to600') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r601to700 = useTracker(() => { const handle = Meteor.subscribe('random601to700') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r701to800 = useTracker(() => { const handle = Meteor.subscribe('random701to800') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r801to900 = useTracker(() => { const handle = Meteor.subscribe('random801to900') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r901to1000 = useTracker(() => { const handle = Meteor.subscribe('random901to1000') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) useEffect(() => { if (isSpamming && !spammerRef.current) { spammerRef.current = setInterval(() => { Meteor.call('echo', 'Echo') }, 100) } else { if (spammerRef.current) { clearInterval(spammerRef.current) spammerRef.current = 0 } } }, [isSpamming]) return (

Welcome to Meteor!

) } ================================================ FILE: devapp-2.2.0/imports/ui/Hello.jsx ================================================ import React, { useState } from 'react' export const Hello = () => { const [counter, setCounter] = useState(0) const increment = () => { setCounter(counter + 1) } return (

You've pressed the button {counter} times.

) } ================================================ FILE: devapp-2.2.0/imports/ui/Info.jsx ================================================ import React from 'react' import { useTracker } from 'meteor/react-meteor-data' import { LinksCollection } from '../api/links' export const Info = () => { const links = useTracker(() => { return LinksCollection.find().fetch() }) return (

Learn Meteor!

) } ================================================ FILE: devapp-2.2.0/package.json ================================================ { "name": "devapp-2.2.0", "private": true, "scripts": { "start": "meteor run", "test": "meteor test --once --driver-package meteortesting:mocha", "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", "visualize": "meteor --production --extra-packages bundle-visualizer" }, "dependencies": { "@babel/runtime": "^7.11.2", "meteor-node-stubs": "^1.0.1", "react": "^16.13.1", "react-dom": "^16.13.1" }, "meteor": { "mainModule": { "client": "client/main.jsx", "server": "server/main.js" }, "testModule": "tests/main.js" } } ================================================ FILE: devapp-2.2.0/server/main.js ================================================ import { Meteor } from 'meteor/meteor' import { LinksCollection } from '../imports/api/links' import { RandomCollection } from '../imports/api/random' function insertLink(title, url) { LinksCollection.insert({ title, url, createdAt: new Date() }) } Meteor.methods({ echo(echo) { return echo }, }) Meteor.startup(() => { if (LinksCollection.find().count() === 0) { insertLink( 'Do the Tutorial', 'https://www.meteor.com/tutorials/react/creating-an-app', ) insertLink('Follow the Guide', 'http://guide.meteor.com') insertLink('Read the Docs', 'https://docs.meteor.com') insertLink('Discussions', 'https://forums.meteor.com') } RandomCollection.remove({}) let counter = 1 new Array(1000) .fill(null) .map(() => ({ name: 'Lorem Ipsum '.concat(String(counter)), number: counter++, })) .forEach(item => { RandomCollection.insert(item) }) }) Meteor.publish('random1to100', function () { return RandomCollection.find({ number: { $gte: 1, $lte: 100 }, }) }) Meteor.publish('random101to200', function () { return RandomCollection.find({ number: { $gte: 101, $lte: 200 }, }) }) Meteor.publish('random201to300', function () { return RandomCollection.find({ number: { $gte: 201, $lte: 300 }, }) }) Meteor.publish('random301to400', function () { return RandomCollection.find({ number: { $gte: 301, $lte: 400 }, }) }) Meteor.publish('random401to500', function () { return RandomCollection.find({ number: { $gte: 401, $lte: 500 }, }) }) Meteor.publish('random501to600', function () { return RandomCollection.find({ number: { $gte: 501, $lte: 600 }, }) }) Meteor.publish('random601to700', function () { return RandomCollection.find({ number: { $gte: 601, $lte: 700 }, }) }) Meteor.publish('random701to800', function () { return RandomCollection.find({ number: { $gte: 701, $lte: 800 }, }) }) Meteor.publish('random801to900', function () { return RandomCollection.find({ number: { $gte: 801, $lte: 900 }, }) }) Meteor.publish('random901to1000', function () { return RandomCollection.find({ number: { $gte: 901, $lte: 1000 }, }) }) ================================================ FILE: devapp-2.2.0/tests/main.js ================================================ import assert from 'assert' describe('devapp-2.2.0', function () { it('package.json has correct name', async function () { const { name } = await import('../package.json') assert.strictEqual(name, 'devapp-2.2.0') }) if (Meteor.isClient) { it('client is not server', function () { assert.strictEqual(Meteor.isServer, false) }) } if (Meteor.isServer) { it('server is not client', function () { assert.strictEqual(Meteor.isClient, false) }) } }) ================================================ FILE: devapp-2.2.4/.gitignore ================================================ node_modules/ ================================================ FILE: devapp-2.2.4/.meteor/.finished-upgraders ================================================ # This file contains information which helps Meteor properly upgrade your # app when you run 'meteor update'. You should check it into version control # with your project. notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file notices-for-facebook-graph-api-2 1.2.0-standard-minifiers-package 1.2.0-meteor-platform-split 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package 1.4.0-remove-old-dev-bundle-link 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package 1.7-split-underscore-from-meteor-base 1.8.3-split-jquery-from-blaze ================================================ FILE: devapp-2.2.4/.meteor/.gitignore ================================================ local ================================================ FILE: devapp-2.2.4/.meteor/.id ================================================ # This file contains a token that is unique to your project. # Check it into your repository along with the rest of this directory. # It can be used for purposes such as: # - ensuring you don't accidentally deploy one app on top of another # - providing package authors with aggregated statistics azmndnm89g3.mkgtn1ux8hf9 ================================================ FILE: devapp-2.2.4/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # Check this file (and the other files in this directory) into your repository. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. meteor-base@1.4.0 # Packages every Meteor app needs to have mobile-experience@1.1.0 # Packages for a great mobile UX mongo@1.11.0 # The database Meteor supports right now reactive-var@1.0.11 # Reactive variable for tracker standard-minifier-css@1.7.2 # CSS minifier run for production mode standard-minifier-js@2.6.0 # JS minifier run for production mode es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers ecmascript@0.15.3 # Enable ECMAScript2015+ syntax in app code typescript@4.3.5 # Enable TypeScript syntax in .ts and .tsx modules shell-server@0.5.0 # Server-side component of the `meteor shell` command hot-module-replacement@0.2.0 # Update client in development without reloading the page insecure@1.0.7 # Allow all DB writes from clients (for prototyping) static-html # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data ================================================ FILE: devapp-2.2.4/.meteor/platforms ================================================ server browser ================================================ FILE: devapp-2.2.4/.meteor/release ================================================ METEOR@2.2.4 ================================================ FILE: devapp-2.2.4/.meteor/versions ================================================ allow-deny@1.1.0 autoupdate@1.7.0 babel-compiler@7.7.0 babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 blaze-tools@1.1.3 boilerplate-generator@1.7.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 callback-hook@1.3.1 check@1.3.1 ddp@1.4.0 ddp-client@2.4.1 ddp-common@1.4.0 ddp-server@2.3.3 diff-sequence@1.1.1 dynamic-import@0.6.0 ecmascript@0.15.3 ecmascript-runtime@0.7.0 ecmascript-runtime-client@0.11.1 ecmascript-runtime-server@0.10.1 ejson@1.1.1 es5-shim@4.8.0 fetch@0.1.1 geojson-utils@1.0.10 hot-code-push@1.0.4 hot-module-replacement@0.2.1 html-tools@1.1.3 htmljs@1.1.1 id-map@1.1.1 insecure@1.0.7 inter-process-messaging@0.1.1 launch-screen@1.2.1 livedata@1.0.18 logging@1.2.0 meteor@1.9.3 meteor-base@1.4.0 minifier-css@1.5.4 minifier-js@2.6.1 minimongo@1.6.2 mobile-experience@1.1.0 mobile-status-bar@1.1.0 modern-browsers@0.1.7 modules@0.16.0 modules-runtime@0.12.0 modules-runtime-hot@0.13.0 mongo@1.11.1 mongo-decimal@0.1.2 mongo-dev-server@1.1.0 mongo-id@1.0.8 npm-mongo@3.9.1 ordered-dict@1.1.0 promise@0.11.2 random@1.2.0 react-fast-refresh@0.1.1 react-meteor-data@2.5.1 reactive-var@1.0.11 reload@1.3.1 retry@1.1.0 routepolicy@1.1.0 shell-server@0.5.0 socket-stream-client@0.3.3 spacebars-compiler@1.3.1 standard-minifier-css@1.7.3 standard-minifier-js@2.6.1 static-html@1.3.2 templating-tools@1.2.2 tracker@1.2.0 typescript@4.3.5 underscore@1.0.10 webapp@1.10.1 webapp-hashing@1.1.0 ================================================ FILE: devapp-2.2.4/client/main.css ================================================ body { padding: 10px; font-family: sans-serif; } ================================================ FILE: devapp-2.2.4/client/main.html ================================================ devapp
================================================ FILE: devapp-2.2.4/client/main.jsx ================================================ import React from 'react' import { Meteor } from 'meteor/meteor' import { render } from 'react-dom' import { App } from '../imports/ui/App' import '../imports/api/links' import '../imports/api/random' Meteor.startup(() => { render(, document.getElementById('react-target')) }) ================================================ FILE: devapp-2.2.4/imports/api/links.js ================================================ import { Mongo } from 'meteor/mongo' export const LinksCollection = new Mongo.Collection('links') ================================================ FILE: devapp-2.2.4/imports/api/random.js ================================================ import { Mongo } from 'meteor/mongo' export const RandomCollection = new Mongo.Collection('random') ================================================ FILE: devapp-2.2.4/imports/ui/App.jsx ================================================ import React, { useEffect, useRef, useState } from 'react' import { useTracker } from 'meteor/react-meteor-data' import { RandomCollection } from '../api/random' export const App = () => { const [isSpamming, setSpamming] = useState(false) const spammerRef = useRef(0) const r1to100 = useTracker(() => { const handle = Meteor.subscribe('random1to100') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r101to200 = useTracker(() => { const handle = Meteor.subscribe('random101to200') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r201to300 = useTracker(() => { const handle = Meteor.subscribe('random201to300') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r301to400 = useTracker(() => { const handle = Meteor.subscribe('random301to400') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r401to500 = useTracker(() => { const handle = Meteor.subscribe('random401to500') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r501to600 = useTracker(() => { const handle = Meteor.subscribe('random501to600') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r601to700 = useTracker(() => { const handle = Meteor.subscribe('random601to700') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r701to800 = useTracker(() => { const handle = Meteor.subscribe('random701to800') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r801to900 = useTracker(() => { const handle = Meteor.subscribe('random801to900') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) const r901to1000 = useTracker(() => { const handle = Meteor.subscribe('random901to1000') return { isLoading: !handle.ready(), docs: RandomCollection.find({}).fetch(), } }, []) useEffect(() => { if (isSpamming && !spammerRef.current) { spammerRef.current = setInterval(() => { Meteor.call('echo', 'Echo') }, 100) } else { if (spammerRef.current) { clearInterval(spammerRef.current) spammerRef.current = 0 } } }, [isSpamming]) return (

Welcome to Meteor!

) } ================================================ FILE: devapp-2.2.4/imports/ui/Hello.jsx ================================================ import React, { useState } from 'react' export const Hello = () => { const [counter, setCounter] = useState(0) const increment = () => { setCounter(counter + 1) } return (

You've pressed the button {counter} times.

) } ================================================ FILE: devapp-2.2.4/imports/ui/Info.jsx ================================================ import React from 'react' import { useTracker } from 'meteor/react-meteor-data' import { LinksCollection } from '../api/links' export const Info = () => { const links = useTracker(() => { return LinksCollection.find().fetch() }) return (

Learn Meteor!

) } ================================================ FILE: devapp-2.2.4/package.json ================================================ { "name": "devapp-2.2.4", "private": true, "scripts": { "start": "meteor run", "test": "meteor test --once --driver-package meteortesting:mocha", "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", "visualize": "meteor --production --extra-packages bundle-visualizer" }, "dependencies": { "@babel/runtime": "^7.11.2", "meteor-node-stubs": "^1.0.1", "react": "^16.13.1", "react-dom": "^16.13.1" }, "meteor": { "mainModule": { "client": "client/main.jsx", "server": "server/main.js" }, "testModule": "tests/main.js" } } ================================================ FILE: devapp-2.2.4/server/main.js ================================================ import { Meteor } from 'meteor/meteor' import { LinksCollection } from '../imports/api/links' import { RandomCollection } from '../imports/api/random' function insertLink(title, url) { LinksCollection.insert({ title, url, createdAt: new Date() }) } Meteor.methods({ echo(echo) { return echo }, }) Meteor.startup(() => { if (LinksCollection.find().count() === 0) { insertLink( 'Do the Tutorial', 'https://www.meteor.com/tutorials/react/creating-an-app', ) insertLink('Follow the Guide', 'http://guide.meteor.com') insertLink('Read the Docs', 'https://docs.meteor.com') insertLink('Discussions', 'https://forums.meteor.com') } RandomCollection.remove({}) let counter = 1 new Array(1000) .fill(null) .map(() => ({ name: 'Lorem Ipsum '.concat(String(counter)), number: counter++, })) .forEach(item => { RandomCollection.insert(item) }) }) Meteor.publish('random1to100', function () { return RandomCollection.find({ number: { $gte: 1, $lte: 100 }, }) }) Meteor.publish('random101to200', function () { return RandomCollection.find({ number: { $gte: 101, $lte: 200 }, }) }) Meteor.publish('random201to300', function () { return RandomCollection.find({ number: { $gte: 201, $lte: 300 }, }) }) Meteor.publish('random301to400', function () { return RandomCollection.find({ number: { $gte: 301, $lte: 400 }, }) }) Meteor.publish('random401to500', function () { return RandomCollection.find({ number: { $gte: 401, $lte: 500 }, }) }) Meteor.publish('random501to600', function () { return RandomCollection.find({ number: { $gte: 501, $lte: 600 }, }) }) Meteor.publish('random601to700', function () { return RandomCollection.find({ number: { $gte: 601, $lte: 700 }, }) }) Meteor.publish('random701to800', function () { return RandomCollection.find({ number: { $gte: 701, $lte: 800 }, }) }) Meteor.publish('random801to900', function () { return RandomCollection.find({ number: { $gte: 801, $lte: 900 }, }) }) Meteor.publish('random901to1000', function () { return RandomCollection.find({ number: { $gte: 901, $lte: 1000 }, }) }) ================================================ FILE: devapp-2.2.4/tests/main.js ================================================ import assert from 'assert' describe('devapp-2.2.4', function () { it('package.json has correct name', async function () { const { name } = await import('../package.json') assert.strictEqual(name, 'devapp-2.2.4') }) if (Meteor.isClient) { it('client is not server', function () { assert.strictEqual(Meteor.isServer, false) }) } if (Meteor.isServer) { it('server is not client', function () { assert.strictEqual(Meteor.isClient, false) }) } }) ================================================ FILE: devapp-3.4/.gitignore ================================================ node_modules/ # Meteor Modern-Tools build context directories _build */build-assets */build-chunks .rsdoctor ================================================ FILE: devapp-3.4/.meteor/.finished-upgraders ================================================ # This file contains information which helps Meteor properly upgrade your # app when you run 'meteor update'. You should check it into version control # with your project. notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file notices-for-facebook-graph-api-2 1.2.0-standard-minifiers-package 1.2.0-meteor-platform-split 1.2.0-cordova-changes 1.2.0-breaking-changes 1.3.0-split-minifiers-package 1.4.0-remove-old-dev-bundle-link 1.4.1-add-shell-server-package 1.4.3-split-account-service-packages 1.5-add-dynamic-import-package 1.7-split-underscore-from-meteor-base 1.8.3-split-jquery-from-blaze ================================================ FILE: devapp-3.4/.meteor/.gitignore ================================================ local ================================================ FILE: devapp-3.4/.meteor/.id ================================================ # This file contains a token that is unique to your project. # Check it into your repository along with the rest of this directory. # It can be used for purposes such as: # - ensuring you don't accidentally deploy one app on top of another # - providing package authors with aggregated statistics hquoz8fwpx2o.w95c0f55ay3 ================================================ FILE: devapp-3.4/.meteor/packages ================================================ # Meteor packages used by this project, one per line. # Check this file (and the other files in this directory) into your repository. # # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. meteor-base@1.5.2 # Packages every Meteor app needs to have mobile-experience@1.1.2 # Packages for a great mobile UX mongo@2.2.0 # The database Meteor supports right now reactive-var@1.0.13 # Reactive variable for tracker standard-minifier-css@1.10.0 # CSS minifier run for production mode standard-minifier-js@3.2.0 # JS minifier run for production mode es5-shim@4.8.1 # ECMAScript 5 compatibility for older browsers ecmascript@0.17.0 # Enable ECMAScript2015+ syntax in app code typescript@5.9.3 # Enable TypeScript syntax in .ts and .tsx modules shell-server@0.7.0 # Server-side component of the `meteor shell` command hot-module-replacement@0.5.4 # Update client in development without reloading the page static-html@1.5.0 # Define static page content in .html files react-meteor-data # React higher-order component for reactively tracking Meteor data rspack # Integrate Rspack into Meteor for client and server app bundling ================================================ FILE: devapp-3.4/.meteor/platforms ================================================ server browser ================================================ FILE: devapp-3.4/.meteor/release ================================================ METEOR@3.4 ================================================ FILE: devapp-3.4/.meteor/versions ================================================ allow-deny@2.1.0 autoupdate@2.0.1 babel-compiler@7.13.0 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 boilerplate-generator@2.1.0 caching-compiler@2.0.1 callback-hook@1.6.1 check@1.5.0 core-runtime@1.0.0 ddp@1.4.2 ddp-client@3.1.1 ddp-common@1.4.4 ddp-server@3.1.2 diff-sequence@1.1.3 dynamic-import@0.7.4 ecmascript@0.17.0 ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.3 ecmascript-runtime-server@0.11.1 ejson@1.1.5 es5-shim@4.8.1 facts-base@1.0.2 fetch@0.1.6 geojson-utils@1.0.12 hot-code-push@1.0.5 hot-module-replacement@0.5.4 id-map@1.2.0 inter-process-messaging@0.1.2 launch-screen@2.0.1 logging@1.3.6 meteor@2.2.0 meteor-base@1.5.2 minifier-css@2.0.1 minifier-js@3.1.0 minimongo@2.0.5 mobile-experience@1.1.2 mobile-status-bar@1.1.1 modern-browsers@0.2.3 modules@0.20.3 modules-runtime@0.13.2 modules-runtime-hot@0.14.3 mongo@2.2.0 mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 npm-mongo@6.16.1 ordered-dict@1.2.0 promise@1.0.0 random@1.2.2 react-fast-refresh@0.3.0 react-meteor-data@4.0.1 reactive-var@1.0.13 reload@1.3.2 retry@1.1.1 routepolicy@1.1.2 rspack@1.0.0 shell-server@0.7.0 socket-stream-client@0.6.1 standard-minifier-css@1.10.0 standard-minifier-js@3.2.0 static-html@1.5.0 static-html-tools@1.0.0 tools-core@1.0.0 tracker@1.3.4 typescript@5.9.3 webapp@2.1.0 webapp-hashing@1.1.2 zodern:types@1.0.13 ================================================ FILE: devapp-3.4/.swcrc ================================================ { "jsc": { "transform": { "react": { "runtime": "automatic" } } } } ================================================ FILE: devapp-3.4/client/main.css ================================================ body { padding: 10px; font-family: sans-serif; } ================================================ FILE: devapp-3.4/client/main.html ================================================ devapp-3.4
================================================ FILE: devapp-3.4/client/main.jsx ================================================ import { createRoot } from 'react-dom/client' import { Meteor } from 'meteor/meteor' import { App } from '/imports/ui/App' import '/imports/ui/styles.css' Meteor.startup(() => { const container = document.getElementById('react-target') const root = createRoot(container) root.render() }) ================================================ FILE: devapp-3.4/imports/api/links.js ================================================ import { Mongo } from 'meteor/mongo' export const LinksCollection = new Mongo.Collection('links') ================================================ FILE: devapp-3.4/imports/ui/App.jsx ================================================ import { Counter } from './Counter.jsx' import { Header } from './Header.jsx' import { Info } from './Info.jsx' export const App = () => (
) ================================================ FILE: devapp-3.4/imports/ui/Counter.jsx ================================================ import { useState } from 'react' export const Counter = () => { const [counter, setCounter] = useState(0) const increment = () => { setCounter(counter + 1) } return (

You've pressed the button{' '} {counter}{' '} {counter === 1 ? 'time' : 'times'}.

) } ================================================ FILE: devapp-3.4/imports/ui/Header.jsx ================================================ import MeteorLogo from './meteor-logo.svg' export const Header = () => { return (
) } ================================================ FILE: devapp-3.4/imports/ui/Info.jsx ================================================ import { useFind, useSubscribe } from 'meteor/react-meteor-data' import { LinksCollection } from '../api/links' export const Info = () => { const isLoading = useSubscribe('links') const links = useFind(() => LinksCollection.find()) if (isLoading()) { return
Loading...
} return (

Learn Meteor!

) } ================================================ FILE: devapp-3.4/imports/ui/styles.css ================================================ /* this file is imported in client/main.jsx */ :root { /* Colors */ --color-background: hsl(210, 20%, 98%); --color-foreground: hsl(220, 20%, 15%); --color-card: hsl(0, 0%, 100%); --color-primary: hsl(4, 70%, 55%); --color-primary-hover: hsl(4, 70%, 45%); --color-muted: hsl(220, 10%, 50%); --color-border: hsl(220, 14%, 90%); /* Shadows */ --shadow-card: 0 1px 3px 0 hsl(220 20% 15% / 0.04), 0 1px 2px -1px hsl(220 20% 15% / 0.04); --shadow-card-hover: 0 10px 15px -3px hsl(220 20% 15% / 0.08), 0 4px 6px -4px hsl(220 20% 15% / 0.04); /* Spacing */ --spacing-xs: 0.25rem; --spacing-sm: 0.5rem; --spacing-md: 1rem; --spacing-lg: 1.5rem; --spacing-xl: 2rem; --spacing-2xl: 3rem; /* Border radius */ --radius: 0.75rem; --radius-sm: 0.5rem; /* Transitions */ --transition-fast: 150ms ease; --transition-normal: 200ms ease; --transition-slow: 250ms ease; } /* ============ Reset & Base Styles ============ */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: var(--color-background); color: var(--color-foreground); line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { text-decoration: none; color: inherit; } /* ============ Layout ============ */ .page { min-height: 100vh; background-color: var(--color-background); } .container { width: 100%; max-width: 1280px; margin: 0 auto; padding-left: var(--spacing-md); padding-right: var(--spacing-md); } @media (min-width: 768px) { .container { padding-left: var(--spacing-lg); padding-right: var(--spacing-lg); } } /* ============ Header / Navigation ============ */ .header { border-bottom: 1px solid var(--color-border); background-color: var(--color-card); border-radius: var(--radius); } .nav { display: flex; align-items: center; justify-content: space-between; height: 4rem; } .logo-container { display: flex; align-items: center; gap: var(--spacing-sm); } .logo { width: 4rem; height: 4rem; } .logo-text { font-weight: 600; color: var(--color-foreground); display: none; } @media (min-width: 640px) { .logo-text { display: inline; } } .page-title { font-size: 1.25rem; font-weight: 700; color: var(--color-foreground); letter-spacing: -0.025em; } @media (min-width: 768px) { .page-title { font-size: 1.5rem; } } .nav-link { font-size: 0.875rem; font-weight: 500; color: var(--color-primary); transition: opacity var(--transition-fast); } .nav-link:hover { opacity: 0.8; } /* ============ Main Content ============ */ .main { padding-top: var(--spacing-xl); padding-bottom: var(--spacing-xl); } @media (min-width: 768px) { .main { padding-top: var(--spacing-2xl); padding-bottom: var(--spacing-2xl); } } /* ============ Card Component ============ */ .card { background-color: var(--color-card); border-radius: var(--radius); box-shadow: var(--shadow-card); border: 1px solid var(--color-border); } /* ============ Counter Section ============ */ .counter-card { padding: var(--spacing-lg); margin-bottom: 2.5rem; } @media (min-width: 768px) { .counter-card { padding: var(--spacing-xl); margin-bottom: var(--spacing-2xl); } } .counter-content { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-md); } @media (min-width: 640px) { .counter-content { flex-direction: row; } } .counter-text { color: var(--color-muted); text-align: center; } @media (min-width: 640px) { .counter-text { text-align: left; } } .counter-value { font-weight: 600; color: var(--color-foreground); } /* ============ Button Component ============ */ .button { display: inline-flex; align-items: center; justify-content: center; min-width: 120px; padding: 0.625rem 1.5rem; font-size: 0.875rem; font-weight: 500; font-family: inherit; color: white; background-color: var(--color-primary); border: none; border-radius: var(--radius-sm); cursor: pointer; transition: background-color var(--transition-normal), transform var(--transition-fast); } .button:hover { background-color: var(--color-primary-hover); } .button:active { transform: scale(0.98); } .button:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } /* ============ Resources Section ============ */ .section-title { font-size: 1.5rem; font-weight: 700; color: var(--color-foreground); margin-bottom: var(--spacing-lg); letter-spacing: -0.025em; } .resources-grid { list-style-type: none; display: grid; grid-template-columns: 1fr; gap: var(--spacing-md); } @media (min-width: 640px) { .resources-grid { grid-template-columns: repeat(2, 1fr); } } /* ============ Resource Card ============ */ .resource-link { display: block; } .resource-card { padding: 1.25rem; transition: box-shadow var(--transition-slow), transform var(--transition-slow); } .resource-card:hover { box-shadow: var(--shadow-card-hover); transform: translateY(-2px); } .resource-content { display: flex; align-items: center; justify-content: space-between; } .resource-title { font-weight: 500; color: var(--color-foreground); transition: color var(--transition-fast); } .resource-link:hover .resource-title { color: var(--color-primary); } .resource-icon { width: 1rem; height: 1rem; color: var(--color-muted); opacity: 0; transition: opacity var(--transition-fast), color var(--transition-fast); } .resource-link:hover .resource-icon { opacity: 1; color: var(--color-primary); } ================================================ FILE: devapp-3.4/package.json ================================================ { "name": "devapp-3.4", "private": true, "scripts": { "start": "meteor run", "test": "meteor test --once --driver-package meteortesting:mocha", "test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", "visualize": "meteor --production --extra-packages bundle-visualizer" }, "dependencies": { "@babel/runtime": "^7.23.5", "@swc/helpers": "^0.5.17", "meteor-node-stubs": "^1.2.12", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@meteorjs/rspack": "^1.0.0", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", "@rspack/plugin-react-refresh": "^1.4.3", "@svgr/webpack": "^8.1.0", "react-refresh": "^0.17.0" }, "meteor": { "mainModule": { "client": "client/main.jsx", "server": "server/main.js" }, "testModule": "tests/main.js", "modern": true } } ================================================ FILE: devapp-3.4/rspack.config.js ================================================ const { defineConfig } = require('@meteorjs/rspack') /** * Rspack configuration for Meteor projects. * * Provides typed flags on the `Meteor` object, such as: * - `Meteor.isClient` / `Meteor.isServer` * - `Meteor.isDevelopment` / `Meteor.isProduction` * - …and other flags available * * Use these flags to adjust your build settings based on environment. */ module.exports = defineConfig(Meteor => { return { module: { rules: [ // Add support for importing SVGs as React components { test: /\.svg$/i, issuer: /\.[jt]sx?$/, use: ['@svgr/webpack'], }, ], }, } }) ================================================ FILE: devapp-3.4/server/main.js ================================================ import { Meteor } from 'meteor/meteor' import { LinksCollection } from '/imports/api/links' import { Random } from 'meteor/random' async function insertLink({ title, url }) { await LinksCollection.insertAsync({ title, url, createdAt: new Date() }) } Meteor.startup(async () => { // If the Links collection is empty, add some data. if ((await LinksCollection.find().countAsync()) === 0) { await insertLink({ title: 'Do the Tutorial', url: 'https://docs.meteor.com/tutorials/react/', }) await insertLink({ title: 'Follow the Guide', url: 'https://docs.meteor.com/tutorials/application-structure/', }) await insertLink({ title: 'Read the Docs', url: 'https://docs.meteor.com', }) await insertLink({ title: 'Discussions', url: 'https://forums.meteor.com', }) await insertLink({ title: 'Join us on Discord', url: 'https://discord.gg/6mS3wHNg', }) await insertLink({ title: 'Deploying in Galaxy', url: 'https://www.meteor.com/hosting', }) } // We publish the entire Links collection to all clients. // In order to be fetched in real-time to the clients Meteor.publish('links', function () { return LinksCollection.find() }) }) Meteor.methods({ about() { return `This is a Meteor application running React with React Router. this is a generated id: ${Random.id()}` }, }) ================================================ FILE: devapp-3.4/tests/main.js ================================================ import assert from 'assert' describe('devapp-3.4', function () { it('package.json has correct name', async function () { const { name } = await import('../package.json') assert.strictEqual(name, 'devapp-3.4') }) if (Meteor.isClient) { it('client is not server', function () { assert.strictEqual(Meteor.isServer, false) }) } if (Meteor.isServer) { it('server is not client', function () { assert.strictEqual(Meteor.isClient, false) }) } }) ================================================ FILE: eslint.config.mjs ================================================ import js from '@eslint/js' import typescript from '@typescript-eslint/eslint-plugin' import typescriptParser from '@typescript-eslint/parser' import react from 'eslint-plugin-react' import reactHooks from 'eslint-plugin-react-hooks' import prettier from 'eslint-plugin-prettier' import unicorn from 'eslint-plugin-unicorn' import globals from 'globals' export default [ { ignores: [ 'node_modules/**', 'extension/**', 'devapp-*/**', '.yarn/**', 'webpack/**', ], }, // Base config for all files { files: ['**/*.{js,jsx,mjs,cjs}'], plugins: { react, 'react-hooks': reactHooks, prettier, unicorn, }, languageOptions: { ecmaVersion: 2020, sourceType: 'module', parser: typescriptParser, parserOptions: { ecmaFeatures: { jsx: true, }, }, globals: { ...globals.browser, ...globals.node, ...globals.mocha, Meteor: 'readonly', Helene: 'readonly', }, }, settings: { react: { version: 'detect', }, }, rules: { ...js.configs.recommended.rules, ...react.configs.recommended.rules, ...unicorn.configs.recommended.rules, 'no-console': 0, 'react/prop-types': 0, 'react/jsx-curly-spacing': 0, 'react/display-name': 0, 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 0, 'no-inner-declarations': 0, 'react/no-unescaped-entities': 0, 'react/react-in-jsx-scope': 0, // Unicorn adjustments for this project 'unicorn/prevent-abbreviations': 0, 'unicorn/filename-case': 0, 'unicorn/no-null': 0, 'unicorn/prefer-module': 0, 'unicorn/prefer-node-protocol': 0, 'prettier/prettier': 'error', }, }, // TypeScript specific config { files: ['**/*.{ts,tsx}'], plugins: { '@typescript-eslint': typescript, react, 'react-hooks': reactHooks, prettier, unicorn, }, languageOptions: { ecmaVersion: 2020, sourceType: 'module', parser: typescriptParser, parserOptions: { ecmaFeatures: { jsx: true, }, }, globals: { ...globals.browser, ...globals.node, ...globals.mocha, Meteor: 'readonly', Helene: 'readonly', }, }, settings: { react: { version: 'detect', }, }, rules: { ...js.configs.recommended.rules, ...typescript.configs.recommended.rules, ...react.configs.recommended.rules, ...unicorn.configs.recommended.rules, 'no-console': 0, 'react/prop-types': 0, 'react/jsx-curly-spacing': 0, 'react/display-name': 0, 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 0, 'no-inner-declarations': 0, 'react/no-unescaped-entities': 0, 'react/react-in-jsx-scope': 0, '@typescript-eslint/no-non-null-assertion': 0, '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/no-empty-interface': 0, '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-unused-vars': 0, '@typescript-eslint/no-this-alias': 0, '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/no-namespace': 0, 'no-undef': 0, // TypeScript handles this // Unicorn adjustments for this project 'unicorn/prevent-abbreviations': 0, 'unicorn/filename-case': 0, 'unicorn/no-null': 0, 'unicorn/prefer-module': 0, 'unicorn/prefer-node-protocol': 0, 'prettier/prettier': 'error', }, }, ] ================================================ FILE: extension/devtools-panel.html ================================================ Panel
================================================ FILE: extension/devtools.html ================================================ Developer Tools ================================================ FILE: extension/manifest-v2.json ================================================ { "manifest_version": 2, "name": "Meteor DevTools Evolved", "description": "The Meteor framework development tool belt, evolved.", "version": "1.8.1", "author": "Leonardo Venturini", "icons": { "16": "icons/meteor-16.png", "48": "icons/meteor-48.png", "128": "icons/meteor-128.png" }, "browser_action": { "default_title": "Meteor" }, "background": { "scripts": ["/dist/background.js"], "persistent": false }, "content_scripts": [ { "matches": [""], "js": ["/dist/content.js"], "run_at": "document_start", "all_frames": true } ], "permissions": [ "https://api.github.com/*", "https://www.google-analytics.com/*", "tabs" ], "content_security_policy": "script-src 'self'; object-src 'self'", "web_accessible_resources": ["/dist/inject.js"], "devtools_page": "devtools.html" } ================================================ FILE: extension/manifest-v3.json ================================================ { "manifest_version": 3, "name": "Meteor DevTools Evolved", "description": "The Meteor framework development tool belt, evolved.", "version": "1.8.1", "author": "Leonardo Venturini", "icons": { "16": "icons/meteor-16.png", "48": "icons/meteor-48.png", "128": "icons/meteor-128.png" }, "action": { "default_title": "Meteor", "default_icon": "icons/meteor-48.png" }, "background": { "service_worker": "/dist/background.js" }, "content_scripts": [ { "matches": [""], "js": ["/dist/content.js"], "run_at": "document_start", "all_frames": true } ], "host_permissions": [ "https://api.github.com/*", "https://www.google-analytics.com/*" ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, "web_accessible_resources": [ { "resources": ["/dist/inject.js"], "matches": ["*://*/*"] } ], "devtools_page": "devtools.html" } ================================================ FILE: extension/options.html ================================================ Options
================================================ FILE: extension/popup.html ================================================ Popup ================================================ FILE: lint-staged.js ================================================ module.exports = { '*.{js,jsx,ts,tsx}': [ 'eslint', 'react-scripts test --bail --watchAll=false --findRelatedTests --passWithNoTests', () => 'tsc-files --noEmit', ], '*.{js,jsx,ts,tsx,json,css,js}': ['prettier --write'], } ================================================ FILE: package.json ================================================ { "name": "meteor-devtools-evolved", "version": "1.8.1", "description": "Meteor DevTools Evolved", "repository": "https://github.com/leonardoventurini/meteor-devtools-evolved", "packageManager": "yarn@4.12.0", "keywords": [ "meteor", "ddp", "devtools" ], "scripts": { "setup": "cd devapp-3.4 && npm install && cd ../ && yarn", "devapp": "cd devapp-3.4 && npm start", "build:chrome": "webpack --config webpack/chrome.prod.js", "build:firefox": "webpack --config webpack/firefox.prod.js", "dev:chrome": "run-p build:chrome devapp open:chrome", "dev:firefox": "run-p build:firefox devapp open:firefox", "dev": "yarn dev:chrome", "wait:firefox": "wait-on extension/firefox/manifest.json http://localhost:2100", "wait:chrome": "wait-on extension/chrome/manifest.json http://localhost:2100", "open:firefox": "yarn wait:firefox && web-ext run --start-url \"http://localhost:2100\" --source-dir ./extension/firefox/ --browser-console", "open:chrome": "yarn wait:chrome && web-ext run -t chromium --start-url \"http://localhost:2100\" --source-dir ./extension/chrome/ --browser-console", "clean": "rimraf extension/firefox extension/chrome", "lint": "eslint .", "audit": "yarn audit --all --recursive --severity high" }, "author": "Leonardo Venturini", "license": "MIT", "dependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.0", "@babel/preset-react": "^7.9.1", "@blueprintjs/core": "4.14.1", "@blueprintjs/icons": "4.12.1", "@blueprintjs/popover2": "^1.11.1", "@heroicons/react": "^2.0.13", "@types/chrome": "0.0.178", "@types/classnames": "^2.2.10", "@types/luxon": "^2.0.5", "@types/meteor": "^2.0.4", "@types/react": "^17.0.27", "@types/react-dom": "^17.0.9", "@types/react-json-tree": "^0.13.0", "@types/react-window": "^1.8.1", "@types/react-window-infinite-loader": "^1.0.3", "@types/styled-components": "^5.1.0", "babel-loader": "^9.1.0", "classnames": "2.3.1", "clean-webpack-plugin": "^4.0.0", "css-loader": "^6.3.0", "d3-collection": "^1.0.7", "d3-hierarchy": "^3.0.1", "d3-selection": "^3.0.0", "d3-shape": "^3.0.1", "daisyui": "^2.15.2", "dexie": "3.2.2", "lodash.debounce": "^4.0.8", "lodash.memoize": "^4.1.2", "lodash.sortby": "^4.7.0", "lodash.throttle": "^4.1.1", "luxon": "2.5.2", "mobx": "6.4.0", "mobx-react-lite": "3.3.0", "normalize.css": "8.0.1", "polished": "4.1.4", "postcss-loader": "^7.0.0", "pretty-bytes": "6.0.0", "react": "17.0.2", "react-dom": "17.0.2", "react-is": "17.0.2", "react-singleton-hook": "^3.2.1", "react-window": "1.8.6", "react-window-infinite-loader": "1.0.7", "sass": "^1.51.0", "sass-loader": "^12.1.0", "style-loader": "^3.3.0", "styled-components": "5.3.3", "tailwindcss": "^3.0.24", "terser-webpack-plugin": "^5.2.4", "ts-loader": "^9.2.6", "tslib": "^2.3.1", "typescript": "^4.4.3", "uuid": "^8.3.2", "webpack": "^5.76.0", "webpack-cli": "^4.9.0", "webpack-merge": "^5.8.0" }, "volta": { "node": "24.13.0", "yarn": "4.12.0" }, "resolutions": { "@babel/traverse": "^7.23.2", "axios": "^1.6.0", "braces": "^3.0.3", "cross-spawn": "^7.0.5", "fast-json-patch": "^3.1.1", "form-data": "^4.0.0", "http-cache-semantics": "^4.1.1", "json5": "^2.2.3", "jsonwebtoken": "^9.0.0", "jws": "^4.0.0", "loader-utils": "^3.2.1", "node-forge": "^1.3.2", "qs": "^6.14.1", "semver": "^7.5.4", "sha.js": "^2.4.12", "ws": "^8.17.1" }, "devDependencies": { "@eslint/js": "^9.0.0", "@types/webextension-polyfill": "^0.9.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "concurrently": "^7.2.2", "copy-webpack-plugin": "^11.0.0", "eslint": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-unicorn": "^56.0.0", "globals": "^15.0.0", "npm-run-all": "^4.1.5", "prettier": "^3.0.0", "prettier-plugin-tailwindcss": "^0.6.0", "wait-on": "^6.0.1", "web-ext": "^7.1.0", "webextension-polyfill": "^0.9.0" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, }, } ================================================ FILE: src/Analytics.ts ================================================ import { exists } from './Utils' import { v4 as uuid } from 'uuid' import { isString } from './Utils/StringUtils' const GA_HOST = 'https://www.google-analytics.com' type UUID = string type RequestObject = { method?: string body?: string headers?: Record } type EventOptions = { clientId?: UUID label?: string value?: number } type EventParams = { ec?: string ea?: string el?: string ev?: number } type PageViewParams = { dp?: string dh?: string dt?: string sc?: number } type ScreenParams = { an?: string av?: string cd?: string aiid?: string aid?: string } type TransactionOptions = { affiliation?: string revenue?: number shipping?: number tax?: number currencyCode?: string } type TransactionParams = { ti?: string ta?: string tr?: number ts?: number tt?: number cu?: string } type TimingOptions = { label?: string dns?: number pageDownTime?: number redirectTime?: number tcpConnectionTime?: number serverResponseTime?: number } type TimingParams = { utc?: string utv?: string utt?: number dns?: number utl?: string pdt?: number rrt?: number tcp?: number srt?: number } type AnalyticsOptions = { userAgent?: string debug?: boolean version?: number clientId?: string } export class Analytics { clientId = uuid() customParams = {} globalDebug = false globalUserAgent = '' globalBaseURL = GA_HOST globalDebugURL = '/debug' globalCollectURL = '/collect' globalBatchURL = '/batch' globalTrackingID: string globalVersion = 1 constructor(trackingId, options: AnalyticsOptions = {}) { const { clientId, userAgent, debug = false, version = 1 } = options if (clientId) this.clientId = clientId if (userAgent) this.globalUserAgent = userAgent this.globalDebug = debug this.globalTrackingID = trackingId this.globalVersion = version this.customParams = {} } set(key: string, value = null) { if (value === null) { delete this.customParams[key] } else { this.customParams[key] = value } } pageView( hostname: string = location?.hostname, path: string = location?.pathname, title: string = document?.title, sessionDuration?: number, ) { const params: PageViewParams = { dh: hostname, dp: path, dt: title, } if (exists(sessionDuration)) { params.sc = sessionDuration } return this.send('pageview', params) } event(category?: string, action?: string, options: EventOptions = {}) { const { label, value } = options const params: EventParams = { ec: category, ea: action } if (label) params.el = label if (value) params.ev = value this.send('event', params).catch(console.error) } screen( appName: string, appVersion: string, appId: string, appInstallerId: string, screenName: string, ) { const params: ScreenParams = { an: appName, av: appVersion, aid: appId, aiid: appInstallerId, cd: screenName, } return this.send('screenview', params) } transaction(transactionId: UUID, options: TransactionOptions = {}) { const { affiliation, revenue, shipping, tax, currencyCode } = options const params: TransactionParams = { ti: transactionId } if (affiliation) params.ta = affiliation if (revenue) params.tr = revenue if (shipping) params.ts = shipping if (tax) params.tt = tax if (currencyCode) params.cu = currencyCode return this.send('transaction', params) } social(socialAction: string, socialNetwork: string, socialTarget: string) { const params = { sa: socialAction, sn: socialNetwork, st: socialTarget } return this.send('social', params) } exception(description: string, fatal: number, clientId: UUID) { const params = { exd: description, exf: fatal } return this.send('exception', params) } timingTrk( timingCategory: string, timingVariable: string, timingTime: number, options: TimingOptions, ) { const { label, dns, pageDownTime, redirectTime, tcpConnectionTime, serverResponseTime, } = options const params: TimingParams = { utc: timingCategory, utv: timingVariable, utt: timingTime, } if (label) params.utl = label if (dns) params.dns = dns if (pageDownTime) params.pdt = pageDownTime if (redirectTime) params.rrt = redirectTime if (tcpConnectionTime) params.tcp = tcpConnectionTime if (serverResponseTime) params.srt = serverResponseTime return this.send('timing', params) } send(hitType: string, params: Record) { const payload = { v: this.globalVersion, tid: this.globalTrackingID, cid: this.clientId, t: hitType, } if (params) Object.assign(payload, params) if (Object.keys(this.customParams).length > 0) { Object.assign(payload, this.customParams) } let url = `${this.globalBaseURL}${this.globalCollectURL}` if (this.globalDebug) { url = `${this.globalBaseURL}${this.globalDebugURL}${this.globalCollectURL}` } const requestObject: RequestObject = { method: 'post', body: Object.keys(payload) .map(key => `${encodeURI(key)}=${encodeURI(payload[key])}`) .join('&'), } if (this.globalUserAgent && isString(this.globalUserAgent)) { requestObject.headers = { 'User-Agent': this.globalUserAgent } } return fetch(url, requestObject) .then(res => { let response = {} response = res.headers.get('content-type') === 'image/gif' ? res.text() : res.json() if (res.status === 200) { return response } throw new Error(response as string) }) .then((json: any) => { if (this.globalDebug && json.hitParsingResult[0].valid) { return { clientId: payload.cid } } return { clientId: payload.cid } }) .catch(error => new Error(error)) } } ================================================ FILE: src/App.tsx ================================================ import { FocusStyleManager } from '@blueprintjs/core' import React from 'react' import { render } from 'react-dom' import { Options } from './Pages/Options' import { Panel } from './Pages/Panel' import { Popup } from './Pages/Popup' import './Styles/Tailwind.css' import './Styles/App.scss' FocusStyleManager.onlyShowFocusOnTabs() const panelElement = document.querySelector('#panel') const optionsElement = document.querySelector('#options') const popupElement = document.querySelector('#popup') if (panelElement) render(, panelElement) if (optionsElement) render(, optionsElement) if (popupElement) render(, popupElement) ================================================ FILE: src/AppToaster.jsx ================================================ import { Position, Toaster } from '@blueprintjs/core' export const AppToaster = Toaster.create({ className: 'app-toaster', position: Position.TOP, }) ================================================ FILE: src/Bridge.ts ================================================ import { detectType } from '@/Pages/Panel/DDP/FilterConstants' import prettyBytes from 'pretty-bytes' import { PanelStore } from '@/Stores/PanelStore' import { DateTime } from 'luxon' import { StringUtils } from '@/Utils/StringUtils' import browser from 'webextension-polyfill' export const syncSubscriptions = () => Bridge.sendContentMessage({ eventType: 'sync-subscriptions', data: null, }) export const syncStats = () => Bridge.sendContentMessage({ eventType: 'stats', data: null, }) export const clearCache = () => Bridge.sendContentMessage({ eventType: 'cache:clear', data: null, }) export const Bridge = new (class { private handlers: Partial> = {} register(eventType: EventType, handler: MessageHandler) { this.handlers[eventType] = handler } handle(message: Message) { if (message.eventType in this.handlers) { const handler = this.handlers[message.eventType] if (handler) handler(message) } } sendContentMessage(message: Message) { const payload: IMessagePayload = { ...message, source: 'meteor-devtools-evolved', } if (browser && browser.devtools) { browser.devtools.inspectedWindow.eval( `__meteor_devtools_evolved_receiveMessage(${JSON.stringify(payload)})`, ) } } chrome() { const backgroundConnection = browser.runtime.connect({ name: 'panel', }) backgroundConnection.postMessage({ name: 'init', tabId: browser.devtools.inspectedWindow.tabId, }) backgroundConnection.onMessage.addListener((message: Message) => Bridge.handle(message), ) } init() { console.log('Setting up bridge...') if (!browser || !browser.devtools) return // FIXME : Need to confirm if using `chrome` instead of `browser` breaking any communication this.chrome() syncStats() } })() Bridge.register('ddp-event', (message: Message) => { const size = StringUtils.getSize(message.data.content) const parsedContent = JSON.parse(message.data.content) const filterType = detectType(parsedContent) const log = { ...message.data, parsedContent, timestampPretty: message.data.timestamp ? DateTime.fromMillis(message.data.timestamp).toFormat('HH:mm:ss.SSS') : '', timestampLong: message.data.timestamp ? DateTime.fromMillis(message.data.timestamp).toLocaleString( DateTime.DATETIME_FULL, ) : '', size, sizePretty: prettyBytes(size), filterType, } if (filterType === 'subscription') { syncSubscriptions() } PanelStore.ddpStore.pushItem(log) }) Bridge.register( 'minimongo-get-collections', (message: Message) => { PanelStore.minimongoStore.setCollections(message.data) }, ) Bridge.register('sync-subscriptions', (message: Message) => { PanelStore.syncSubscriptions(JSON.parse(message.data.subscriptions)) }) Bridge.register('stats', (message: Message) => { console.log(message.data) PanelStore.setGitCommitHash(message.data.gitCommitHash) }) Bridge.register('meteor-data-performance', (message: Message) => { PanelStore.performanceStore.push(message.data) }) ================================================ FILE: src/Browser/Background.ts ================================================ import browser from 'webextension-polyfill' type Connection = Map declare global { interface Window { connections: Connection } } const Cache = new Map() const connections: Connection = new Map() globalThis.connections = connections const panelListener = () => { browser.runtime.onConnect.addListener(port => { console.debug('runtime.onConnect', port) port.onMessage.addListener(request => { console.debug('port.onMessage', request) if (request.name === 'init') { connections.set(request.tabId, port) // Pick things from cache and send it to the panel. if (Cache.has(request.tabId)) { for (const message of Cache.get(request.tabId)) { port.postMessage(message) } } port.onDisconnect.addListener(() => { connections.delete(request.tabId) }) } }) }) } const tabRemovalListener = () => { browser.tabs.onRemoved.addListener(tabId => { console.debug('tabs.onRemoved', tabId) if (connections.has(tabId)) { connections.delete(tabId) Cache.delete(tabId) } }) } // For cross-browser support const action = browser.browserAction || browser.action action.onClicked.addListener(e => { console.debug('action.onClicked', e) browser.tabs .create({ url: 'http://cloud.meteor.com/?utm_source=chrome_extension&utm_medium=extension&utm_campaign=meteor_devtools_evolved', }) .catch(console.error) }) const handleConsole = ( tabId: number, { data: { type, message } }: Message<{ type: ConsoleType; message: string }>, ) => { if (type in console) { console[type](`[${tabId}]`, message) } else { console.warn('Wrong console type.') } } const contentListener = () => { // @ts-ignore browser.runtime.onMessage.addListener((request, sender, sendResponse) => { setTimeout(() => { const tabId = sender?.tab?.id if (!tabId) return // The message event has to from the panel to the content and then through here. if (request?.eventType === 'cache:clear') { console.debug('clear cache') Cache.delete(tabId) return } if (request?.eventType === 'console') { handleConsole(tabId, request) return } if (Cache.has(tabId)) { const entry = Cache.get(tabId) if (entry.length >= 10_000) { entry.shift() } entry.push(request) } else { Cache.set(tabId, [request]) } if (connections.has(tabId)) { connections.get(tabId).postMessage(request) } }, 0) sendResponse() }) } const tabListener = () => { const tabEvent = { 'create-tab': request => browser.tabs .create({ url: request.data.url, }) .catch(console.error), } /** * @issue https://stackoverflow.com/a/73836810/10567157 */ chrome.runtime.onMessage.addListener( function (request, sender, sendResponse) { sendResponse({ foo: true }) if (request.source !== 'meteor-devtools-evolved') return true tabEvent[request.eventType]?.(request) return true }, ) } panelListener() tabRemovalListener() contentListener() tabListener() ================================================ FILE: src/Browser/Content.ts ================================================ import browser from 'webextension-polyfill' const messageHandler = (event: MessageEvent) => { // Only accept messages from same frame if (event.source !== globalThis) return // Only accept messages that we know are ours if (event.data.source !== 'meteor-devtools-evolved') return browser.runtime.sendMessage(event.data).catch(() => { // Cleans up and prevent "context invalidated" errors. window.removeEventListener('message', messageHandler) }) } window.addEventListener('message', messageHandler) const url = browser.runtime.getURL('/dist/inject.js') const script = document.createElement('script') script.setAttribute('type', 'text/javascript') script.setAttribute('src', url) document.documentElement.prepend(script) ================================================ FILE: src/Browser/DevTools.ts ================================================ import browser from 'webextension-polyfill' import { checkFirefoxBrowser } from '@/Utils' const isFirefox = checkFirefoxBrowser() browser.devtools.panels.create( `${isFirefox ? '' : '☄️'} Meteor`, '', 'devtools-panel.html', ) ================================================ FILE: src/Browser/Inject.ts ================================================ import { DDPInjector } from '@/Injectors/DDPInjector' import { MinimongoInjector, updateCollections, } from '@/Injectors/MinimongoInjector' import { MeteorAdapter } from '@/Injectors/MeteorAdapter' const isFrame = (function () { try { return globalThis.self !== window.top } catch { return true } })() const PARENTHESIS_REGEX = /(\S*) \(([^)]+)\)/ export const sendMessage = (eventType: EventType, data: object) => { window.postMessage( { eventType, data, source: 'meteor-devtools-evolved', } as Message, '*', ) } const warning = (message: string) => { sendMessage('console', { type: 'info', message, } as { type: ConsoleType; message: string }) } /** * @todo Do nothing here, and run any stack trace processing logic inside the extension, so if any errors happen it happens in the sandbox console. */ const getStackTrace = (stackTraceLimit: number) => { const originalStackTraceLimit = Error.stackTraceLimit try { Error.stackTraceLimit = stackTraceLimit const error = new Error('Stack trace') if (!error.stack) return [] return error?.stack ?.split('\n') .map(trace => { const matches = PARENTHESIS_REGEX.exec(trace) if (!matches) return null return { callee: matches?.[1], url: matches?.[2], } }) .filter(Boolean) } finally { Error.stackTraceLimit = originalStackTraceLimit } } export const sendLogMessage = (message: DDPLog) => { const stackTrace = getStackTrace(15) if (stackTrace && stackTrace.length > 0) { stackTrace.splice(0, 2) } sendMessage('ddp-event', { ...message, trace: stackTrace, host: location.host, }) if ( message.content !== '{"msg":"ping"}' && message.content !== '{"msg":"pong"}' ) updateCollections() } type MessageHandler = (message: Message) => void type Registration = { eventType: EventType handler: MessageHandler } interface IRegistry { subscriptions: Registration[] register(eventType: EventType, handler: MessageHandler): void run(message: Message): void } export const Registry: IRegistry = { subscriptions: [], register(eventType: EventType, handler: MessageHandler) { this.subscriptions.push({ eventType, handler, }) }, run(message: IMessagePayload) { for (const { eventType, handler } of this.subscriptions) { if ( message.source === 'meteor-devtools-evolved' && eventType === message.eventType ) { handler(message) } } }, } export function injectAll() { if (!globalThis.__meteor_devtools_evolved) { if (isFrame) return false warning( isFrame ? `Initializing from iframe "${location.href}"...` : 'Initializing on the main page...', ) let attempts = 100 let interval = null function inject() { --attempts if (typeof Meteor === 'object' && !globalThis.__meteor_devtools_evolved) { globalThis.__meteor_devtools_evolved = true DDPInjector() MinimongoInjector() MeteorAdapter() globalThis.__meteor_devtools_evolved_receiveMessage = Registry.run.bind(Registry) warning(`Initialized. Attempts: ${100 - attempts}.`) } if (attempts === 0) { clearInterval(interval) if (!globalThis.Meteor) { warning( isFrame ? `Unable to find Meteor on iframe "${location.href}"` : 'Unable to find Meteor on the main page.', ) } } } inject() interval = globalThis.setInterval(inject, 10) } } injectAll() ================================================ FILE: src/Browser/MeteorLibrary.ts ================================================ import { JSONUtils } from '@/Utils/JSONUtils' import { mapValues, omit } from '@/Utils/Objects' export const getSubscriptions = () => { const payload = mapValues( Meteor?.connection?._subscriptions ?? {}, (value: any) => omit(value, ['connection', 'readyDeps']), ) return JSONUtils.stringify(payload) } ================================================ FILE: src/Components/Button.tsx ================================================ import React, { ButtonHTMLAttributes, FunctionComponent } from 'react' import styled from 'styled-components' import { Icon, IconName, Intent } from '@blueprintjs/core' import { centerItems, truncate } from '@/Styles/Mixins' import classnames from 'classnames' import { isNumber, isString } from 'lodash' import { Popover2 } from '@blueprintjs/popover2' const ButtonWrapper = styled.button` ${centerItems}; cursor: pointer; position: relative; overflow: hidden; background: transparent; border: none; color: #eee; font-size: 1rem; padding: 0 8px; .icon + span { margin-left: 4px; } &.warning { background-color: rgba(217, 130, 43, 0.25); color: #ffb366; &:hover { background-color: rgba(217, 130, 43, 0.25); } &:active { background-color: rgba(217, 130, 43, 0.1); } } &:hover:not([disabled], .warning) { background-color: rgba(0, 0, 0, 0.2); } &[disabled] { cursor: not-allowed; } &.shine { &:before { content: ''; display: block; position: absolute; background: rgba(255, 255, 255, 0.5); width: 60px; height: 100%; left: 0; top: 0; opacity: 0.5; filter: blur(30px); transform: translateX(-100px) skewX(-15deg); } &:after { content: ''; display: block; position: absolute; background: rgba(255, 255, 255, 0.2); width: 30px; height: 100%; left: 30px; top: 0; opacity: 0; filter: blur(5px); transform: translateX(-100px) skewX(-15deg); } &:hover:before { transform: translateX(300px) skewX(-15deg); opacity: 0.6; transition: 1.5s; } &:hover:after { transform: translateX(300px) skewX(-15deg); opacity: 1; transition: 1.5s; } } .button-wrapper { display: flex; align-items: center; width: 100%; span.content { flex-grow: 1; ${truncate}; text-align: left; } span.subtitle { flex-shrink: 0; flex-grow: 1; font-size: 10px; color: #ccc; margin-left: auto; text-align: right; } } ` interface Props extends ButtonHTMLAttributes { icon?: IconName | JSX.Element intent?: Intent shine?: boolean active?: boolean subtitle?: string } export const Button: FunctionComponent = ({ icon, children, intent, className, shine, active, subtitle, title, ...rest }) => { const classes = classnames( { shine, active, warning: intent === 'warning', }, className, 'h-full', ) if (title) { return ( {title}} interactionKind='hover' className='inline-flex items-center' >
{icon && (isString(icon) ? ( ) : ( icon ))} {(children || isNumber(children)) && ( {children} )} {(subtitle || isNumber(subtitle)) && ( {subtitle} )}
) } return (
{icon && (isString(icon) ? ( ) : ( icon ))} {(children || isNumber(children)) && ( {children} )} {(subtitle || isNumber(subtitle)) && ( {subtitle} )}
) } ================================================ FILE: src/Components/Field.tsx ================================================ import React, { FunctionComponent } from 'react' import styled from 'styled-components' import { centerItems } from '@/Styles/Mixins' import { Icon, IconName } from '@blueprintjs/core' import { exists } from '@/Utils' import classnames from 'classnames' const Wrapper = styled.span` ${centerItems}; height: 100%; padding: 0 8px; .icon + span { margin-left: 4px; } &.warning { background-color: rgba(217, 130, 43, 0.25); color: #ffb366; } ` interface Props { icon?: IconName intent?: 'warning' className?: string } export const Field: FunctionComponent = ({ children, icon, className, intent, }) => { const classes = classnames( { warning: intent === 'warning', }, className, ) return ( {icon && } {exists(children) && {children}} ) } ================================================ FILE: src/Components/PopoverButton.tsx ================================================ import React, { FunctionComponent } from 'react' import { IconName } from '@blueprintjs/core' import { Button } from '@/Components/Button' import styled from 'styled-components' import { Popover2, Popover2Props } from '@blueprintjs/popover2' interface WrapperProps { height: number } const Wrapper = styled.span` button.popover-button { display: inline-block; height: ${(props: WrapperProps) => props.height}px; } ` interface Props extends Popover2Props { icon: IconName height?: number } export const PopoverButton: FunctionComponent = ({ icon, children, height = 28, ...rest }) => ( ) ================================================ FILE: src/Components/Separator.tsx ================================================ import React, { FunctionComponent } from 'react' import styled from 'styled-components' interface WrapperProps { horizontal?: boolean } const Wrapper = styled.div` width: ${({ horizontal }: WrapperProps) => (horizontal ? undefined : '1px')}; height: ${({ horizontal }: WrapperProps) => (horizontal ? '1px' : undefined)}; margin: 0 3px; background-color: rgba(0, 0, 0, 0.05); ` export const Separator: FunctionComponent = props => ( ) ================================================ FILE: src/Components/StatusBar.tsx ================================================ import React, { FunctionComponent } from 'react' import styled from 'styled-components' import { NAVBAR_HEIGHT } from '@/Styles/Constants' import { lighten } from 'polished' import { centerItems } from '@/Styles/Mixins' const backgroundColor = '#202b33' const Wrapper = styled.div` user-select: none; display: flex; box-sizing: border-box; flex-direction: row; height: ${NAVBAR_HEIGHT}px; width: 100%; background-color: ${backgroundColor}; button { height: 100%; flex: 1 1 auto; &:hover { background-color: ${lighten(0.05, backgroundColor)}; } } .left-group, .right-group { ${centerItems}; } .right-group { margin-left: auto; } & > * + * { margin-left: 8px; } ` export const StatusBar: FunctionComponent = ({ children }) => ( {children} ) ================================================ FILE: src/Components/TabBar.tsx ================================================ import React, { FunctionComponent, useState } from 'react' import styled from 'styled-components' import { IconName, Menu, MenuItem, Position } from '@blueprintjs/core' import classnames from 'classnames' import { Button } from './Button' import { lighten } from 'polished' import { NAVBAR_HEIGHT } from '@/Styles/Constants' import { useBreakpoints } from '@/Utils/Hooks/useBreakpoints' import { Popover2 } from '@blueprintjs/popover2' const backgroundColor = '#202b33' const TabBarWrapper = styled.div` user-select: none; display: flex; box-sizing: border-box; flex-direction: row; height: ${NAVBAR_HEIGHT}px; width: 100%; border-bottom: 1px solid ${lighten(0.1, backgroundColor)}; background-color: ${backgroundColor}; button.mde-tab { &.active { background-color: ${lighten(0.1, backgroundColor)}; } &:hover:not(.active) { background-color: ${lighten(0.05, backgroundColor)}; } } .right-menu { display: flex; flex-direction: row; margin-left: auto; button.menu-item { &:hover { background-color: ${lighten(0.05, backgroundColor)}; } .bp3-icon { margin-bottom: 2px; } } } ` export interface ITab { key: string content: JSX.Element | string icon: IconName shine?: boolean handler?: () => void } export interface IMenuItem { key: string content?: JSX.Element | string icon?: IconName | JSX.Element shine?: boolean handler: () => void title?: string } interface Props { tabs: ITab[] menu?: IMenuItem[] onChange?: (key: string) => void } export const TabBar: FunctionComponent = ({ tabs, menu, onChange }) => { const [activeKey, setKey] = useState(tabs[0].key) const { navigationCollapse } = useBreakpoints() const rightMenu = navigationCollapse ? ( {menu?.map(item => ( ))} } position={Position.BOTTOM_LEFT} > )) ) return ( {tabs.map(tab => ( ))}
{rightMenu}
) } ================================================ FILE: src/Components/TextInput.tsx ================================================ import React, { FunctionComponent, InputHTMLAttributes } from 'react' import styled from 'styled-components' import { Icon, IconName } from '@blueprintjs/core' import { centerItems } from '@/Styles/Mixins' const Wrapper = styled.div` ${centerItems}; height: 100%; padding: 0 8px; background-color: rgba(0, 0, 0, 0.2); .icon { margin-right: 6px; } input[type='text'] { border: none; background: transparent; height: 100%; color: #eee; ::placeholder { color: #aaa; } } ` interface Props extends InputHTMLAttributes { icon?: IconName } export const TextInput: FunctionComponent = ({ icon, ...rest }) => ( ) ================================================ FILE: src/Constants.ts ================================================ export const DEFAULT_OFFSET = 50 export const DEVELOPMENT = process.env.MODE === 'development' export enum PanelPage { DDP = 'ddp', BOOKMARKS = 'bookmarks', MINIMONGO = 'minimongo', SUBSCRIPTIONS = 'subscriptions', PERFORMANCE = 'performance', } ================================================ FILE: src/Database/PanelDatabase.ts ================================================ import Dexie from 'dexie' import { toJS } from 'mobx' class Database extends Dexie { bookmarks: Dexie.Table data: Dexie.Table, string> constructor() { super('MeteorToolsDatabase') this.version(1).stores({ bookmarks: 'id, timestamp, log', }) this.version(2).stores({ data: 'id', }) this.bookmarks = this.table('bookmarks') this.data = this.table('data') } add(log: DDPLog) { return this.bookmarks.add({ id: log.id, timestamp: Date.now(), log: toJS(log), }) } get(key: string) { return this.bookmarks.get(key) } remove(key: string) { return this.bookmarks.delete(key) } getAll() { return this.bookmarks.toArray() } async getSettings() { return (await this.data.get('settings')) ?? {} } async saveSettings(settings: ISettings) { return (await this.data.get('settings')) ? this.data.update('settings', settings) : this.data.add({ id: 'settings', ...settings, }) } } export const PanelDatabase = new Database() ================================================ FILE: src/Injectors/DDPInjector.ts ================================================ import { sendLogMessage } from '@/Browser/Inject' type MessageCallback = (message: DDPLog) => void const generateId = () => (Date.now() + Math.random()).toString(36) const injectOutboundInterceptor = (callback: MessageCallback) => { const send = Meteor.connection._stream.send Meteor.connection._stream.send = function (...args) { send.apply(this, args) callback({ id: generateId(), content: args[0], isOutbound: true, timestamp: Date.now(), }) } } const injectInboundInterceptor = (callback: MessageCallback) => { Meteor.connection._stream.on('message', (...args) => { callback({ id: generateId(), content: args[0], isInbound: true, timestamp: Date.now(), }) }) } export const DDPInjector = () => { injectOutboundInterceptor(sendLogMessage) injectInboundInterceptor(sendLogMessage) } ================================================ FILE: src/Injectors/MeteorAdapter.ts ================================================ import { Registry, sendMessage } from '@/Browser/Inject' import { getSubscriptions } from '@/Browser/MeteorLibrary' import { JSONUtils } from '@/Utils/JSONUtils' export const MeteorAdapter = () => { Registry.register('ddp-run-method', (message: Message) => { const { method, params } = message.data Meteor.call(method, ...params) }) Registry.register('sync-subscriptions', () => { sendMessage('sync-subscriptions', { subscriptions: getSubscriptions(), }) }) Registry.register('stats', () => { sendMessage('stats', { gitCommitHash: Meteor.gitCommitHash, }) }) Registry.register('cache:clear', () => { sendMessage('cache:clear', {}) }) const prototype = Mongo.Collection.prototype for (const [key, val] of Object.entries(prototype)) { if ( ['find', 'findOne', 'insert', 'update', 'upsert', 'remove'].includes( key, ) && typeof val === 'function' ) { const original = prototype[key] prototype[key] = function (...args) { const startMs = Date.now() const result = original.apply(this, args) sendMessage('meteor-data-performance', { collectionName: this._name, key, args: JSON.stringify(args, JSONUtils.getCircularReplacer()), runtime: Date.now() - startMs, }) return result } } } } ================================================ FILE: src/Injectors/MinimongoInjector.ts ================================================ import { warning } from '@/Log' import { Registry, sendMessage } from '@/Browser/Inject' import throttle from 'lodash.throttle' function cloneDeep(obj: any) { return structuredClone(obj) } function isArray(obj: any) { return Array.isArray(obj) } const cleanup = (object: any) => { if (typeof object !== 'object') return object const clonedObject = cloneDeep(object) if (!clonedObject) return clonedObject for (const key of Object.keys(clonedObject)) { if (!clonedObject[key]) { return } if (typeof clonedObject[key] === 'object') { if (isArray(clonedObject[key])) { clonedObject[key] = clonedObject[key].map((item: any) => cleanup(item)) return } if (clonedObject[key] instanceof Date) { clonedObject[key] = `[Object::${ clonedObject[key].constructor.name }] ${clonedObject[key].toISOString()}` return } if (clonedObject[key].constructor.name !== 'Object') { if (typeof clonedObject[key].toString === 'function') { clonedObject[key] = `[Object::${ clonedObject[key].constructor.name }] ${clonedObject[key].toString()}` return } else { clonedObject[key] = `[Object::${clonedObject[key].constructor.name}]` return } } clonedObject[key] = cleanup(clonedObject[key]) } } return clonedObject } const getDocs = (collection: any) => { return collection._docs._map instanceof Map ? collection._docs._map?.values() || [] : Object.values(collection._docs._map || {}) } const getCollections = () => { const collections = Meteor.connection._mongo_livedata_collections if (!collections) { warning( 'Collections not initialized in the client yet. Possibly forgotten to be imported.', ) return } const data = Object.fromEntries( Object.values(collections).map((collection: any) => [ collection.name, [...getDocs(collection)].map(item => cleanup(item)), ]), ) sendMessage('minimongo-get-collections', data as any) } export const updateCollections = throttle(getCollections, 1000, { leading: true, trailing: true, }) export const MinimongoInjector = () => { Registry.register('minimongo-get-collections', () => { getCollections() }) } ================================================ FILE: src/Log.ts ================================================ export const warning = (message: string) => { console.log( '%c'.concat('Meteor DevTools Evolved: ').concat(message), 'color: #bada55', ) } ================================================ FILE: src/Pages/Options.tsx ================================================ import React, { FunctionComponent } from 'react' export const Options: FunctionComponent = () => { return (

Options

) } ================================================ FILE: src/Pages/Panel/Bookmarks/Bookmarks.tsx ================================================ import { usePanelStore } from '@/Stores/PanelStore' import { Hideable } from '@/Utils/Hideable' import { observer } from 'mobx-react-lite' import React, { FunctionComponent } from 'react' import { DDPContainer } from '@/Pages/Panel/DDP/DDPContainer' import { BookmarksStatus } from './BookmarksStatus' interface Props { isVisible: boolean } export const Bookmarks: FunctionComponent = observer(({ isVisible }) => { const store = usePanelStore() const bookmarkStore = store.bookmarkStore return ( ) }) ================================================ FILE: src/Pages/Panel/Bookmarks/BookmarksStatus.tsx ================================================ import { observer } from 'mobx-react-lite' import React, { FormEvent, FunctionComponent, useCallback } from 'react' import { usePanelStore } from '@/Stores/PanelStore' import { StatusBar } from '@/Components/StatusBar' import { DDPFilterMenu } from '@/Pages/Panel/DDP/DDPFilterMenu' import { Position } from '@blueprintjs/core/lib/esm/common/position' import { TextInput } from '@/Components/TextInput' import { PopoverButton } from '@/Components/PopoverButton' import { Field } from '@/Components/Field' import { exists } from '@/Utils' export const BookmarksStatus: FunctionComponent = observer(() => { const store = usePanelStore() const { bookmarkStore, settingStore } = store const activeFilters = store.settingStore.activeFilters const setFilter = useCallback( (type, isEnabled) => settingStore.setFilter(type, isEnabled), [settingStore], ) const collectionLength = bookmarkStore.collection.length const { pagination } = bookmarkStore return (
} position={Position.RIGHT_TOP} > Filter ) => pagination.setSearch(event.currentTarget.value) } /> {pagination.length}
{exists(collectionLength) && ( {collectionLength} )}
) }) ================================================ FILE: src/Pages/Panel/DDP/DDP.tsx ================================================ import { usePanelStore } from '@/Stores/PanelStore' import { Hideable } from '@/Utils/Hideable' import { observer } from 'mobx-react-lite' import React, { FunctionComponent } from 'react' import { DDPStatus } from './DDPStatus' import { DDPContainer } from '@/Pages/Panel/DDP/DDPContainer' interface Props { isVisible: boolean } export const DDP: FunctionComponent = observer(({ isVisible }) => { const store = usePanelStore() const ddpStore = store.ddpStore return ( ) }) ================================================ FILE: src/Pages/Panel/DDP/DDPContainer.tsx ================================================ import React, { FunctionComponent, useRef } from 'react' import { DDPLog } from '@/Pages/Panel/DDP/DDPLog' import { FixedSizeList } from 'react-window' import { observer } from 'mobx-react-lite' import { DDPStore } from '@/Stores/Panel/DDPStore' import { BookmarkStore } from '@/Stores/Panel/BookmarkStore' import { useDimensions } from '@/Utils/Hooks/useDimensions' import { usePanelStore } from '@/Stores/PanelStore' interface Props { source: DDPStore | BookmarkStore isVisible: boolean } export const DDPContainer: FunctionComponent = observer( ({ source, isVisible }) => { const store = usePanelStore() const contentRef = useRef(null) const { width, height } = useDimensions(contentRef, [isVisible]) const Row: FunctionComponent = observer(({ data, index, style }) => { const item = (data as any).items[index] const log = 'log' in item ? item.log : item return ( ) }) const list = ( {Row} ) return (
{source.filtered.length > 0 ? list : null}
) }, ) ================================================ FILE: src/Pages/Panel/DDP/DDPFilterMenu.tsx ================================================ import { Switch } from '@blueprintjs/core' import { observer } from 'mobx-react-lite' import React, { FormEvent, FunctionComponent } from 'react' import { FilterCriteria } from './FilterConstants' interface Props { activeFilters: FilterTypeMap setFilter: (filter: FilterType, isEnabled: boolean) => void } export const DDPFilterMenu: FunctionComponent = observer( ({ activeFilters, setFilter }) => { const filters = Object.keys(FilterCriteria).map(filter => ( ) => setFilter(filter as FilterType, event.currentTarget.checked) } /> )) return
{filters}
}, ) ================================================ FILE: src/Pages/Panel/DDP/DDPLog.tsx ================================================ import { Tag, Tooltip } from '@blueprintjs/core' import classnames from 'classnames' import React, { CSSProperties, FunctionComponent } from 'react' import { DDPLogDirection } from './DDPLogDirection' import { DDPLogPreview } from './DDPLogPreview' import { DateTime } from 'luxon' import styled from 'styled-components' import { truncate } from '@/Styles/Mixins' import { DDPLogMenu } from '@/Pages/Panel/DDP/DDPLogMenu' interface Props { log: DDPLog style: CSSProperties isNew: boolean isStarred: boolean } const DDPLogWrapper = styled.div` display: flex; align-items: center; flex-direction: row; justify-content: space-between; padding: 5px 15px; transition: background-color 0.5s ease; &.m-new { background-color: #30594d; } &.m-starred { background-color: #304066; } div + div { margin-left: 10px; } .time { font-size: 11px; font-family: inherit; } .content { display: flex; flex: 1; align-items: center; min-width: 0; .content-icon { margin-right: 10px; } .content-preview { flex: 0 1 auto; min-width: 0; code { font-family: monospace; ${truncate} } } } &:hover { background-color: #394b59; } ` export const DDPLog: FunctionComponent = ({ log, style, isNew, isStarred, }) => { const classes = classnames( { 'm-new': isNew, 'm-starred': isStarred, }, 'group', ) return (
{log.timestampPretty}
{log.sizePretty}
) } ================================================ FILE: src/Pages/Panel/DDP/DDPLogDirection.tsx ================================================ import { Icon } from '@blueprintjs/core' import React, { FunctionComponent } from 'react' interface Prop { isOutbound?: boolean isInbound?: boolean } export const DDPLogDirection: FunctionComponent = ({ isOutbound, isInbound, }) => { if (isOutbound && isInbound) return if (isOutbound) return if (isInbound) return return } ================================================ FILE: src/Pages/Panel/DDP/DDPLogMenu.tsx ================================================ import { Icon } from '@blueprintjs/core' import { PanelPage } from '@/Constants' import { Bridge } from '@/Bridge' import React, { FunctionComponent } from 'react' import { usePanelStore } from '@/Stores/PanelStore' interface Props { log: DDPLog } export const DDPLogMenu: FunctionComponent = ({ log }) => { const store = usePanelStore() return (
log.trace && store.setActiveStackTrace(log.trace)} style={{ cursor: 'pointer' }} /> store.bookmarkStore.bookmarkIds.includes(log.id) ? store.bookmarkStore.remove(log) : store.bookmarkStore.add(log) } style={{ cursor: 'pointer' }} /> {log.parsedContent?.msg === 'method' && ( { store.setSelectedTabId(PanelPage.DDP) Bridge.sendContentMessage({ eventType: 'ddp-run-method', data: log.parsedContent, }) }} style={{ cursor: 'pointer' }} /> )}
) } ================================================ FILE: src/Pages/Panel/DDP/DDPLogPreview.tsx ================================================ import { usePanelStore } from '@/Stores/PanelStore' import { Icon, IconName, Tag, Tooltip } from '@blueprintjs/core' import React, { FunctionComponent } from 'react' const getTag = (icon: IconName, title: string) => ( ) const getTypeTag = (filterType?: FilterType | null) => { switch (filterType) { case 'heartbeat': { return getTag('heart', 'Heartbeat') } case 'connection': { return getTag('globe-network', 'Connection') } case 'collection': { return getTag('database', 'Collection') } case 'subscription': { return getTag('feed-subscribed', 'Subscription') } case 'method': { return getTag('derive-column', 'Method') } default: { return getTag('warning-sign', 'Unknown') } } } export const DDPLogPreview: FunctionComponent> = ({ filterType, parsedContent, preview, }) => { const store = usePanelStore() return ( <> {getTypeTag(filterType)} { if (parsedContent) store.setActiveObject(parsedContent) }} className='content-preview' intent={parsedContent?.error ? 'danger' : 'none'} > {preview} ) } ================================================ FILE: src/Pages/Panel/DDP/DDPStatus.tsx ================================================ import { Spinner, Tag, Tooltip } from '@blueprintjs/core' import { isNumber } from 'lodash' import { observer } from 'mobx-react-lite' import React, { FormEvent, FunctionComponent, useCallback } from 'react' import { usePanelStore } from '@/Stores/PanelStore' import { StatusBar } from '@/Components/StatusBar' import { DDPFilterMenu } from '@/Pages/Panel/DDP/DDPFilterMenu' import { Position } from '@blueprintjs/core/lib/esm/common/position' import { TextInput } from '@/Components/TextInput' import { PopoverButton } from '@/Components/PopoverButton' import { Button } from '@/Components/Button' import prettyBytes from 'pretty-bytes' import { Field } from '@/Components/Field' import { StringUtils } from '@/Utils/StringUtils' import { AppToaster } from '@/AppToaster' export const DDPStatus: FunctionComponent = observer(() => { const store = usePanelStore() const { ddpStore, settingStore } = store const activeFilters = settingStore.activeFilters const setFilter = useCallback( (type, isEnabled) => settingStore.setFilter(type, isEnabled), [settingStore], ) const collectionLength = ddpStore.collection.length const { inboundBytes, outboundBytes, isLoading, pagination } = ddpStore return (
} position={Position.RIGHT_TOP} > Filter ) => pagination.setSearch(event.currentTarget.value) } /> {pagination.length}
{isLoading && ( )} {store.gitCommitHash ? ( { StringUtils.toClipboard(store.gitCommitHash as string) AppToaster.show({ icon: 'tick', message: 'Copied to Clipboard', intent: 'success', timeout: 1000, }) }} style={{ marginRight: 4 }} > {store.gitCommitHash.slice(0, 8)} ) : null} {!!inboundBytes && ( {prettyBytes(inboundBytes)} )} {!!outboundBytes && ( {prettyBytes(outboundBytes)} )} {isNumber(collectionLength) && ( )}
) }) ================================================ FILE: src/Pages/Panel/DDP/FilterConstants.ts ================================================ export const FilterCriteria: FilterTypeMap = { heartbeat: ['ping', 'pong'], subscription: ['sub', 'unsub', 'nosub', 'ready'], collection: ['added', 'removed', 'changed'], method: ['method', 'result', 'updated'], connection: ['connect', 'connected', 'failed'], } export const FilterCriteriaMap: { [key: string]: FilterType } = Object.fromEntries( Object.entries(FilterCriteria).flatMap(([key, matchers]) => matchers.map(matcher => [matcher, key]), ), ) export const detectType = (content?: DDPLogContent) => { if (content && content.msg && content.msg in FilterCriteriaMap) { return FilterCriteriaMap[content.msg] } return null } ================================================ FILE: src/Pages/Panel/DrawerJSON.tsx ================================================ import { ObjectTreerinator } from '@/Utils/ObjectTreerinator' import { Button, Classes, Drawer } from '@blueprintjs/core' import React, { FunctionComponent } from 'react' import { StringUtils } from '@/Utils/StringUtils' import { Popover2 } from '@blueprintjs/popover2' interface Props { title: string | null viewableObject: ViewableObject onClose(): void } export const DrawerJSON: FunctionComponent = ({ title, viewableObject, onClose, }) => { return (
{!!viewableObject && }
Copied
} >
) } ================================================ FILE: src/Pages/Panel/DrawerStackTrace.tsx ================================================ import { Classes, Drawer } from '@blueprintjs/core' import { Tooltip2 } from '@blueprintjs/popover2' import classnames from 'classnames' import React, { FunctionComponent } from 'react' interface Props { activeStackTrace: StackTrace[] | null onClose(): void } export const DrawerStackTrace: FunctionComponent = ({ activeStackTrace, onClose, }) => (
{activeStackTrace?.map((stack: StackTrace, index: number) => { const text = (
{stack?.callee?.trim() || 'Anonymous'}
) return (
              {stack?.url ? (
                
                  
                    {text}
                  
                
              ) : (
                text
              )}
            
) })}
) ================================================ FILE: src/Pages/Panel/HelpDrawer.tsx ================================================ import { Classes, Drawer, DrawerSize, Icon } from '@blueprintjs/core' import React, { FunctionComponent } from 'react' import { GridItem, PartnersGrid } from './PartnersGrid' import AuthorLogo from '@/Assets/leonardoventurini.png' import MeteorCloudLogo from '@/Assets/meteor-cloud-logo.png' const people: GridItem[] = [ { name: 'Leonardo Venturini', title: 'Senior Software Engineer', role: 'Author', email: 'leonardo@techster.tech', imageUrl: AuthorLogo, description: 'If you need help with extension related issues or general Node.js or Meteor consulting', slack: 'https://meteor-community.slack.com/archives/DRKE6HDD5/', linkedin: 'https://www.linkedin.com/in/leonardo-venturini/', website: 'https://leonardoventurini.tech/', }, ] const orgs: GridItem[] = [ { name: 'Galaxy', title: 'Organization', role: 'Partner', website: 'https://social.meteor.com/devtools-evolved/', imageUrl: MeteorCloudLogo, description: 'If you want a full service cloud offering for deploying, hosting, and scaling your apps with zero DevOps', }, ] interface Props { isHelpDrawerVisible: boolean onClose(): void } const YEAR = new Date().getFullYear() export const HelpDrawer: FunctionComponent = ({ isHelpDrawerVisible, onClose, }) => { return ( Help } isOpen={isHelpDrawerVisible} onClose={onClose} size={DrawerSize.LARGE} >

Extension

Meteor & Development

Basics

Behold, the evolution of Meteor DevTools.

Change Log

The extension initializes with the page content, which means you have to refresh the page after installation, also it needs the devtools panel to be opened at least once in the current tab for any messages to be processed.

Other than that you can just explore the extension at your leisure. It should be easy enough.

Feedback

Any feedback you might have can be addressed directly at our{' '} GitHub Issues {' '} page, that way we can discuss and transition into development more easily. You can also reach the author on the{' '} Meteor Community Slack {' '} or the{' '} Meteor Forums .

Starring the project is the easiest way to support the work and be part of our community of{' '} stargazers .

Let's make Meteor great again.

Firefox

The Firefox port of the extension was a contribution made by{' '} RF Niloy . Thank you!

License

The MIT License (MIT)

Copyright (c) {YEAR}{' '} Leonardo Venturini

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

) } ================================================ FILE: src/Pages/Panel/Minimongo/Minimongo.tsx ================================================ import { MinimongoNavigator } from '@/Pages/Panel/Minimongo/MinimongoNavigator' import { usePanelStore } from '@/Stores/PanelStore' import { Hideable } from '@/Utils/Hideable' import { observer } from 'mobx-react-lite' import React, { FunctionComponent } from 'react' import { MinimongoContainer } from '@/Pages/Panel/Minimongo/MinimongoContainer' import styled from 'styled-components' import { MinimongoStatus } from '@/Pages/Panel/Minimongo/MinimongoStatus' import { Button } from '@/Components/Button' import prettyBytes from 'pretty-bytes' interface Props { isVisible: boolean } const Wrapper = styled.div` display: flex; flex-direction: row; height: 100%; .sidebar { display: flex; height: 100%; width: 222px; overflow-y: auto; font-size: 11px; font-family: monospace; nav { display: flex; flex: 1; flex-direction: column; width: 100%; button { flex: 0 0 20px; width: 100%; &.active { background: rgba(255, 255, 255, 0.15); } &:hover:not(.active) { background: rgba(255, 255, 255, 0.1); } } } } .container { height: 100%; min-width: 0; flex-grow: 1; flex-shrink: 1; .row { display: flex; align-items: center; padding: 5px 15px; & > * + * { margin-left: 8px; } } } ` export const Minimongo: FunctionComponent = observer(({ isVisible }) => { const { minimongoStore } = usePanelStore() const isActiveCollectionMissing = minimongoStore.activeCollection && !(minimongoStore.activeCollection in minimongoStore.collections) if (isActiveCollectionMissing) { minimongoStore.setActiveCollection(null) } return (
) }) ================================================ FILE: src/Pages/Panel/Minimongo/MinimongoContainer.tsx ================================================ import React, { CSSProperties, FunctionComponent, useRef } from 'react' import { areEqual, FixedSizeList } from 'react-window' import { observer } from 'mobx-react-lite' import { usePanelStore } from '@/Stores/PanelStore' import { MinimongoRow } from '@/Pages/Panel/Minimongo/MinimongoRow' import { useDimensions } from '@/Utils/Hooks/useDimensions' interface Props { isVisible: boolean } export const MinimongoContainer: FunctionComponent = observer( ({ isVisible }) => { const contentRef = useRef(null) const store = usePanelStore() const { activeCollectionDocuments, activeCollection } = store.minimongoStore const { width, height } = useDimensions(contentRef, [isVisible]) interface IRow { data: { items: IDocumentWrapper[] } index: number style: CSSProperties } const Row: FunctionComponent = React.memo( ({ data, index, style }: IRow) => { const item = data.items![index] return ( store.setActiveObject(item.document)} onCollectionClick={() => store.minimongoStore.setActiveCollection(item.collectionName) } isAllVisible={!activeCollection} /> ) }, areEqual, ) return (
{Row}
) }, ) ================================================ FILE: src/Pages/Panel/Minimongo/MinimongoNavigator.tsx ================================================ import { Button, Classes, Dialog, InputGroup, Menu, MenuItem, NonIdealState, } from '@blueprintjs/core' import React, { FormEvent, FunctionComponent } from 'react' import { usePanelStore } from '@/Stores/PanelStore' import { observer } from 'mobx-react-lite' export const MinimongoNavigator: FunctionComponent = observer(() => { const { minimongoStore } = usePanelStore() const setActiveCollection = (collectionName: string | null) => { minimongoStore.setActiveCollection(collectionName) minimongoStore.setNavigatorVisible(false) } return ( { minimongoStore.setNavigatorVisible(false) minimongoStore.setSearch('') }} title='Collections' isOpen={minimongoStore.isNavigatorVisible} >
{minimongoStore.filteredCollectionNames.length > 0 ? ( minimongoStore.filteredCollectionNames.map(key => ( setActiveCollection(key)} /> )) ) : (
)}
) => minimongoStore.setSearch(event.currentTarget.value) } />
) }) ================================================ FILE: src/Pages/Panel/Minimongo/MinimongoRow.tsx ================================================ import { StringUtils } from '@/Utils/StringUtils' import { Tag } from '@blueprintjs/core' import React, { CSSProperties, FunctionComponent } from 'react' import styled from 'styled-components' import { truncate } from '@/Styles/Mixins' const Wrapper = styled.div` &, & code { font-family: monospace; font-size: 12px; } .collection { ${truncate}; cursor: pointer; flex: 0 0 auto; } .preview { ${truncate}; flex: 0 1 auto; } ` interface Props { item: IDocumentWrapper style: CSSProperties onClick: () => void onCollectionClick: () => void isAllVisible: boolean } export const MinimongoRow: FunctionComponent = ({ item, style, onClick, onCollectionClick, isAllVisible, }) => { return ( {isAllVisible && ( onCollectionClick()} > {item.collectionName} )} onClick()}> {StringUtils.truncate(item._string, 256)} ) } ================================================ FILE: src/Pages/Panel/Minimongo/MinimongoStatus.tsx ================================================ import React, { FormEvent, FunctionComponent } from 'react' import { StatusBar } from '@/Components/StatusBar' import { Button } from '@/Components/Button' import { TextInput } from '@/Components/TextInput' import { Field } from '@/Components/Field' import { observer } from 'mobx-react-lite' import { usePanelStore } from '@/Stores/PanelStore' export const MinimongoStatus: FunctionComponent = observer(() => { const { minimongoStore } = usePanelStore() return (
{minimongoStore.activeCollection && ( )} ) => minimongoStore.activeCollectionDocuments.pagination.setSearch( event.currentTarget.value, ) } /> {minimongoStore.activeCollectionDocuments.pagination.length}
) }) ================================================ FILE: src/Pages/Panel/Navigation.tsx ================================================ import { PanelPage } from '@/Constants' import React, { FunctionComponent, useEffect } from 'react' import { usePanelStore } from '@/Stores/PanelStore' import { observer } from 'mobx-react-lite' import { Bridge, syncSubscriptions } from '@/Bridge' import { IMenuItem, ITab, TabBar } from '@/Components/TabBar' import { Tag } from '@blueprintjs/core' import { isNumber } from 'lodash' import { useAnalytics } from '@/Utils/Hooks/useAnalytics' import { openTab } from '@/Utils/BackgroundEvents' export const Navigation: FunctionComponent = observer(() => { const panelStore = usePanelStore() const analytics = useAnalytics() useEffect(() => { setTimeout(() => { panelStore.settingStore.updateRepositoryData() }, 2000) }, []) const { repositoryData } = panelStore.settingStore const tabs: ITab[] = [ { key: PanelPage.DDP, content: 'DDP', icon: 'changes', }, { key: PanelPage.BOOKMARKS, content: 'Bookmarks', icon: 'star', }, { key: PanelPage.MINIMONGO, content: 'Minimongo', icon: 'database', handler: () => { // Fetch collection data from the page. Bridge.sendContentMessage({ eventType: 'minimongo-get-collections', data: null, }) }, }, { key: PanelPage.SUBSCRIPTIONS, content: 'Subscriptions', icon: 'feed-subscribed', handler: () => { syncSubscriptions() }, }, { key: PanelPage.PERFORMANCE, content: 'Performance', icon: 'lightning', }, ] const menu: IMenuItem[] = [ { key: 'help', icon: 'help', content: 'Help', shine: true, handler: () => { panelStore.setHelpDrawerVisible(true) analytics?.event('navigation', 'click', { label: 'partners' }) }, }, { key: 'reload', icon: 'refresh', content: 'Reload', handler: () => location.reload(), shine: true, }, ] if (repositoryData) { menu.unshift({ key: 'feedback', icon: 'issue', content: Issues, handler: () => { openTab([...repositoryData.html_url, '/issues']) analytics?.event('navigation', 'click', { label: 'feedback' }) }, shine: true, }) menu.unshift({ key: 'star', icon: 'star', content: ( <> Star {isNumber(repositoryData.stargazers_count) ? ( {repositoryData.stargazers_count} ) : null} ), shine: true, handler: () => { openTab([...repositoryData.html_url, '/stargazers']) analytics?.event('navigation', 'click', { label: 'star' }) }, }) } menu.unshift({ key: 'sponsor', content: ❤️ Sponsor, shine: true, title: 'If you find this extension useful, please consider sponsoring', handler: () => { openTab('https://github.com/sponsors/leonardoventurini') analytics?.event('navigation', 'click', { label: 'sponsor' }) }, }) return (
panelStore.setSelectedTabId(key)} />
) }) ================================================ FILE: src/Pages/Panel/PartnersGrid.tsx ================================================ import React from 'react' import { ChatBubbleLeftIcon, EnvelopeIcon, LinkIcon, } from '@heroicons/react/20/solid' import classnames from 'classnames' export type GridItem = { name: string title: string role: string email?: string imageUrl?: string website?: string slack?: string linkedin?: string description?: string } export function PartnersGrid({ items, className = '' }) { return (
    {items.map(person => (
  • {person.name}

    {person.role}

    {person.title}

    {person.imageUrl ? ( ) : null}
    {person.description ? (
    {person.description}
    ) : null}
    {person.email ? ( ) : null} {person.website ? ( ) : null} {person.slack ? ( ) : null} {person.linkedin ? ( ) : null}
  • ))}
) } ================================================ FILE: src/Pages/Panel/Performance/Performance.tsx ================================================ import React, { PropsWithChildren } from 'react' import { Hideable } from '@/Utils/Hideable' import { HTMLTable, Tag } from '@blueprintjs/core' import { usePanelStore } from '@/Stores/PanelStore' import { observer } from 'mobx-react-lite' import styled from 'styled-components' import { StatusBar } from '@/Components/StatusBar' import { Button } from '@/Components/Button' type Props = { isVisible: boolean } const Wrapper = styled.div` overflow-y: auto !important; table, tbody { width: 100%; max-width: 100%; } table, tbody, tr, td, td span { font-size: 11px !important; } ` export const Performance = observer( ({ isVisible }: PropsWithChildren) => { const panelStore = usePanelStore() const { renderData } = panelStore.performanceStore return ( Collection Method Arguments Total Average Calls {renderData.map(data => ( {data.collectionName} {data.method} {data.args} {Math.round(data.runtime)} ms {data.averageRuntime.toFixed(3)} ms {data.calls}x ))}
) }, ) ================================================ FILE: src/Pages/Panel/Subscriptions/Subscriptions.tsx ================================================ import { usePanelStore } from '@/Stores/PanelStore' import { Hideable } from '@/Utils/Hideable' import { observer } from 'mobx-react-lite' import React, { FormEvent, FunctionComponent } from 'react' import { HTMLTable, Tag } from '@blueprintjs/core' import styled from 'styled-components' import { sortBy } from 'lodash' import { useInterval } from '@/Utils/Hooks/useInterval' import { syncSubscriptions } from '@/Bridge' import { StatusBar } from '@/Components/StatusBar' import { Field } from '@/Components/Field' import { TextInput } from '@/Components/TextInput' interface Props { isVisible: boolean } const Wrapper = styled.div` overflow-y: auto !important; table, tbody { width: 100%; max-width: 100%; } tbody { font-family: monospace; } table, tbody, tr, td, td span { font-size: 11px !important; } ` export const Subscriptions: FunctionComponent = observer( ({ isVisible }) => { useInterval(() => isVisible && syncSubscriptions(), 5000) const panelStore = usePanelStore() const subscriptions = sortBy( panelStore.subscriptionStore.subsWithMeta, 'meta.init.timestamp', ) return ( ID Name Params Active Ready Duration {subscriptions.map(subscription => { const duration = panelStore.ddpStore.getSubscriptionDuration(subscription) return ( panelStore.setActiveObject( { params: subscription.params, }, `${subscription.name} [${subscription.id}]`, ) } > {subscription.id} {subscription.name} {JSON.stringify(subscription.params)} {JSON.stringify(!subscription.inactive)} {JSON.stringify(subscription.ready)} {duration} ) })} ) => panelStore.subscriptionStore.pagination.setSearch( event.currentTarget.value, ) } />
{subscriptions.length}
) }, ) ================================================ FILE: src/Pages/Panel.tsx ================================================ import { PanelStoreProvider, usePanelStore } from '@/Stores/PanelStore' import { observer } from 'mobx-react-lite' import React, { FunctionComponent, useEffect, useRef } from 'react' import { Bookmarks } from './Panel/Bookmarks/Bookmarks' import { DDP } from './Panel/DDP/DDP' import { DrawerJSON } from './Panel/DrawerJSON' import { DrawerStackTrace } from './Panel/DrawerStackTrace' import { Minimongo } from './Panel/Minimongo/Minimongo' import { Navigation } from './Panel/Navigation' import { Bridge } from '@/Bridge' import { PanelPage } from '@/Constants' import { Subscriptions } from '@/Pages/Panel/Subscriptions/Subscriptions' import styled from 'styled-components' import { MIN_LAYOUT_WIDTH, NAVBAR_HEIGHT, STATUS_HEIGHT, } from '@/Styles/Constants' import { Performance } from '@/Pages/Panel/Performance/Performance' import { useAnalytics } from '@/Utils/Hooks/useAnalytics' import { HelpDrawer } from './Panel/HelpDrawer' Bridge.init() const Layout = styled.div` display: flex; flex-direction: column; position: relative; padding-top: ${NAVBAR_HEIGHT}px; padding-bottom: ${STATUS_HEIGHT}px; max-height: 100vh; min-width: ${MIN_LAYOUT_WIDTH}px; .mde-navbar { position: absolute; top: 0; left: 0; right: 0; } .mde-layout__tab-panel { position: relative; .mde-content { height: calc(100vh - ${NAVBAR_HEIGHT + STATUS_HEIGHT}px); padding: 0; overflow: hidden; } } ` const PanelObserverComponent: FunctionComponent = observer(() => { const store = usePanelStore() const panelRef = useRef(null) const analytics = useAnalytics() useEffect(() => { analytics?.pageView().catch(console.error) }, [analytics]) return ( { store.setActiveObject(null, null) }} /> store.setActiveStackTrace(null)} /> store.setHelpDrawerVisible(false)} />
) }) export const Panel = () => ( ) ================================================ FILE: src/Pages/Popup.tsx ================================================ import React, { FunctionComponent } from 'react' export const Popup: FunctionComponent = () => (

Popup

) ================================================ FILE: src/Stores/Common/Searchable.ts ================================================ import { DEFAULT_OFFSET } from '@/Constants' import { calculatePagination } from '@/Utils/Pagination' import debounce from 'lodash.debounce' import { action, computed, observable, runInAction } from 'mobx' type BufferCallback = ((buffer: T[]) => void) | null type FilterFunction = ((collection: T[], search: string) => T[]) | null export abstract class Searchable { bufferCallback: BufferCallback = null filterFunction: FilterFunction = null lastPush: number = 0 loadingTimeout: ReturnType | null = null buffer: T[] = [] @observable.shallow collection: T[] = [] @observable currentPage: number = 1 @observable search: string = '' @observable isLoading: boolean = false @action setCollection(collection: T[]) { this.collection = collection } pushItem(log: T) { this.lastPush = Date.now() if (!this.isLoading) { runInAction(() => { this.isLoading = true }) } this.buffer.push(log) this.submitLogs() this.setLoadingState(false) } submitLogs = debounce( action(() => { this._submitLogs() }), 100, { maxWait: 1000, }, ) @action _submitLogs() { if (this.bufferCallback) { this.bufferCallback(this.buffer) } console.log('submitted') this.collection.unshift(...this.buffer.reverse()) this.buffer = [] } setSearch = debounce( action((search: string) => { this.search = search this.currentPage = 1 }), 250, ) setLoadingState(isLoading: boolean) { if (this.loadingTimeout) { clearTimeout(this.loadingTimeout) } this.loadingTimeout = setTimeout( action(() => { this.isLoading = isLoading console.log('loading:false') }), 250, ) } @action setCurrentPage(currentPage: number) { this.currentPage = currentPage } @computed get filtered() { return this.filterFunction ? this.filterFunction(this.collection, this.search) : this.collection } @computed get pagination() { return calculatePagination( DEFAULT_OFFSET, this.filtered.length, this.currentPage, this.setSearch.bind(this), this.setCurrentPage.bind(this), ) } @computed get paginated() { return this.filtered.slice(this.pagination.start, this.pagination.end) } } ================================================ FILE: src/Stores/Panel/BookmarkStore.ts ================================================ import { PanelDatabase } from '@/Database/PanelDatabase' import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { Searchable } from '../Common/Searchable' import { PanelStore } from '@/Stores/PanelStore' export class BookmarkStore extends Searchable { constructor() { super() makeObservable(this) } @observable.shallow bookmarkIds: (string | undefined)[] = [] async sync() { const collection = await PanelDatabase.getAll() runInAction(() => { this.collection = collection this.bookmarkIds = this.collection.map( (bookmark: Bookmark) => bookmark.id, ) }) } @action async remove(log: DDPLog) { if (log.timestamp) { await PanelDatabase.remove(log.id) await this.sync() } } @action async add(log: DDPLog) { const key = await PanelDatabase.add(log) const bookmark = await PanelDatabase.get(key) if (bookmark) { runInAction(() => { this.collection.push(bookmark) this.bookmarkIds.push(bookmark.log.id) }) } } filterFunction = (collection: Bookmark[], search: string) => collection .filter( bookmark => !this.filterRegularExpression.test(bookmark.log.content), ) .filter( bookmark => !search || bookmark.log.content.toLowerCase().includes(search.toLowerCase()), ) @computed get filterRegularExpression() { return new RegExp( `"msg":"(${PanelStore.settingStore.activeFilterBlacklist.join('|')})"`, ) } } ================================================ FILE: src/Stores/Panel/DDPStore.ts ================================================ import debounce from 'lodash.debounce' import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { Searchable } from '../Common/Searchable' import { PanelStore } from '@/Stores/PanelStore' import { generatePreview } from '@/Utils/MessageFormatter' import { clearCache } from '@/Bridge' export class DDPStore extends Searchable { @observable inboundBytes = 0 @observable outboundBytes = 0 @observable newLogs: string[] = [] constructor() { super() makeObservable(this) } bufferCallback = (buffer: DDPLog[]) => { this.buffer = buffer.map((log: DDPLog) => ({ ...log, preview: generatePreview( log.content, log.parsedContent as DDPLogContent, log.filterType, ), })) this.newLogs.push(...buffer.map(({ id }) => id)) this.inboundBytes += buffer .filter(log => log.isInbound) .reduce((sum, log) => sum + (log.size ?? 0), 0) this.outboundBytes += buffer .filter(log => log.isOutbound) .reduce((sum, log) => sum + (log.size ?? 0), 0) this.clearNewLogs() } // eslint-disable-next-line unicorn/consistent-function-scoping clearNewLogs = debounce(() => { runInAction(() => { this.newLogs = [] }) }, 1000) filterFunction = (collection: DDPLog[], search: string) => collection .filter(log => !this.filterRegularExpression.test(log.content)) .filter( log => !search || `${log.content.toLowerCase()}${log.preview ?? ''}`.includes( search.toLowerCase(), ), ) @action clearLogs() { this.collection = [] this.inboundBytes = 0 this.outboundBytes = 0 clearCache() } @computed get filterRegularExpression() { return new RegExp( `"msg":"(${PanelStore.settingStore.activeFilterBlacklist.join('|')})"`, ) } @computed get subscriptionLogs() { return this.collection.filter( log => log.parsedContent.msg === 'ready' || log.parsedContent.msg === 'sub', ) } getSubscriptionInit(subscription) { return this.subscriptionLogs.find( log => log.parsedContent.id === subscription.id, ) } getSubscriptionReady(subscription) { return this.subscriptionLogs.find(log => log.parsedContent.subs?.includes?.(subscription.id), ) } getSubscriptionDuration(subscription) { const initLog = this.getSubscriptionInit(subscription) const readyLog = this.getSubscriptionReady(subscription) if (initLog && readyLog) return `${readyLog.timestamp - initLog.timestamp}ms` if (readyLog) return `???` if (initLog) return `waiting` return 'NA' } getSubscriptionMeta(subscription) { return { meta: { init: this.getSubscriptionInit(subscription), ready: this.getSubscriptionReady(subscription), }, } } } ================================================ FILE: src/Stores/Panel/MinimongoStore/CollectionStore.ts ================================================ import { Searchable } from '@/Stores/Common/Searchable' import { makeObservable } from 'mobx' export class CollectionStore extends Searchable { constructor() { super() makeObservable(this) } filterFunction = (collection: IDocumentWrapper[], search: string) => collection.filter( document => !search || JSON.stringify(document).toLowerCase().includes(search.toLowerCase()), ) } ================================================ FILE: src/Stores/Panel/MinimongoStore/index.ts ================================================ import debounce from 'lodash.debounce' import { action, computed, makeObservable, observable } from 'mobx' import { CollectionStore } from './CollectionStore' import { JSONUtils } from '@/Utils/JSONUtils' import { StringUtils } from '@/Utils/StringUtils' import prettyBytes from 'pretty-bytes' import { mapValues } from '@/Utils/Objects' export class MinimongoStore { activeCollectionDocuments = new CollectionStore() @observable collections: MinimongoCollections = {} @observable collectionMetadata: ICollectionMetadata = {} @observable activeCollection: string | null = null @observable search: string = '' @observable collectionColorMap: Record = {} @observable isNavigatorVisible = false constructor() { makeObservable(this) } @computed get totalDocuments() { return Object.values(this.collections).reduce( (acc, cur) => acc + cur.length, 0, ) } @computed get collectionNames() { return Object.keys(this.collections).sort() } @computed get filteredCollectionNames() { return this.collectionNames.filter( name => !this.search || name.toLowerCase().includes(this.search.toLowerCase()), ) } @computed get totalSize() { return Object.entries(this.collectionMetadata).reduce( (sum, [collectionName, metadata]) => sum + metadata.collectionSize, 0, ) } @action getMetadata(collectionName: string) { return this.collectionMetadata?.[collectionName] } @action computeCollectionSizes() { for (const collectionName of Object.keys(this.collections)) { const collectionSize = this.collections[collectionName].reduce( (acc: number, cur: IDocumentWrapper) => acc + cur._size, 0, ) this.collectionMetadata[collectionName] = { collectionSize, collectionSizePretty: prettyBytes(collectionSize), } } } @action syncDocuments() { if (this.activeCollection) { return this.activeCollectionDocuments.setCollection( this.collections[this.activeCollection], ) } this.activeCollectionDocuments.setCollection( Object.entries(this.collections).flatMap( ([collectionName, documents]) => { return documents }, ), ) } @action setCollections(collections: RawCollections) { this.collections = mapValues(collections, (collection, collectionName) => { return collection.map(document => MinimongoStore.wrapDocument(document, collectionName), ) }) this.computeCollectionSizes() this.syncDocuments() } @action setActiveCollection(collection: string | null) { this.activeCollection = collection this.syncDocuments() } setSearch = debounce( action((search: string) => (this.search = search)), 250, ) @action setNavigatorVisible(isVisible: boolean) { this.isNavigatorVisible = isVisible } static wrapDocument( document: IDocument, collectionName: string, ): IDocumentWrapper { const _string = JSONUtils.stringify(document) console.log({ collectionName }) return { collectionName, document, _string, _size: StringUtils.getSize(_string), } } } ================================================ FILE: src/Stores/Panel/PerformanceStore.ts ================================================ import { action, makeObservable, observable } from 'mobx' import sortBy from 'lodash.sortby' import debounce from 'lodash.debounce' type AccCallData = { collectionName: string key: string method: string args: string runtime: number averageRuntime: number updatedAt: number calls: number } export class PerformanceStore { constructor() { makeObservable(this) } callMap = new Map() @observable.shallow renderData: AccCallData[] = [] updateRenderData = debounce( action(() => { this.renderData = sortBy( [...this.callMap.values()], ['runtime', 'args', 'method', 'collectionName'], ) .reverse() .slice(0, 100) }), 250, { maxWait: 5000, }, ) push(data: CallData) { const key = `${data.collectionName}${data.key}${data.args}` if (this.callMap.has(key)) { const existingData = this.callMap.get(key) const runtime = (existingData?.runtime ?? 0) + data.runtime this.callMap.set(key, { collectionName: data.collectionName, key, method: data.key, args: data.args, runtime, averageRuntime: runtime / existingData.calls, updatedAt: Date.now(), calls: existingData.calls + 1, }) } else { this.callMap.set(key, { collectionName: data.collectionName, key, method: data.key, args: data.args, runtime: data?.runtime, averageRuntime: data?.runtime, updatedAt: Date.now(), calls: 1, }) } this.updateRenderData() } @action clear() { this.callMap.clear() this.renderData = [] } } ================================================ FILE: src/Stores/Panel/SettingStore.ts ================================================ import { action, makeObservable, observable, reaction, runInAction, toJS, } from 'mobx' import { PanelDatabase } from '@/Database/PanelDatabase' import { FilterCriteria } from '@/Pages/Panel/DDP/FilterConstants' import { compact, flatten, omit } from '@/Utils/Objects' export class SettingStore implements ISettings { hydrated = false @observable repositoryData: IGitHubRepository | null = null @observable activeFilterBlacklist: string[] = [] @observable activeFilters: FilterTypeMap = { heartbeat: true, subscription: true, collection: true, method: true, connection: true, } constructor() { makeObservable(this) PanelDatabase.getSettings().then(settings => { runInAction(() => { Object.assign(this, settings) }) setTimeout(() => { runInAction(() => { this.hydrated = true }) }, 1000) }) reaction( () => toJS(this), (data: ISettings) => { if (this.hydrated) { PanelDatabase.saveSettings(omit(data, ['hydrated']) as ISettings) .then(() => { console.log('Settings updated.') }) .catch(console.error) } }, ) } @action setRepositoryData(repositoryData: IGitHubRepository) { this.repositoryData = repositoryData } @action updateRepositoryData() { fetch( 'https://api.github.com/repos/leonardoventurini/meteor-devtools-evolved', ) .then(response => response.json()) .then(data => { if (data) { if (!data.stargazers_count || !data.open_issues_count) { console.log('Not updating repository data', data) return } runInAction(() => { this.setRepositoryData(data) }) } }) .catch(console.error) } @action setFilter(type: FilterType, isEnabled: boolean) { this.activeFilters[type] = isEnabled this.activeFilterBlacklist = flatten( compact( Object.entries(this.activeFilters).map(([type, isEnabled]) => { return isEnabled ? false : FilterCriteria[type as FilterType] }), ), ) } } ================================================ FILE: src/Stores/Panel/SubscriptionStore.ts ================================================ import { Searchable } from '@/Stores/Common/Searchable' import { computed, makeObservable } from 'mobx' import { PanelStore } from '@/Stores/PanelStore' export class SubscriptionStore extends Searchable { constructor() { super() makeObservable(this) } filterFunction = (collection: IMeteorSubscription[], search: string) => collection.filter( document => !search || JSON.stringify(document).toLowerCase().includes(search.toLowerCase()), ) @computed get subsWithMeta() { return this.filtered.map(sub => ({ ...sub, ...PanelStore.ddpStore.getSubscriptionMeta(sub), })) } } ================================================ FILE: src/Stores/PanelStore.tsx ================================================ import { action, makeObservable, observable, toJS } from 'mobx' import React, { createContext, FunctionComponent, useContext } from 'react' import { BookmarkStore } from './Panel/BookmarkStore' import { DDPStore } from './Panel/DDPStore' import { MinimongoStore } from './Panel/MinimongoStore' import { PanelPage } from '@/Constants' import { SettingStore } from '@/Stores/Panel/SettingStore' import { SubscriptionStore } from '@/Stores/Panel/SubscriptionStore' import { PerformanceStore } from './Panel/PerformanceStore' export class PanelStoreConstructor { @observable selectedTabId: string = PanelPage.DDP @observable activeObjectTitle: string | null = null @observable activeObject: ViewableObject = null @observable.shallow activeStackTrace: StackTrace[] | null = null @observable isHelpDrawerVisible = false @observable subscriptions: Record = {} @observable gitCommitHash?: string | null = null ddpStore = new DDPStore() bookmarkStore = new BookmarkStore() minimongoStore = new MinimongoStore() subscriptionStore = new SubscriptionStore() settingStore = new SettingStore() performanceStore = new PerformanceStore() constructor() { makeObservable(this) this.bookmarkStore.sync().catch(console.error) } @action syncSubscriptions(subscriptions: Record) { this.subscriptionStore.setCollection(Object.values(subscriptions)) } @action setActiveObject(viewableObject: ViewableObject, title: string | null = null) { this.activeObject = viewableObject this.activeObjectTitle = title } @action setActiveStackTrace(trace: StackTrace[] | null) { this.activeStackTrace = trace } @action setSelectedTabId(selectedTabId: string) { this.selectedTabId = selectedTabId } @action setHelpDrawerVisible(isHelpDrawerVisible: boolean) { this.isHelpDrawerVisible = isHelpDrawerVisible } @action getSubscriptionById(id: string) { const subs = toJS(this.subscriptions) return id in subs ? subs[id] : null } @action setGitCommitHash(hash: string) { this.gitCommitHash = hash } } export const PanelStore = new PanelStoreConstructor() const PanelStoreContext = createContext(null) export const PanelStoreProvider: FunctionComponent = ({ children }) => ( {children} ) export const usePanelStore = () => { const store = useContext(PanelStoreContext) if (!store) { throw new Error('Must be used within a provider.') } return store } ================================================ FILE: src/Styles/App.scss ================================================ @import '~normalize.css/normalize.css'; @import '~@blueprintjs/core/lib/css/blueprint.css'; @import '~@blueprintjs/popover2/lib/css/blueprint-popover2.css'; @import '~@blueprintjs/icons/lib/css/blueprint-icons.css'; @import 'Utils'; $background-color: #30404d; ::-webkit-scrollbar { width: 10px; background: transparent; } ::-webkit-scrollbar-track { -webkit-box-shadow: none; background: transparent; } ::-webkit-scrollbar-thumb { -webkit-box-shadow: none; background-color: lighten($background-color, 15%); } html, body { font-size: 12px; } body { background-color: $background-color; overflow: hidden; } pre { white-space: pre-wrap; } .truncated { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .mde-stack-trace { pre { margin-bottom: 4px; } } .bp3-menu-item { i.fas, i.fab { margin-top: 2px; font-size: 16px; } } ================================================ FILE: src/Styles/Breakpoints.ts ================================================ import { mapValues } from '@/Utils/Objects' import { css, FlattenSimpleInterpolation } from 'styled-components' type BreakpointLabel = 'xs' | 'sm' | 'md' | 'lg' | 'xl' export const Breakpoints: Record = { xs: 0, sm: 600, md: 960, lg: 1280, xl: 1920, } export const respond = mapValues( Breakpoints, (value: number) => (content: FlattenSimpleInterpolation) => css` @media (min-width: ${value}px) { ${content}; } `, ) ================================================ FILE: src/Styles/Constants.ts ================================================ export const MIN_LAYOUT_WIDTH = 600 export const NAVBAR_HEIGHT = 29 export const STATUS_HEIGHT = 29 export const BACKGROUND_COLOR = '#30404d' ================================================ FILE: src/Styles/Mixins.ts ================================================ import { css } from 'styled-components' export const truncate = () => css` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; ` export const centerItems = () => css` display: flex; flex-direction: row; align-items: center; ` ================================================ FILE: src/Styles/Tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; .section { @apply text-base; } .section-title { @apply mb-4 font-medium uppercase text-gray-500; } ================================================ FILE: src/Styles/_Utils.scss ================================================ @mixin truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ================================================ FILE: src/Utils/BackgroundEvents.ts ================================================ import browser from 'webextension-polyfill' export const openTab = (url: string): void => { browser.runtime .sendMessage({ source: 'meteor-devtools-evolved', eventType: 'create-tab', data: { url: url }, }) .catch(console.error) } ================================================ FILE: src/Utils/Hideable.tsx ================================================ import React, { FunctionComponent, HTMLProps } from 'react' interface Props { isVisible: boolean } export const Hideable: FunctionComponent> = ({ children, isVisible, ...props }) => { const styles = { display: isVisible ? undefined : 'none', } return (
{children}
) } ================================================ FILE: src/Utils/Hooks/useAnalytics.ts ================================================ import { singletonHook } from 'react-singleton-hook' import { useEffect, useState } from 'react' import { Analytics } from '@/Analytics' export const useAnalytics = singletonHook(null, () => { const [instance, setInstance] = useState() useEffect(() => { const GA_TID = 'UA-211731487-1' setInstance(new Analytics(GA_TID, { userAgent: navigator.userAgent })) }, []) return instance }) ================================================ FILE: src/Utils/Hooks/useBreakpoints.ts ================================================ import { useRef } from 'react' import { useDimensions } from '@/Utils/Hooks/useDimensions' type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'navigationCollapse' export const useBreakpoints = () => { const ref = useRef(document.body) const { width } = useDimensions(ref, []) const breakpoints: { [key in Breakpoint]: boolean } = { xs: width <= 360, sm: width <= 720, md: width <= 1280, lg: width <= 1920, xl: width > 1920, navigationCollapse: width <= 980, } return breakpoints } ================================================ FILE: src/Utils/Hooks/useDimensions.ts ================================================ import { RefObject, useEffect, useState } from 'react' import { useResize } from '@/Utils/Hooks/useResize' export const useDimensions = (ref: RefObject, deps: any[]) => { const [dimensions, setDimensions] = useState({ height: 300, width: 300, }) useEffect(() => { setDimensions({ width: ref?.current?.clientWidth ?? 300, height: ref?.current?.clientHeight ?? 300, }) }, deps) useResize(() => { setDimensions({ width: ref?.current?.clientWidth ?? 300, height: ref?.current?.clientHeight ?? 300, }) }) return dimensions } ================================================ FILE: src/Utils/Hooks/useInterval.ts ================================================ import { useEffect, useRef } from 'react' export const useInterval = (callback: () => void, delay: number) => { const savedCallback = useRef<() => void>() useEffect(() => { savedCallback.current = callback }, [callback]) useEffect(() => { if (delay) { const id = setInterval( () => savedCallback.current && savedCallback.current(), delay, ) return () => clearInterval(id) } }, [delay]) } ================================================ FILE: src/Utils/Hooks/useResize.ts ================================================ import { useEffect } from 'react' export const useResize = (onResize: () => void) => { useEffect(() => { window.addEventListener('resize', onResize) return () => { window.removeEventListener('resize', onResize) } }, []) } ================================================ FILE: src/Utils/JSONUtils.ts ================================================ export namespace JSONUtils { export const getCircularReplacer = () => { const seen = new WeakSet() return (key: string, value: any) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) return seen.add(value) } return value } } export const stringify = (value: any) => JSON.stringify(value, getCircularReplacer()) } ================================================ FILE: src/Utils/MessageFormatter.ts ================================================ import { isString, StringUtils } from '@/Utils/StringUtils' import { isNumber } from './Numbers' const MAX_CHARACTERS = 512 export const MessageFormatter = { heartbeat({ msg }: DDPLogContent) { return msg }, collection({ msg, collection }: DDPLogContent) { const prepMap: { [key: string]: string } = { added: 'to', removed: 'from', changed: 'at', } if (msg && msg in prepMap) { return `${msg} ${prepMap[msg]} ${collection}` } }, connection({ msg, session }: DDPLogContent) { return session || msg }, subscription({ msg, id, name, subs }: any) { if (msg === 'unsub') { return `${id} stopping` } if (msg === 'nosub') { return `${id} stopped` } if (msg === 'sub') { return `${name} initializing` } if (msg === 'ready') { const idsToNames = subs.map((id: string) => id).filter(Boolean) return `[${idsToNames.join(', ')}] ready` } return null }, method({ msg, method, result, error }: DDPLogContent) { if (msg === 'method') { return method } if (msg === 'result' && error) { return StringUtils.truncate( `${error.errorType}: ${error.message}`, MAX_CHARACTERS, ) } if (msg === 'result') { return StringUtils.truncate(JSON.stringify(result), MAX_CHARACTERS) } return msg }, } const idFormat = (message: string, id?: string | number | null) => { if (isNumber(id) || isString(id)) { return `[${id}] ${StringUtils.truncate(message, MAX_CHARACTERS)}` } return message } export const generatePreview = ( content: string, parsedContent: DDPLogContent, filterType?: FilterType | null, ) => { if (parsedContent && filterType) { const message = (() => { if (filterType in MessageFormatter) { return MessageFormatter[filterType](parsedContent) } return null })() if (message) { return idFormat(message, parsedContent.id) } } return StringUtils.truncate(content, MAX_CHARACTERS) } ================================================ FILE: src/Utils/Numbers.ts ================================================ export const isNumber = (value: any) => typeof value === 'number' ================================================ FILE: src/Utils/ObjectTreerinator/ArrayNodeRenderer.tsx ================================================ import React from 'react' import { ObjectTreeNode } from '@/Utils/ObjectTreerinator/index' import { isArray, isBoolean, isNil, isNumber, isObject, isString } from 'lodash' import { Collapsible } from '@/Utils/ObjectTreerinator/Collapsible' export const ArrayNodeRenderer = (child: any, level: number) => { if (isNil(child)) return ( null ) if (isString(child)) return {`"${child}"`} if (isNumber(child)) return {child} if (isBoolean(child)) return {JSON.stringify(child)} if (isArray(child)) return (
    {child.map((item, index) => (
  1. {index}: {ArrayNodeRenderer(item, level + 1)}
  2. ))}
) if (isObject(child)) return return {`"${JSON.stringify(child)}"`} } ================================================ FILE: src/Utils/ObjectTreerinator/ArrayRenderer.tsx ================================================ import React, { FunctionComponent } from 'react' import { Collapsible } from '@/Utils/ObjectTreerinator/Collapsible' import { ArrayNodeRenderer } from '@/Utils/ObjectTreerinator/ArrayNodeRenderer' interface Props { property: string child: any[] level: number } export const ArrayRenderer: FunctionComponent = ({ property, child, level, }) => (
  • {property}
      {child.map((item, index) => (
    1. {index}: {ArrayNodeRenderer(item, level + 1)}
    2. ))}
  • ) ================================================ FILE: src/Utils/ObjectTreerinator/BooleanRenderer.tsx ================================================ import React from 'react' export const BooleanRenderer = (key: string, child: boolean) => (
  • {key}{JSON.stringify(child)}
  • ) ================================================ FILE: src/Utils/ObjectTreerinator/Collapsible.tsx ================================================ import React, { FunctionComponent, useState } from 'react' import { isArray, isEmpty, isObject } from 'lodash' interface Props { object: any level?: number } export const Collapsible: FunctionComponent = ({ children, object, level = 0, }) => { const [isCollapsed, setIsCollapsed] = useState(level > 5) if (isArray(object)) { const isArrayEmpty = isEmpty(object) if (isCollapsed || isArrayEmpty) { return ( !isArrayEmpty && setIsCollapsed(false)} >{`[${object.length}]`} ) } return ( <> {level > 1 && ( setIsCollapsed(true)}> {'[-]'} )} {children} ) } if (isObject(object)) { const isObjectEmpty = isEmpty(object) if (isCollapsed) { return ( !isObjectEmpty && setIsCollapsed(false)} >{`{${Object.keys(object).length}}`} ) } return ( <> {level > 1 && ( setIsCollapsed(true)}> {'{-}'} )} {children} ) } console.error('Not a valid collapsible value.') console.trace(object) return null } ================================================ FILE: src/Utils/ObjectTreerinator/NullRenderer.tsx ================================================ import React from 'react' export const NullRenderer = (key: string) => (
  • {key}null
  • ) ================================================ FILE: src/Utils/ObjectTreerinator/NumberRenderer.tsx ================================================ import React from 'react' export const NumberRenderer = (key: string, child: number) => (
  • {key}{child}
  • ) ================================================ FILE: src/Utils/ObjectTreerinator/ObjectRenderer.tsx ================================================ import React, { FunctionComponent } from 'react' import { ObjectTreeNode } from '@/Utils/ObjectTreerinator/index' interface Props { property: string child: object level: number } export const ObjectRenderer: FunctionComponent = ({ property, child, level, }) => (
  • {property}
  • ) ================================================ FILE: src/Utils/ObjectTreerinator/StringRenderer.tsx ================================================ import React from 'react' export const StringRenderer = (key: string, child: string) => (
  • {key}{`"${child}"`}
  • ) ================================================ FILE: src/Utils/ObjectTreerinator/index.tsx ================================================ import { isArray, isBoolean, isNil, isNumber, isObject, isString, toPairs, } from 'lodash' import React, { FunctionComponent } from 'react' import { Collapsible } from './Collapsible' import { StringRenderer } from '@/Utils/ObjectTreerinator/StringRenderer' import { ArrayRenderer } from '@/Utils/ObjectTreerinator/ArrayRenderer' import { ObjectRenderer } from '@/Utils/ObjectTreerinator/ObjectRenderer' import { BooleanRenderer } from '@/Utils/ObjectTreerinator/BooleanRenderer' import { NumberRenderer } from '@/Utils/ObjectTreerinator/NumberRenderer' import { NullRenderer } from '@/Utils/ObjectTreerinator/NullRenderer' import styled from 'styled-components' const TreeWrapper = styled.div` font-family: monospace; font-size: 12px; padding: 1rem; span[role='collapsible-property'] { color: #669eff; } span[role='property'] { color: #ff6e4a; } span[role='index'] { color: #808080; } span[role='string'] { color: #c88953; } span[role='null'] { color: #c274c2; } span[role='number'] { color: #ad99ff; } span[role='boolean'] { color: #c274c2; } span[role='expand'], span[role='collapse'] { font-family: monospace; color: #808080; margin-left: 0.33rem; cursor: pointer; user-select: none; } ul, ol { list-style: none; padding-left: 1rem; margin: 0; } & > ul, & > ol { padding: 0; } ` export const ObjectTreeNode: FunctionComponent<{ object: { [key: string]: any } level: number }> = ({ object, level }) => { if (!(typeof object === 'object' && object?.constructor === Object)) { console.error('Invalid Object') console.debug(object) } const children = toPairs(object).map(([key, child]) => { if (isString(child)) return StringRenderer(key, child) if (isNumber(child)) return NumberRenderer(key, child) if (isBoolean(child)) return BooleanRenderer(key, child) if (isNil(child)) return NullRenderer(key) if (isArray(child)) return ( ) if (isObject(child)) return ( ) return StringRenderer(key, JSON.stringify(child)) }) return (
      {children}
    ) } export const ObjectTreerinator: FunctionComponent<{ object?: { [key: string]: any } }> = ({ object }) => ( {object && } ) ================================================ FILE: src/Utils/Objects.ts ================================================ export const isObject = (value: any) => typeof value === 'object' export function omit(object, keys) { const result = {} for (const key of Object.keys(object)) { if (!keys.includes(key)) { result[key] = object[key] } } return result } export function mapValues(object, fn) { const result = {} for (const key of Object.keys(object)) { result[key] = fn(object[key], key) } return result } export function flatten(array) { return array.flat() } export function compact(array) { return array.filter(Boolean) } export const isNil = value => value === null || value === undefined export const isUndefined = value => value === undefined ================================================ FILE: src/Utils/Pagination.ts ================================================ export const calculatePagination = ( offset: number, length: number, currentPage: number, setSearch: (search: string) => void, setCurrentPage: (page: number) => void, ): Pagination => { const lastIndex = length - 1 const start = (currentPage - 1) * offset const end1 = start + offset const end2 = Math.min(end1, length) const pages = Math.ceil(length / offset) const hasOnePage = pages === 1 const hasNextPage = currentPage < pages const hasPreviousPage = currentPage > 1 return { offset, length, lastIndex, start: Math.max(start, 0), end: end2, pages, hasOnePage, hasNextPage, hasPreviousPage, currentPage, setCurrentPage, pageItems: Math.min(length, end2), setSearch(search: string) { setSearch(search) }, next() { if (hasNextPage) { setCurrentPage(currentPage + 1) } }, prev() { if (hasPreviousPage) { setCurrentPage(currentPage - 1) } }, } } ================================================ FILE: src/Utils/StringUtils.ts ================================================ import memoize from 'lodash.memoize' export const isString = (value: any) => typeof value === 'string' export namespace StringUtils { export const classPrefix = 'mde' export const truncate = (str: string, max: number = 40) => { return isString(str) && str.length > max ? [...str.slice(0, max), '...'] : str } /** * Five levels of brightness from 1 to 5. * * @param brightness */ export const getRandomColor = (brightness: number) => { if (brightness < 1 || brightness > 5) throw new Error( 'Only five brightness levels, from 1 to 5, are acceptable.', ) const variance = 255 / 5 const getByte = () => Math.round(variance * (brightness - 1) + Math.random() * variance) const rgb = [0, 0, 0].map(() => getByte()).join(',') return `rgb(${rgb})` } export const toClipboard = (data: string, mimeType = 'text/plain') => { document.addEventListener('copy', function (event: ClipboardEvent) { event.clipboardData?.setData(mimeType, data) event.preventDefault() }) document.execCommand('copy', false) } export const getSize = memoize((content: string) => new Blob([content]).size) export function getPrefixedClass(className) { return `${classPrefix}-${className}` } } ================================================ FILE: src/Utils/index.ts ================================================ import { DEVELOPMENT } from '@/Constants' import browser from 'webextension-polyfill' import { isNil } from './Objects' export const inDevelopmentOnly = (callback: () => any) => { if (DEVELOPMENT) { console.trace('DEVELOPMENT ONLY') callback() } } export const checkFirefoxBrowser = async (): Promise => { const { name } = (await browser.runtime.getBrowserInfo?.()) || {} return name === 'Firefox' } export const exists = (value: any) => !isNil(value) ================================================ FILE: src/index.d.ts ================================================ declare module '*.gif' declare module '*.png' type MeteorID = string interface Window { __meteor_devtools_evolved: boolean __meteor_devtools_evolved_receiveMessage(message: Message): void } declare namespace Meteor { const connection: any const gitCommitHash: string | undefined | null } type MessageSource = 'meteor-devtools-evolved' type EventType = | 'ddp-event' | 'minimongo-get-collections' | 'ddp-run-method' | 'console' | 'sync-subscriptions' | 'stats' | 'meteor-data-performance' | 'cache:clear' interface Message { eventType: EventType data: T } interface IMessagePayload extends Message { source: MessageSource } declare interface StackTrace { url: string callee: string } interface DDPError { isClientSafe: boolean error: number reason: string message: string errorType: string } interface DDPLogContent { msg?: string collection?: string session?: string id?: string method?: string result?: string name?: string error?: DDPError subs?: string[] } interface DDPLog { id: string content: string parsedContent?: DDPLogContent trace?: StackTrace[] isInbound?: boolean isOutbound?: boolean timestamp?: number timestampPretty?: string timestampLong?: string size?: number sizePretty?: string host?: string filterType?: FilterType | null preview?: string } interface Bookmark { id?: string timestamp: number log: DDPLog } type FilterType = | 'heartbeat' | 'subscription' | 'collection' | 'method' | 'connection' type FilterTypeMap = { [key in FilterType]: T } interface Pagination { readonly offset: number readonly length: number readonly lastIndex: number readonly start: number readonly end: number readonly pages: number readonly currentPage: number readonly hasOnePage: boolean readonly hasNextPage: boolean readonly hasPreviousPage: boolean readonly pageItems: number setSearch(search: string): void setCurrentPage(page: number): void next(): void prev(): void } interface IDocument extends Record { _id: string } type MinimongoCollections = Record type RawCollections = Record type ViewableObject = object | null type MessageHandler = (message: Message) => void interface IDocumentWrapper { collectionName: string document: IDocument _string: string _size: number } interface IGitHubRepository { id: number node_id: string name: string full_name: string private: boolean owner: { login: string id: number node_id: string avatar_url: string gravatar_id: string url: string html_url: string followers_url: string following_url: string gists_url: string starred_url: string subscriptions_url: string organizations_url: string repos_url: string events_url: string received_events_url: string type: string site_admin: boolean } html_url: string description: string fork: boolean url: string forks_url: string keys_url: string collaborators_url: string teams_url: string hooks_url: string issue_events_url: string events_url: string assignees_url: string branches_url: string tags_url: string blobs_url: string git_tags_url: string git_refs_url: string trees_url: string statuses_url: string languages_url: string stargazers_url: string contributors_url: string subscribers_url: string subscription_url: string commits_url: string git_commits_url: string comments_url: string issue_comment_url: string contents_url: string compare_url: string merges_url: string archive_url: string downloads_url: string issues_url: string pulls_url: string milestones_url: string notifications_url: string labels_url: string releases_url: string deployments_url: string created_at: string updated_at: string pushed_at: string git_url: string ssh_url: string clone_url: string svn_url: string homepage: string size: number stargazers_count: number watchers_count: number language: string has_issues: boolean has_projects: boolean has_downloads: boolean has_wiki: boolean has_pages: boolean forks_count: number mirror_url: string | null archived: boolean disabled: boolean open_issues_count: number license: { key: string name: string spdx_id: string url: string node_id: string } forks: number open_issues: number watchers: number default_branch: string temp_clone_token: string | null network_count: number subscribers_count: number } interface ISettings { repositoryData: IGitHubRepository | null activeFilterBlacklist: string[] activeFilters: FilterTypeMap } type ConsoleType = 'log' | 'info' | 'warn' | 'error' interface IMeteorSubscription { id: string name: string params: any[] inactive: boolean ready: boolean } interface ICollectionMetadata { [key: string]: { collectionSize: number collectionSizePretty: string } } type CallData = { collectionName: string key: string args: string runtime: number } ================================================ FILE: tailwind.config.js ================================================ module.exports = { darkMode: 'class', content: ['./src/**/*.{js,jsx,ts,tsx}'], theme: { extend: {}, }, plugins: [require('daisyui')], daisyui: { styled: true, themes: true, base: true, utils: true, logs: false, rtl: false, prefix: '', darkTheme: 'light', }, } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "strict": false, "noImplicitAny": false, "target": "ES6", "module": "commonjs", "moduleResolution": "node", "experimentalDecorators": true, "emitDecoratorMetadata": true, "useDefineForClassFields": true, "sourceMap": true, "esModuleInterop": true, "jsx": "react", "baseUrl": "./", "paths": { "@/*": ["src/*"] } }, "include": ["src/*.d.ts", "src/**/*.ts", "src/**/*.tsx"] } ================================================ FILE: webpack/base.js ================================================ const path = require('path') const { merge } = require('webpack-merge') const { DefinePlugin } = require('webpack') const { getTypeScriptAliases } = require('./utils') const src = path.join(__dirname, '../src/') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const CopyPlugin = require('copy-webpack-plugin') const aliases = getTypeScriptAliases() const manifestVersion = { chrome: 3, firefox: 2, } module.exports = (browser = 'chrome', override) => { const extDir = path.join(__dirname, `../extension`) const distPath = `${extDir}/${browser}/dist/` return merge( { entry: { bundle: path.resolve(src, 'App.tsx'), inject: path.resolve(src, 'Browser', 'Inject.ts'), background: path.resolve(src, 'Browser', 'Background.ts'), content: path.resolve(src, 'Browser', 'Content.ts'), devtools: path.resolve(src, 'Browser', 'DevTools.ts'), }, output: { chunkFilename: '[name].js', path: distPath, publicPath: '/dist/', }, plugins: [ new CleanWebpackPlugin(), new DefinePlugin({ 'process.env.MODE': JSON.stringify(override.mode), }), new CopyPlugin({ patterns: [ { from: extDir, to: `${extDir}/${browser}`, globOptions: { dot: true, gitignore: true, ignore: [ '**/manifest-v2.json', '**/manifest-v3.json', '**/firefox', '**/chrome', ], }, }, { from: `${extDir}/manifest-v${manifestVersion[browser]}.json`, to: `${extDir}/${browser}/manifest.json`, }, ], }), ], module: { rules: [ { parser: { amd: false, }, }, { test: /\.js/, use: 'babel-loader', include: src, }, { test: /\.tsx?$/, use: 'ts-loader' }, { test: /\.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'], }, { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], }, { test: /\.(gif|png|jpg)$/, type: 'asset/resource', generator: { filename: 'assets/[name][ext]', }, }, ], }, resolve: { alias: aliases, extensions: [ '.css', '.eot', '.js', '.json', '.jsx', '.mjs', '.sass', '.scss', '.ttf', '.gif', '.ts', '.tsx', '.woff', '.jpg', '.png', ], }, performance: { hints: false, }, }, override, ) } ================================================ FILE: webpack/chrome.dev.js ================================================ const base = require('./base') module.exports = base('chrome', { watch: true, mode: 'development', devtool: 'inline-source-map', stats: { modules: false, }, }) ================================================ FILE: webpack/chrome.prod.js ================================================ const base = require('./base') const TerserPlugin = require('terser-webpack-plugin') module.exports = base('chrome', { mode: 'production', optimization: { minimize: true, minimizer: [new TerserPlugin()], }, }) ================================================ FILE: webpack/firefox.dev.js ================================================ const base = require('./base') module.exports = base('firefox', { watch: true, mode: 'development', devtool: 'inline-source-map', stats: { modules: false, }, }) ================================================ FILE: webpack/firefox.prod.js ================================================ const base = require('./base') const TerserPlugin = require('terser-webpack-plugin') module.exports = base('firefox', { mode: 'production', optimization: { minimize: true, minimizer: [new TerserPlugin()], }, }) ================================================ FILE: webpack/utils.js ================================================ const { resolve } = require('path') const { toPairs } = require('lodash') const getTypeScriptAliases = () => { const { paths } = require('../tsconfig').compilerOptions console.log(toPairs(paths)) return toPairs(paths).reduce( (acc, [key, item]) => ({ ...acc, [key.replace('/*', '')]: resolve( __dirname, '..', item[0].replace('/*', '').replace('*', ''), ), }), {}, ) } module.exports = { getTypeScriptAliases }