Repository: FormidableLabs/webpack-dashboard Branch: master Commit: 9333a27e4b8f Files: 52 Total size: 86.7 KB Directory structure: gitextract_wps1zf6f/ ├── .changeset/ │ ├── cold-wolves-occur.md │ └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github/ │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .mocharc.yml ├── .npmignore ├── .nycrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── bin/ │ └── webpack-dashboard.js ├── dashboard/ │ └── index.js ├── docs/ │ └── getting-started.md ├── examples/ │ ├── .eslintrc.json │ ├── config/ │ │ ├── webpack.config.js │ │ └── webpack.config.ts │ ├── duplicates-esm/ │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ ├── simple/ │ │ ├── package.json │ │ └── src/ │ │ └── index.js │ └── tree-shaking/ │ ├── package.json │ └── src/ │ └── index.js ├── index.js ├── package.json ├── plugin/ │ ├── index.d.ts │ └── index.js ├── test/ │ ├── .eslintrc.json │ ├── base.spec.js │ ├── bin/ │ │ └── webpack-dashboard.spec.js │ ├── dashboard/ │ │ └── index.spec.js │ ├── plugin/ │ │ └── index.spec.js │ ├── setup.js │ └── utils/ │ ├── format-assets.spec.js │ ├── format-modules.spec.js │ ├── format-output.spec.js │ └── format-versions.spec.js └── utils/ ├── error-serialization.js ├── format-assets.js ├── format-duplicates.js ├── format-modules.js ├── format-output.js ├── format-problems.js └── format-versions.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/cold-wolves-occur.md ================================================ --- "webpack-dashboard": patch --- '#359 update node version to 18 in github actions workflows and github actions versions' ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", "changelog": [ "@svitejs/changesets-changelog-github-compact", { "repo": "FormidableLabs/webpack-dashboard" } ], "access": "public", "baseBranch": "master" } ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 100 [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ dist-* ================================================ FILE: .eslintrc.json ================================================ { "extends": ["formidable/configurations/es6-node", "plugin:prettier/recommended"], "rules": { "func-style": "off", "arrow-parens": ["error", "as-needed"] } } ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master pull_request: branches: - master jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] node-version: [18.x] steps: # Setup - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: cache: "yarn" node-version: ${{ matrix.node-version }} # Installation - run: yarn --version - run: yarn install --frozen-lockfile env: CI: true # CI - run: yarn check-ci # Test - run: yarn test # Code coverage - run: yarn codecov ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - master jobs: release: name: Release runs-on: ubuntu-latest permissions: contents: write id-token: write issues: write repository-projects: write deployments: write packages: write pull-requests: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: cache: "yarn" node-version: 18 - name: Install dependencies run: yarn install --frozen-lockfile - name: Check CI run: yarn check-ci - name: Unit Tests run: yarn test - name: PR or Publish id: changesets uses: changesets/action@v1 with: version: yarn changeset version publish: yarn changeset publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .gitignore ================================================ # dependencies /node_modules/ # misc .DS_Store npm-debug.log* .nyc_output .coverage yarn-error.log package-lock.json dist-* .vscode .lankrc.js ================================================ FILE: .mocharc.yml ================================================ require: "./test/setup.js" recursive: true ================================================ FILE: .npmignore ================================================ /* !/bin !/dashboard !/plugin !/utils !LICENSE !CHANGELOG.md !README.md !package.json !index.js !index.d.ts ================================================ FILE: .nycrc ================================================ { "reporter": [ "html", "lcov", "text" ], "report-dir": "./.coverage" } ================================================ FILE: .prettierignore ================================================ examples/**/dist-*/*.js examples/**/dist-*/*.json ================================================ FILE: .prettierrc ================================================ { "arrowParens": "avoid", "trailingComma": "none", "endOfLine": "auto" } ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 3.3.8 ### Patch Changes - Adding GitHub release workflow ([#354](https://github.com/FormidableLabs/webpack-dashboard/pull/354)) ## 3.3.7 - Bug: Move plugin types and update to webpack v5. [#324](https://github.com/FormidableLabs/webpack-dashboard/issues/324) ## 3.3.6 - Bug: Allow socket messages to be null. [#335](https://github.com/FormidableLabs/webpack-dashboard/issues/335), [#336](https://github.com/FormidableLabs/webpack-dashboard/issues/336) ## [3.3.5] - 2021-07-12 - Chore: Update dependencies. [#333](https://github.com/FormidableLabs/webpack-dashboard/issues/333) - Coverage: Add CodeCov stats. [#206](https://github.com/FormidableLabs/webpack-dashboard/issues/206) - CI: Update Node matrix to 12/14/16. ## [3.3.4] - 2021-07-12 - Chore: Refactor internal stats consumption to perform `inspectpack` analysis in the main thread, without using `main` streams. - Chore: Refactor internal handler in plugin to always be a wrapped function so that we can't accidentally have asynchronous code call the handler function after it is removed / nulled. - Bugfix: Add message counting delayed cleanup in plugin to allow messages to drain in Dashboard. Fixes [#294](https://github.com/FormidableLabs/webpack-dashboard/issues/294). ## [3.3.3] - 2021-05-05 - Security: Update `socket.io` version to get rid of vulnerable `xmlhttprequest-ssl` package. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/325 by @texpert. ## [3.3.2] - 2021-05-05 - Empty publish. ## [3.3.1] - 2021-01-29 - Bugfix: Ensure `Status` is properly updating and reaches completion. Fixes #321 ## [3.3.0] - 2021-01-21 - Add `webpack@5` support. Closes #316 - Bugfix: `webpack@5` warning message conflict. Fixes #314 - Update various production dependencies. ## [3.2.1] - 2020-08-24 - Add missing dependency on `chalk`. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/309 by @am-a. ## [3.2.0] - 2019-09-08 - Add left / right navigation keys to assets in Modules and Problems screens. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/288 by @wapgear. ## [3.1.0] - 2019-08-27 - Add `DashboardPlugin({ includeAssets: [ "stringPrefix", /regexObj/ ] })` Webpack plugin filtering option. - Add `webpack-dashboard --include-assets stringPrefix1 -a stringPrefix2` CLI filtering option. - Change `"mode"` SocketIO event to `"options"` as it now passes both `minimal` and `includeAssets` from CLI to the Webpack plugin. - Fix unit tests that incorrectly relied on `.complete()` for `most` observables. - Add additional `examples` fixture for development. ## [3.0.7] - 2019-05-15 ### Features - Very minor path normalization for displaying modules paths on Windows and Prettier fixes for Windows. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/284 by @ryan-roemer. - Add AppVeyor for Windows builds in CI. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/284 by @ryan-roemer. ### Migration Instructions No changes required to start using v3.0.7 🎉. ## [3.0.6] - 2019-05-09 ### Features - Prevent dashboard from spawning its own console for the child process on Windows. Closes #212. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/284 by @snack-able. ### Migration Instructions No changes required to start using v3.0.6 🎉. ## [3.0.5] - 2019-04-24 ### Features - Use `npm-run-all` as task runner for `package.json` scripts. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/283. - Use `test` in lieu of `test-summary` for `nyc` coverage reporting on command line. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/283. ### Security - Address `handlebars` security vulnerability. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/282 by @juliusl. - Address additional security vulnerabilities in `js-yaml`. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/283. ### Migration Instructions No changes required to start using v3.0.5 🎉. ## [3.0.4] - 2019-04-24 [DEPRECATED] `v3.0.4` was an erroneous publish. ## [3.0.3] - 2019-04-18 ### Bugs - **Socket.io disconnects / large stats object size**: Dramatically reduce the size of the webpack stats object being sent from client (webpack plugin) to server (CLI). Add client error/disconnect information for better future debugging. Original issue: https://github.com/FormidableLabs/inspectpack/issues/279 and fix: https://github.com/FormidableLabs/inspectpack/pull/281 ### Migration Instructions No changes required to start using v3.0.3 🎉. ## [3.0.2] - 2019-03-28 ### Features - Upgrade `inspectpack` dependency to handle `null` chunks. Original issue: https://github.com/FormidableLabs/inspectpack/issues/110 and upstream fix: https://github.com/FormidableLabs/inspectpack/pull/111 ### Migration Instructions No changes required to start using v3.0.2 🎉. ## [3.0.1] - 2019-03-26 ### Features - Use `process.kill` with `SIGINT` to gracefully exit the dashboard process. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/277 by @joakimbeng. - Update dependencies to address security warnings. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/275 by @stereobooster. ### Migration Instructions No changes required to start using v3.0.1 🎉. We do recommend adopting this patch as soon as possible to get the security upgrades. ## [3.0.0] - 2019-02-14 ### Features - Migrated from using `blessed` to [`neo-blessed`](https://github.com/embark-framework/neo-blessed) as the underlying terminal renderer. `neo-blessed` is a maintained fork of `blessed` and brings in some nice fixes for us. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/270 - Added Prettier to the codebase 🎉 Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/270 ### Docs - Added a warning about deprecation of Node 6 support. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/270 ### Migration Instructions With this release we are dropping support for Node 6 altogether. `neo-blessed` requires Node [>= 8.0.0](https://github.com/embark-framework/neo-blessed/blob/master/package.json#L38), meaning all users of the dashboard will need to run it using Node 8 or above. Previous versions of `webpack-dashboard` are still compatible with Node 6. ## [2.1.0] - 2019-01-29 ### Features - Added a few example setups to make the local development experience with `webpack-dashboard` a lot easier. Users can now clone the repo, `yarn`, and `yarn dev` to get running. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/267 - Migrated to `inspectpack@4`. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/263 - Added TypeScript defitions. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/269 ### Tests - Added regression tests to fix an unknown import issue for our `format-*` utils. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/263 - Added tests for all `Dashboard` methods. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/263 ### Docs - Added a Local Development section to the README to make it easier to contribute to `webpack-dashboard`. Included in: https://github.com/FormidableLabs/webpack-dashboard/pull/267 ### Migration Instructions No changes required to start using v2.1.0 🎉 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributing to Webpack-Dashboard ## Contributor Covenant Code of Conduct ### Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ### Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ### Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ### Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ### Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at lauren.eastridge@formidable.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ### Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ ## Development ### Installing dependencies ```sh yarn install ``` ### Testing You will find tests for files colocated with `*.test.ts` suffixes. Whenever making any changes, ensure that all existing tests pass by running `yarn test`. If you are adding a new feature or some extra functionality, you should also make sure to accompany those changes with appropriate tests. ### Linting and Formatting Before committing any changes, be sure to do `yarn lint`; this will lint all relevant files using [ESLint](http://eslint.org/) and report on any changes that you need to make. ### Before submitting a PR... Thanks for taking the time to help us make webpack-dashboard even better! Before you go ahead and submit a PR, make sure that you have done the following: - Run the tests using `yarn test` - Run lint and flow using `yarn lint` - Run `yarn changeset` ### Using changesets Our official release path is to use automation to perform the actual publishing of our packages. The steps are to: 1. A human developer adds a changeset. Ideally this is as a part of a PR that will have a version impact on a package. 2. On merge of a PR our automation system opens a "Version Packages" PR. 3. On merging the "Version Packages" PR, the automation system publishes the packages. Here are more details: ### Add a changeset When you would like to add a changeset (which creates a file indicating the type of change), in your branch/PR issue this command: ```sh $ yarn changeset ``` to produce an interactive menu. Navigate the packages with arrow keys and hit `` to select 1+ packages. Hit `` when done. Select semver versions for packages and add appropriate messages. From there, you'll be prompted to enter a summary of the change. Some tips for this summary: 1. Aim for a single line, 1+ sentences as appropriate. 2. Include issue links in GH format (e.g. `#123`). 3. You don't need to reference the current pull request or whatnot, as that will be added later automatically. After this, you'll see a new uncommitted file in `.changesets` like: ```sh $ git status # .... Untracked files: (use "git add ..." to include in what will be committed) .changeset/flimsy-pandas-marry.md ``` Review the file, make any necessary adjustments, and commit it to source. When we eventually do a package release, the changeset notes and version will be incorporated! ### Creating versions On a merge of a feature PR, the changesets GitHub action will open a new PR titled `"Version Packages"`. This PR is automatically kept up to date with additional PRs with changesets. So, if you're not ready to publish yet, just keep merging feature PRs and then merge the version packages PR later. ### Publishing packages On the merge of a version packages PR, the changesets GitHub action will publish the packages to npm. ================================================ FILE: ISSUE_TEMPLATE.md ================================================ #### Please provide a description and details of the bug / issue below: --- #### If the issue is visual, please provide screenshots here --- #### Steps to reproduce the problem --- #### Please provide a gist of relevant files 1. package.json (specifically the script you are using to start the dashboard) 2. webpack.config.js 3. index.js (Your express based dev server, if applicable) --- #### More Details - What operating system are you on? - What terminal application are you using? - What version of webpack-dashboard are you using? - What is the output of running `echo $TERM`? ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016-present, Formidable Labs. All rights reserved. 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 ================================================ [![Webpack Dashboard — Formidable, We build the modern web](https://raw.githubusercontent.com/FormidableLabs/webpack-dashboard/master/webpack-dashboard-Hero.png)](https://formidable.com/open-source/) [![npm version][npm_img]][npm_site] [![Actions Status][actions_img]][actions_site] [![Coverage Status][cov_img]][cov_site] [![Maintenance Status][maintenance-image]](#maintenance-status) A CLI dashboard for your webpack dev server ### What's this all about? When using webpack, especially for a dev server, you are probably used to seeing something like this: ![https://i.imgur.com/p1uAqkD.png](https://i.imgur.com/p1uAqkD.png) That's cool, but it's mostly noise and scrolly and not super helpful. This plugin changes that. Now when you run your dev server, you basically work at NASA: ![https://i.imgur.com/qL6dXJd.png](https://i.imgur.com/qL6dXJd.png) ### Install ```sh $ npm install --save-dev webpack-dashboard # ... or ... $ yarn add --dev webpack-dashboard ``` > ℹ️ **Note**: You can alternatively globally install the dashboard (e.g. `npm install -g webpack-dashboard`) for use with any project and everything should work the same. ### Use **`webpack-dashboard@^3.0.0` requires Node 8 or above.** Previous versions support down to Node 6. First, import the plugin and add it to your webpack config: ```js // Import the plugin: const DashboardPlugin = require("webpack-dashboard/plugin"); // Add it to your webpack configuration plugins. module.exports = { // ... plugins: [new DashboardPlugin()]; // ... }; ``` Then, modify your dev server start script previously looked like: ```js "scripts": { "dev": "node index.js", # OR "dev": "webpack-dev-server", # OR "dev": "webpack", } ``` You would change that to: ```js "scripts": { "dev": "webpack-dashboard -- node index.js", # OR "dev": "webpack-dashboard -- webpack-dev-server", # OR "dev": "webpack-dashboard -- webpack", } ``` Now you can just run your start script like normal, except now, you are awesome. Not that you weren't before. I'm just saying. More so. #### Customizations More configuration customization examples can be found in our [getting started](./docs/getting-started.md) guide. For example, if you want to use a custom port of `webpack-dashboard` to communicate between its plugin and CLI tool, you would first set the number in the options object in webpack configuration: ```js plugins: [new DashboardPlugin({ port: 3001 })]; ``` Then, you would pass it along to the CLI to match: ```sh $ webpack-dashboard --port 3001 -- webpack ``` > ⚠️ **Warning**: When choosing a custom port, you need to find one that is **not** already in use. You should not choose one that is being used by `webpack-dev-server` / `devServer` or any other process. Instead, pick one that is **only** for `webpack-dashboard` and pair that up in the plugin configuration and CLI port flag. ### Run it Finally, start your server using whatever command you have set up. Either you have `npm run dev` or `npm start` pointed at `node devServer.js` or something along those lines. Then, sit back and pretend you're an astronaut. ### Supported Operating Systems and Terminals **macOS →** Webpack Dashboard works in Terminal, iTerm 2, and Hyper. For mouse events, like scrolling, in Terminal you will need to ensure _View → Enable Mouse Reporting_ is enabled. This is supported in macOS El Capitan, Sierra, and High Sierra. In iTerm 2, to select full rows of text hold the ⌥ Opt key. To select a block of text hold the ⌥ Opt + ⌘ Cmd key combination. **Windows 10 →** Webpack Dashboard works in Command Prompt, PowerShell, and Linux Subsystem for Windows. Mouse events are not supported at this time, as discussed further in the documentation of the underlying terminal library we use [Blessed](https://github.com/chjj/blessed#windows-compatibility). The main log can be scrolled using the , , Page Up, and Page Down keys. **Linux →** Webpack Dashboard has been verified in the built-in terminal app for Debian-based Linux distributions such as Ubuntu or Mint. Mouse events and scrolling are supported automatically. To highlight or select lines hold the ⇧ Shift key. ### API #### webpack-dashboard (CLI) ##### Options - `-c, --color [color]` - Custom ANSI color for your dashboard - `-m, --minimal` - Runs the dashboard in minimal mode - `-t, --title [title]` - Set title of terminal window - `-p, --port [port]` - Custom port for socket communication server - `-a, --include-assets [string prefix]` - Limit display to asset names matching string prefix (option can be repeated and is concatenated to `new DashboardPlugin({ includeAssets })` options array) ##### Arguments `[command]` - The command you want to run, i.e. `webpack-dashboard -- node index.js` #### Webpack plugin #### Options - `host` - Custom host for connection the socket client - `port` - Custom port for connecting the socket client - `includeAssets` - Limit display to asset names matching string prefix or regex (`Array`) - `handler` - Plugin handler method, i.e. `dashboard.setData` _Note: you can also just pass a function in as an argument, which then becomes the handler, i.e. `new DashboardPlugin(dashboard.setData)`_ ### Local Development We've standardized our local development process for `webpack-dashboard` on using `yarn`. We recommend using `yarn 1.10.x+`, as these versions include the `integrity` checksum. The checksum helps to verify the integrity of an installed package before its code is executed. 🚀 To run this repo locally against our provided examples, take the usual steps. ```sh yarn yarn dev ``` We re-use a small handful of the fixtures from [`inspectpack`](https://github.com/FormidableLabs/inspectpack) so that you can work locally on the dashboard while simulating common `node_modules` dependency issues you might face in the wild. These live in `/examples`. To change the example you're working against, simply alter the `EXAMPLE` env variable in the `dev` script in `package.json` to match the scenario you want to run in `/examples`. For example, if you want to run the `tree-shaking` example, change the `dev` script from this: ```sh $ cross-env EXAMPLE=duplicates-esm \ node bin/webpack-dashboard.js -- \ webpack-cli --config examples/config/webpack.config.js --watch ``` to this: ```sh $ cross-env EXAMPLE=tree-shaking WEBPACK_MODE=production \ node bin/webpack-dashboard.js -- \ webpack-cli --config examples/config/webpack.config.js --watch ``` Then just run `yarn dev` to get up and running. PRs are very much appreciated! ## Contributing Please see our [contributing guide](CONTRIBUTING.MD). #### Credits Module output deeply inspired by: [https://github.com/robertknight/webpack-bundle-size-analyzer](https://github.com/robertknight/webpack-bundle-size-analyzer) Error output deeply inspired by: [https://github.com/facebookincubator/create-react-app](https://github.com/facebookincubator/create-react-app) #### Maintenance Status **Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. [maintenance-image]: https://img.shields.io/badge/maintenance-active-green.svg?color=brightgreen&style=flat [npm_img]: https://img.shields.io/npm/v/webpack-dashboard.svg?style=flat [npm_site]: https://www.npmjs.com/package/webpack-dashboard [actions_img]: https://github.com/FormidableLabs/webpack-dashboard/workflows/CI/badge.svg [actions_site]: https://github.com/FormidableLabs/webpack-dashboard/actions [cov_img]: https://codecov.io/gh/FormidableLabs/webpack-dashboard/branch/master/graph/badge.svg [cov_site]: https://codecov.io/gh/FormidableLabs/webpack-dashboard ================================================ FILE: bin/webpack-dashboard.js ================================================ #!/usr/bin/env node "use strict"; const commander = require("commander"); const spawn = require("cross-spawn"); const Dashboard = require("../dashboard/index"); const io = require("socket.io"); const DEFAULT_PORT = 9838; const pkg = require("../package.json"); const collect = (val, prev) => prev.concat([val]); // Wrap up side effects in a script. // eslint-disable-next-line max-statements, complexity const main = opts => { opts = opts || {}; const argv = typeof opts.argv === "undefined" ? process.argv : opts.argv; const isWindows = process.platform === "win32"; const program = new commander.Command("webpack-dashboard") .version(pkg.version) .option("-c, --color [color]", "Dashboard color") .option("-m, --minimal", "Minimal mode") .option("-t, --title [title]", "Terminal window title") .option("-p, --port [port]", "Socket listener port") .option("-a, --include-assets [string prefix]", "Asset names to limit to", collect, []) .usage("[options] -- [script] [arguments]") .parse(argv); const cliOpts = program.opts(); const cliArgs = program.args; let logFromChild = true; let child; if (!cliArgs.length) { logFromChild = false; } if (logFromChild) { const command = cliArgs[0]; const args = cliArgs.slice(1); const env = process.env; env.FORCE_COLOR = true; child = spawn(command, args, { env, stdio: [null, null, null, null], detached: !isWindows }); } const dashboard = new Dashboard({ color: cliOpts.color || "green", minimal: cliOpts.minimal || false, title: cliOpts.title || null }); const port = parseInt(cliOpts.port || DEFAULT_PORT, 10); const server = opts.server || io(port); server.on("error", err => { // eslint-disable-next-line no-console console.log(err); }); if (logFromChild) { server.on("connection", socket => { socket.emit("options", { minimal: cliOpts.minimal || false, includeAssets: cliOpts.includeAssets || [] }); socket.on("message", (message, ack) => { // Note: `message` may be null. // https://github.com/FormidableLabs/webpack-dashboard/issues/335 if (message && message.type !== "log") { dashboard.setData(message, ack); } }); }); child.stdout.on("data", data => { dashboard.setData([ { type: "log", value: data.toString("utf8") } ]); }); child.stderr.on("data", data => { dashboard.setData([ { type: "log", value: data.toString("utf8") } ]); }); process.on("exit", () => { process.kill(isWindows ? child.pid : -child.pid); }); } else { server.on("connection", socket => { socket.on("message", (message, ack) => { dashboard.setData(message, ack); }); }); } }; if (require.main === module) { main(); } module.exports = main; ================================================ FILE: dashboard/index.js ================================================ "use strict"; const chalk = require("chalk"); const blessed = require("neo-blessed"); const { formatOutput } = require("../utils/format-output"); const { formatModules } = require("../utils/format-modules"); const { formatAssets } = require("../utils/format-assets"); const { formatProblems } = require("../utils/format-problems"); const { deserializeError } = require("../utils/error-serialization"); const PERCENT_MULTIPLIER = 100; const DEFAULT_SCROLL_OPTIONS = { scrollable: true, input: true, alwaysScroll: true, scrollbar: { ch: " ", inverse: true }, keys: true, vi: true, mouse: true }; class Dashboard { // eslint-disable-next-line max-statements constructor(options) { // Options, params options = options || {}; const title = options.title || "webpack-dashboard"; this.color = options.color || "green"; this.minimal = options.minimal || false; this.stats = null; // Data binding, lookup tables. this.actionForMessageType = { progress: this.setProgress.bind(this), operations: this.setOperations.bind(this), status: this.setStatus.bind(this), stats: this.setStats.bind(this), log: this.setLog.bind(this), clear: this.clear.bind(this), sizes: _data => { if (this.minimal) { return; } if (_data.value instanceof Error) { this.setSizesError(_data.value); } else { this.setSizes(_data); } }, problems: _data => { if (this.minimal) { return; } if (_data.value instanceof Error) { this.setProblemsError(_data.value); } else { this.setProblems(_data); } } }; // Start UI stuff. this.screen = blessed.screen({ title, smartCSR: true, dockBorders: false, fullUnicode: true, autoPadding: true }); this.layoutLog(); this.layoutStatus(); if (!this.minimal) { this.layoutModules(); this.layoutAssets(); this.layoutProblems(); } this.screen.key(["escape", "q", "C-c"], () => { process.kill(process.pid, "SIGINT"); }); this.screen.render(); } setData(dataArray, ack) { dataArray .map(data => data.error ? Object.assign({}, data, { value: deserializeError(data.value) }) : data ) .forEach(data => { this.actionForMessageType[data.type](data); }); this.screen.render(); // Send ack back if requested. if (ack) { ack(); } } setProgress(data) { const percent = parseInt(data.value * PERCENT_MULTIPLIER, 10); const formattedPercent = `${percent.toString()}%`; if (!percent) { this.progressbar.setProgress(percent); } if (this.minimal) { this.progress.setContent(formattedPercent); } else { this.progressbar.setContent(formattedPercent); this.progressbar.setProgress(percent); } } setOperations(data) { this.operations.setContent(data.value); } setStatus(data) { let content; switch (data.value) { case "Success": content = `{green-fg}{bold}${data.value}{/}`; break; case "Failed": content = `{red-fg}{bold}${data.value}{/}`; break; case "Error": content = `{red-fg}{bold}${data.value}{/}`; break; default: content = `{bold}${data.value}{/}`; } this.status.setContent(content); } setStats(data) { const stats = { hasErrors: () => data.value.errors, hasWarnings: () => data.value.warnings, toJson: () => data.value.data }; // Save for later when merging inspectpack sizes into the asset list this.stats = stats; if (stats.hasErrors()) { this.status.setContent("{red-fg}{bold}Failed{/}"); } this.logText.log(formatOutput(stats)); if (!this.minimal) { this.modulesMenu.setLabel(chalk.yellow("Modules (loading...)")); this.assets.setLabel(chalk.yellow("Assets (loading...)")); this.problemsMenu.setLabel(chalk.yellow("Problems (loading...)")); } } setSizes(data) { const { assets } = data.value; // Start with top-level assets. this.assets.setLabel("Assets"); this.assetTable.setData(formatAssets(assets)); // Then split modules across assets. const previousSelection = this.modulesMenu.selected; const modulesItems = Object.keys(assets).reduce( (memo, name) => Object.assign({}, memo, { [name]: () => { this.moduleTable.setData(formatModules(assets[name].files)); this.screen.render(); } }), {} ); this.modulesMenu.setLabel("Modules"); this.modulesMenu.setItems(modulesItems); this.modulesMenu.selectTab(previousSelection); // Final render. this.screen.render(); } setSizesError(err) { this.modulesMenu.setLabel(chalk.red("Modules (error)")); this.assets.setLabel(chalk.red("Assets (error)")); this.logText.log(chalk.red("Could not load module/asset sizes.")); this.logText.log(chalk.red(err)); } setProblems(data) { const { duplicates, versions } = data.value; // Separate across assets. // Use duplicates as the "canary" to get asset names. const assetNames = Object.keys(duplicates.assets); const previousSelection = this.problemsMenu.selected; const problemsItems = assetNames.reduce( (memo, name) => Object.assign({}, memo, { [name]: () => { this.problems.setContent( formatProblems({ duplicates: duplicates.assets[name], versions: versions.assets[name] }) ); this.screen.render(); } }), {} ); this.problemsMenu.setLabel("Problems"); this.problemsMenu.setItems(problemsItems); this.problemsMenu.selectTab(previousSelection); this.screen.render(); } setProblemsError(err) { this.problemsMenu.setLabel(chalk.red("Problems (error)")); this.logText.log(chalk.red("Could not analyze bundle problems.")); this.logText.log(chalk.red(err.stack)); } setLog(data) { if (this.stats && this.stats.hasErrors()) { return; } this.logText.log(data.value.replace(/[{}]/g, "")); } clear() { this.logText.setContent(""); } layoutLog() { this.log = blessed.box({ label: "Log", padding: 1, width: this.minimal ? "100%" : "75%", height: this.minimal ? "70%" : "36%", left: "0%", top: "0%", border: { type: "line" }, style: { fg: -1, border: { fg: this.color } } }); this.logText = blessed.log( Object.assign({}, DEFAULT_SCROLL_OPTIONS, { parent: this.log, tags: true, width: "100%-5" }) ); this.screen.append(this.log); this.mapNavigationKeysToScrollLog(); } mapNavigationKeysToScrollLog() { this.screen.key(["pageup"], () => { this.logText.setScrollPerc(0); this.logText.screen.render(); }); this.screen.key(["pagedown"], () => { // eslint-disable-next-line no-magic-numbers this.logText.setScrollPerc(100); this.logText.screen.render(); }); this.screen.key(["up"], () => { this.logText.scroll(-1); this.logText.screen.render(); }); this.screen.key(["down"], () => { this.logText.scroll(1); this.logText.screen.render(); }); this.screen.key(["left"], () => { const currentIndex = this.modulesMenu.selected; this.modulesMenu.selectTab(currentIndex - 1); this.problemsMenu.selectTab(currentIndex - 1); this.problemsMenu.screen.render(); this.modulesMenu.screen.render(); }); this.screen.key(["right"], () => { const currentIndex = this.modulesMenu.selected; this.modulesMenu.selectTab(currentIndex + 1); this.problemsMenu.selectTab(currentIndex + 1); this.problemsMenu.screen.render(); this.modulesMenu.screen.render(); }); } layoutModules() { this.modulesMenu = blessed.listbar({ label: "Modules", mouse: true, tags: true, width: "50%", height: "66%", left: "0%", top: "36%", border: { type: "line" }, padding: 1, style: { fg: -1, border: { fg: this.color }, prefix: { fg: -1 }, item: { fg: "white" }, selected: { fg: "black", bg: this.color } }, autoCommandKeys: true }); this.moduleTable = blessed.table( Object.assign({}, DEFAULT_SCROLL_OPTIONS, { parent: this.modulesMenu, height: "100%", width: "100%-5", padding: { top: 2, right: 1, left: 1 }, align: "left", data: [["Name", "Size", "Percent"]], tags: true }) ); this.screen.append(this.modulesMenu); } layoutAssets() { this.assets = blessed.box({ label: "Assets", tags: true, padding: 1, width: "50%", height: "28%", left: "50%", top: "36%", border: { type: "line" }, style: { fg: -1, border: { fg: this.color } } }); this.assetTable = blessed.table( Object.assign({}, DEFAULT_SCROLL_OPTIONS, { parent: this.assets, height: "100%", width: "100%-5", align: "left", padding: 1, data: [["Name", "Size"]] }) ); this.screen.append(this.assets); } layoutProblems() { this.problemsMenu = blessed.listbar({ label: "Problems", mouse: true, width: "50%", height: "38%", left: "50%", top: "63%", border: { type: "line" }, padding: { top: 1 }, style: { border: { fg: this.color }, prefix: { fg: -1 }, item: { fg: "white" }, selected: { fg: "black", bg: this.color } }, autoCommandKeys: true }); this.problems = blessed.box( Object.assign({}, DEFAULT_SCROLL_OPTIONS, { parent: this.problemsMenu, padding: 1, border: { fg: -1 }, style: { fg: -1, border: { fg: this.color } }, tags: true }) ); this.screen.append(this.problemsMenu); } // eslint-disable-next-line complexity layoutStatus() { this.wrapper = blessed.layout({ width: this.minimal ? "100%" : "25%", height: this.minimal ? "30%" : "36%", top: this.minimal ? "70%" : "0%", left: this.minimal ? "0%" : "75%", layout: "grid" }); this.status = blessed.box({ parent: this.wrapper, label: "Status", tags: true, padding: { left: 1 }, width: this.minimal ? "34%-1" : "100%", height: this.minimal ? "100%" : "34%", valign: "middle", border: { type: "line" }, style: { fg: -1, border: { fg: this.color } } }); this.operations = blessed.box({ parent: this.wrapper, label: "Operation", tags: true, padding: { left: 1 }, width: this.minimal ? "34%-1" : "100%", height: this.minimal ? "100%" : "34%", valign: "middle", border: { type: "line" }, style: { fg: -1, border: { fg: this.color } } }); this.progress = blessed.box({ parent: this.wrapper, label: "Progress", tags: true, padding: this.minimal ? { left: 1 } : 1, width: this.minimal ? "33%" : "100%", height: this.minimal ? "100%" : "34%", valign: "middle", border: { type: "line" }, style: { fg: -1, border: { fg: this.color } } }); this.progressbar = new blessed.ProgressBar({ parent: this.progress, height: 1, width: "90%", top: "center", left: "center", hidden: this.minimal, orientation: "horizontal", style: { bar: { bg: this.color } } }); this.screen.append(this.wrapper); } } module.exports = Dashboard; ================================================ FILE: docs/getting-started.md ================================================ # Getting Started with Webpack-Dashboard ## Install ```sh $ npm install --save-dev webpack-dashboard # ... or ... $ yarn add --dev webpack-dashboard ``` ## Use ***OS X Terminal.app users:*** Make sure that **View → Allow Mouse Reporting** is enabled, otherwise scrolling through logs and modules won't work. If your version of Terminal.app doesn't have this feature, you may want to check out an alternative such as [iTerm2](https://www.iterm2.com/index.html). First, import the plugin and add it to your webpack config: ```js // Import the plugin: const DashboardPlugin = require("webpack-dashboard/plugin"); // Add it to your webpack configuration plugins. module.exports = { // ... plugins: [new DashboardPlugin({ /* options */ })]; // ... }; ``` Because sockets use a port, the constructor now supports passing an options object that can include a custom port (if the default is giving you problems). If using a custom port, the port number must be included in the options object here, as well as passed using the -p flag in the call to webpack-dashboard. See how below: ```js plugins: [ new DashboardPlugin({ port: 3001 }) ] ``` The next step, is to call webpack-dashboard from your `package.json`. So if your dev server start script previously looked like: ```js "scripts": { "dev": "node index.js" } ``` You would change that to: ```js "scripts": { "dev": "webpack-dashboard -- node index.js" } ``` If you are using the webpack-dev-server script, you can do something like: ```js "scripts": { "dev": "webpack-dashboard -- webpack-dev-server --config ./webpack.dev.js" } ``` Again, the new version uses sockets, so if you want to use a custom port you must use the `-p` option to pass that: ```js "scripts": { "dev": "webpack-dashboard -p 3001 -- node index.js" } ``` You can also pass a supported ANSI color using the `-c` flag to custom colorize your dashboard: ```js "scripts": { "dev": "webpack-dashboard -c magenta -- node index.js" } ``` Now you can just run your start script like normal, except now, you are awesome. Not that you weren't before. I'm just saying. More so. ## Other usage We previously provided detailed guides for integration with `webpack-dev-server` and `express`, but as both of those projects now can be entirely configuration file based, we recommend just following the [webpack development server guide](https://webpack.js.org/guides/development/) to integrate with your appropriate development server setup of choice. ================================================ FILE: examples/.eslintrc.json ================================================ { "parserOptions": { "sourceType": "module" } } ================================================ FILE: examples/config/webpack.config.js ================================================ const { resolve } = require("path"); const { StatsWriterPlugin } = require("webpack-stats-plugin"); const { DuplicatesPlugin } = require("inspectpack/plugin"); const Dashboard = require("../../plugin"); const webpackPkg = require("webpack/package.json"); const webpackVers = webpackPkg.version.split(".")[0]; // Specify the directory of the example we're working with const cwd = `${process.cwd()}/examples/${process.env.EXAMPLE}`; if (!process.env.EXAMPLE) { throw new Error("EXAMPLE is required"); } const mode = process.env.WEBPACK_MODE || "development"; module.exports = { mode, devtool: false, context: resolve(cwd), entry: { bundle: "./src/index.js", // Hard-code path to the "hello world" no-dep entry for 2+ asset testing hello: "../simple/src/index.js" }, output: { path: resolve(cwd, `dist-${mode}-${webpackVers}`), pathinfo: true, filename: "[name].js" }, plugins: [ new StatsWriterPlugin({ fields: ["assets", "modules"], stats: { source: true // Needed for webpack5+ } }), new DuplicatesPlugin({ verbose: true, emitErrors: false }), new Dashboard({ // Optionally filter which assets to report on by string prefix or regex. // includeAssets: ["bundle", /bund/] }) ] }; ================================================ FILE: examples/config/webpack.config.ts ================================================ /** * No-op build with TS config to see if webpack-cli bombs out. */ import DashboardPlugin from '../../plugin'; import * as path from 'path'; import * as webpack from 'webpack'; const webpackVers = webpack.version; const cwd = `${process.cwd()}/examples/${process.env.EXAMPLE}`; if (!process.env.EXAMPLE) { throw new Error("EXAMPLE is required"); } const mode = process.env.WEBPACK_MODE || "development"; const config: webpack.Configuration = { mode: 'development', entry: { bundle: "./src/index.js" }, context: path.resolve(cwd), output: { path: path.resolve(cwd, `dist-ts-${mode}-${webpackVers}`), pathinfo: true, filename: "[name].js" }, devtool: false, plugins: [ new DashboardPlugin() ] }; export default config; ================================================ FILE: examples/duplicates-esm/package.json ================================================ { "name": "duplicates-esm", "version": "1.2.3", "description": "DUMMY APP", "main": "src/index.js", "dependencies": { "foo": "^1.0.0", "uses-foo": "^1.0.9" } } ================================================ FILE: examples/duplicates-esm/src/index.js ================================================ /* eslint-disable no-console*/ import { foo } from "foo"; import { usesFoo } from "uses-foo"; console.log("foo", foo()); console.log("usesFoo", usesFoo()); ================================================ FILE: examples/simple/package.json ================================================ { "name": "simple", "version": "1.2.3", "description": "DUMMY APP", "main": "src/index.js" } ================================================ FILE: examples/simple/src/index.js ================================================ /* eslint-disable no-console*/ const hello = () => "hello world"; console.log(hello()); ================================================ FILE: examples/tree-shaking/package.json ================================================ { "name": "tree-shaking", "version": "1.2.3", "description": "DUMMY APP", "main": "src/index.js", "dependencies": { "foo": "^1.0.0", "uses-foo": "^1.0.9" } } ================================================ FILE: examples/tree-shaking/src/index.js ================================================ /* eslint-disable no-console*/ import { red } from "foo"; import { usesRed } from "uses-foo"; console.log("red", red()); console.log("usesRed", usesRed()); ================================================ FILE: index.js ================================================ "use strict"; const dashboard = require("./dashboard/index"); module.exports = dashboard; ================================================ FILE: package.json ================================================ { "name": "webpack-dashboard", "version": "3.3.8", "description": "a CLI dashboard for webpack dev server", "bin": "bin/webpack-dashboard.js", "main": "index.js", "engines": { "node": ">=8.0.0" }, "types": "index.d.ts", "scripts": { "test": "mocha \"test/**/*.spec.js\"", "test-cov": "nyc mocha \"test/**/*.spec.js\"", "lint": "eslint .", "check": "run-s format-check lint test check-ts", "check-ci": "run-s format-check lint test-cov check-ts", "check-ts": "tsc plugin/index.d.ts examples/config/webpack.config.ts --noEmit", "dev": "cross-env EXAMPLE=duplicates-esm node bin/webpack-dashboard.js -- webpack-cli --config examples/config/webpack.config.js --watch", "dev-ts": "cross-env EXAMPLE=duplicates-esm node bin/webpack-dashboard.js -- webpack-cli --config examples/config/webpack.config.ts --watch", "format": "prettier --write \"./{bin,examples,plugin,test,utils}/**/*.js\"", "format-check": "prettier --list-different \"./{bin,examples,plugin,test,utils}/**/*.js\"" }, "repository": { "type": "git", "url": "git+https://github.com/FormidableLabs/webpack-dashboard.git" }, "keywords": [ "webpack", "cli", "plugin", "dashboard" ], "author": "Ken Wheeler", "license": "MIT", "bugs": { "url": "https://github.com/FormidableLabs/webpack-dashboard/issues" }, "homepage": "https://github.com/FormidableLabs/webpack-dashboard", "peerDependencies": { "webpack": "*" }, "dependencies": { "@changesets/cli": "^2.26.1", "chalk": "^4.1.1", "commander": "^8.0.0", "cross-spawn": "^7.0.3", "filesize": "^7.0.0", "handlebars": "^4.1.2", "inspectpack": "^4.7.1", "neo-blessed": "^0.2.0", "socket.io": "^4.1.3", "socket.io-client": "^4.1.3" }, "devDependencies": { "@svitejs/changesets-changelog-github-compact": "^0.1.1", "@types/node": "^22.1.0", "babel-eslint": "^10.1.0", "chai": "^4.3.4", "codecov": "^3.8.3", "cross-env": "^7.0.3", "eslint": "^7.30.0", "eslint-config-formidable": "^4.0.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-filenames": "^1.1.0", "eslint-plugin-import": "^2.23.4", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", "mocha": "^9.0.2", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "prettier": "^2.3.2", "sinon": "^11.1.1", "sinon-chai": "^3.7.0", "ts-node": "^10.4.0", "typescript": "^4.3.5", "webpack": "^5.44.0", "webpack-cli": "^4.7.2", "webpack-stats-plugin": "^1.0.3" }, "publishConfig": { "provenance": true } } ================================================ FILE: plugin/index.d.ts ================================================ interface IMessage { type: string; value: string | number | { [key: string]: any } error?: boolean; } interface IDashboardOptions { port?: number; host?: string; handler?: (dataArray: IMessage[]) => void; } interface ICompiler { hooks?: any; plugin?: (name: string, callback: () => void) => void; } export default class DashboardPlugin { constructor(options?: IDashboardOptions); apply(compiler: ICompiler): void; } ================================================ FILE: plugin/index.js ================================================ /* eslint-disable max-params, max-statements */ "use strict"; const webpack = require("webpack"); const io = require("socket.io-client"); const inspectpack = require("inspectpack"); const serializer = require("../utils/error-serialization"); const DEFAULT_PORT = 9838; const DEFAULT_HOST = "127.0.0.1"; const ONE_SECOND = 1000; const INSPECTPACK_PROBLEM_ACTIONS = ["duplicates", "versions"]; const INSPECTPACK_PROBLEM_TYPE = "problems"; const CLEANUP_MAX_NUM_TRIES = 3; // Try 3 times to close before giving up. const CLEANUP_RETRY_DELAY_MS = 100; // Delay before a retry. function noop() {} function getTimeMessage(timer) { let time = Date.now() - timer; if (time >= ONE_SECOND) { time /= ONE_SECOND; time = Math.round(time); time += "s"; } else { time += "ms"; } return ` (${time})`; } // Naive camel-casing. const camel = str => str.replace(/-([a-z])/, group => group[1].toUpperCase()); // Normalize webpack3 vs. 4 API differences. function _webpackHook(hookType, compiler, event, callback) { if (compiler.hooks) { hookType = hookType || "tap"; compiler.hooks[camel(event)][hookType]("webpack-dashboard", callback); } else { compiler.plugin(event, callback); } } const webpackHook = _webpackHook.bind(null, "tap"); const webpackAsyncHook = _webpackHook.bind(null, "tapAsync"); class DashboardPlugin { constructor(options) { if (typeof options === "function") { this._handler = options; } else { options = options || {}; this.host = options.host || DEFAULT_HOST; this.port = options.port || DEFAULT_PORT; this.includeAssets = options.includeAssets || []; this._handler = options.handler || null; } this.watching = false; this.openMessages = 0; } handler(...args) { if (this._handler) { this._handler(...args); } } cleanup(numTried = 0) { if (!this._cleanedUp && !this.watching && this.socket) { // Clear handler so we don't emit any more messages. this._handler = null; // Check if we have unhandled dashboard messages. if (this.openMessages > 0 && numTried < CLEANUP_MAX_NUM_TRIES) { // Wait a small interval and try again, up to a maximum. setTimeout(() => this.cleanup(numTried++), CLEANUP_RETRY_DELAY_MS); return; } // Close! this._cleanedUp = true; this.socket.close(); } } apply(compiler) { // Reached compile "done" state. let reachedDone = false; // Compile has finished in "done", "error", "failed" states. let finished = false; let timer; if (!this._handler) { this._handler = noop; const port = this.port; const host = this.host; this.socket = io(`http://${host}:${port}`); this.socket.on("connect", () => { // Manually track messages we send to the dashboard and decrement later. const socketMsg = this.socket.emit.bind(this.socket, "message"); const ack = () => { this.openMessages--; }; this._handler = (...args) => { this.openMessages++; socketMsg(...args, ack); }; }); this.socket.once("options", args => { this.minimal = args.minimal; this.includeAssets = this.includeAssets.concat(args.includeAssets || []); }); this.socket.on("error", err => { // eslint-disable-next-line no-console console.log(err); }); this.socket.on("disconnect", () => { if (!reachedDone) { // eslint-disable-next-line no-console console.log("Socket.io disconnected before completing build lifecycle."); } }); } new webpack.ProgressPlugin((percent, msg) => { // Skip reporting once finished. if (finished) { return; } this.handler([ { type: "status", value: "Compiling" }, { type: "progress", value: percent }, { type: "operations", value: msg + getTimeMessage(timer) } ]); }).apply(compiler); webpackAsyncHook(compiler, "watch-run", (c, done) => { this.watching = true; done(); }); webpackAsyncHook(compiler, "run", (c, done) => { this.watching = false; done(); }); webpackHook(compiler, "compile", () => { timer = Date.now(); finished = false; this.handler([ { type: "status", value: "Compiling" } ]); }); webpackHook(compiler, "invalid", () => { finished = true; this.handler([ { type: "status", value: "Invalidated" }, { type: "progress", value: 0 }, { type: "operations", value: "idle" }, { type: "clear" } ]); }); webpackHook(compiler, "failed", () => { finished = true; this.handler([ { type: "status", value: "Failed" }, { type: "operations", value: `idle${getTimeMessage(timer)}` } ]); }); webpackAsyncHook(compiler, "done", (stats, done) => { const { errors, options } = stats.compilation; const statsOptions = (options.devServer && options.devServer.stats) || options.stats || { colors: true }; const status = errors.length ? "Error" : "Success"; // We only need errors/warnings for stats information for finishing up. // This allows us to avoid sending a full stats object to the CLI which // can cause socket.io client disconnects for large objects. // See: https://github.com/FormidableLabs/webpack-dashboard/issues/279 const statsJsonOptions = { all: false, errors: true, warnings: true }; reachedDone = true; finished = true; this.handler([ { type: "status", value: status }, { type: "progress", value: 1 }, { type: "operations", value: `idle${getTimeMessage(timer)}` }, { type: "stats", value: { errors: stats.hasErrors(), warnings: stats.hasWarnings(), data: stats.toJson(statsJsonOptions) } }, { type: "log", value: stats.toString(statsOptions) } ]); // Skip metrics in minimal mode. const getMetrics = () => (this.minimal ? Promise.resolve() : this.getMetrics(stats)); // eslint-disable-next-line promise/catch-or-return getMetrics() .then(datas => this.handler(datas)) .catch(err => { console.log("Error from inspectpack:", err); // eslint-disable-line no-console }) // eslint-disable-next-line promise/always-return .then(() => { this.cleanup(); done(); // eslint-disable-line promise/no-callback-in-promise }); }); } getMetrics(statsObj) { // Get the **full** stats object here for `inspectpack` analysis. const stats = statsObj.toJson({ source: true // Needed for webpack5+ }); // Truncate off non-included assets. const { includeAssets } = this; if (includeAssets.length) { stats.assets = stats.assets.filter(({ name }) => includeAssets.some(pattern => { if (typeof pattern === "string") { return name.startsWith(pattern); } else if (pattern instanceof RegExp) { return pattern.test(name); } // Pass through bad options.. return false; }) ); } // Late destructure so that we can stub. const { actions } = inspectpack; const { serializeError } = serializer; const getSizes = () => actions("sizes", { stats }) .then(instance => instance.getData()) .then(data => ({ type: "sizes", value: data })) .catch(err => ({ type: "sizes", error: true, value: serializeError(err) })); const getProblems = () => Promise.all( INSPECTPACK_PROBLEM_ACTIONS.map(action => actions(action, { stats }).then(instance => instance.getData()) ) ) .then(datas => ({ type: INSPECTPACK_PROBLEM_TYPE, value: INSPECTPACK_PROBLEM_ACTIONS.reduce( (memo, action, i) => Object.assign({}, memo, { [action]: datas[i] }), {} ) })) .catch(err => ({ type: INSPECTPACK_PROBLEM_TYPE, error: true, value: serializeError(err) })); return Promise.all([getSizes(), getProblems()]); } } module.exports = DashboardPlugin; ================================================ FILE: test/.eslintrc.json ================================================ { "extends": ["formidable/configurations/es6-node-test", "plugin:prettier/recommended"] } ================================================ FILE: test/base.spec.js ================================================ "use strict"; /** * Base server unit test initialization / global before/after's. * * This file should be `require`'ed by all other test files. * * **Note**: Because there is a global sandbox server unit tests should always * be run in a separate process from other types of tests. */ const sinon = require("sinon"); const blessed = require("neo-blessed"); const base = (module.exports = { sandbox: null }); beforeEach(() => { base.sandbox = sinon.createSandbox({ useFakeTimers: true }); // Stub out **all** of blessed so we don't end up in a terminal. // Blessed is a `typeof` function, so manually iterate key.s Object.keys(blessed) .filter(key => typeof blessed[key] === "function") .forEach(key => { base.sandbox.stub(blessed, key); }); // Some manual hacking. blessed.screen.returns({ append: sinon.spy(), key: sinon.spy(), render: sinon.spy() }); blessed.listbar.returns({ selected: "selected", setLabel: sinon.spy(), setProblems: sinon.spy(), selectTab: sinon.spy(), setItems: sinon.spy() }); blessed.box.returns({ setContent: sinon.spy(), setLabel: sinon.spy() }); blessed.log.returns({ log: sinon.spy() }); blessed.table.returns({ setData: sinon.spy() }); blessed.ProgressBar.returns({ setContent: sinon.spy(), setProgress: sinon.spy() }); }); afterEach(() => { base.sandbox.restore(); }); ================================================ FILE: test/bin/webpack-dashboard.spec.js ================================================ "use strict"; const base = require("../base.spec"); const cli = require("../../bin/webpack-dashboard"); describe("bin/webpack-dashboard", () => { it("can invoke the dashboard cli", () => { expect(() => cli({ argv: [], server: { on: base.sandbox.spy() } }) ).to.not.throw(); }); }); ================================================ FILE: test/dashboard/index.spec.js ================================================ "use strict"; const chalk = require("chalk"); const blessed = require("neo-blessed"); const base = require("../base.spec"); const Dashboard = require("../../dashboard"); const mockSetItems = () => { // Override ListBar fakes from what we do in `base.spec.js`. // Note that these are **already** stubbed. We're not monkey-patching blessed. blessed.listbar.returns({ selected: "selected", setLabel: base.sandbox.spy(), selectTab: base.sandbox.spy(), setItems: base.sandbox.stub().callsFake(obj => { // Naively simulate what setItems would do calling each object key. Object.keys(obj).forEach(key => obj[key]()); }) }); }; describe("dashboard", () => { const options = { color: "red", minimal: true, title: "my-title" }; it("can create a new no option dashboard", () => { const dashboard = new Dashboard(); expect(dashboard).to.be.ok; expect(dashboard.color).to.equal("green"); expect(dashboard.minimal).to.be.false; expect(dashboard.stats).to.be.null; }); it("can create a new with options dashboard", () => { const dashboardWithOptions = new Dashboard(options); expect(dashboardWithOptions).to.be.ok; expect(dashboardWithOptions.color).to.equal("red"); expect(dashboardWithOptions.minimal).to.be.true; }); describe("set* methods", () => { let dashboard; beforeEach(() => { dashboard = new Dashboard(); }); describe("setData", () => { const dataArray = [ { type: "progress", value: 0.57 }, { type: "operations", value: "IDLE" } ]; it("can setData", () => { expect(() => dashboard.setData(dataArray)).to.not.throw; }); }); describe("setOperations", () => { const data = { value: "IDLE" }; it("can setOperations", () => { expect(() => dashboard.setOperations(data)).to.not.throw; dashboard.setOperations(data); expect(dashboard.operations.setContent).to.have.been.calledWith(data.value); }); }); describe("setStatus", () => { const data = { value: "Success" }; it("can setStatus", () => { expect(() => dashboard.setStatus(data)).to.not.throw; dashboard.setStatus(data); expect(dashboard.status.setContent).to.have.been.calledWith( `{green-fg}{bold}${data.value}{/}` ); }); it("should display a failed status on build failure", () => { data.value = "Failed"; expect(() => dashboard.setStatus(data)).to.not.throw; dashboard.setStatus(data); expect(dashboard.status.setContent).to.have.been.calledWith( `{red-fg}{bold}${data.value}{/}` ); }); it("should display any other status string without coloring", () => { data.value = "Unknown"; expect(() => dashboard.setStatus(data)).to.not.throw; dashboard.setStatus(data); expect(dashboard.status.setContent).to.have.been.calledWith(`{bold}${data.value}{/}`); }); }); describe("setProgress", () => { const data = { value: 0.57 }; it("can setProgress", () => { expect(() => dashboard.setProgress(data)).to.not.throw; dashboard.setProgress(data); expect(dashboard.progressbar.setProgress).to.have.been.calledOnce; expect(dashboard.progressbar.setContent).to.have.been.called; }); it(`should call progressbar.setProgress twice if not in minimal mode and percent is falsy`, () => { data.value = null; expect(() => dashboard.setProgress(data)).to.not.throw; dashboard.setProgress(data); expect(dashboard.progressbar.setProgress).to.have.been.calledTwice; }); }); describe("setStats", () => { const data = { value: { errors: null, data: { errors: [], warnings: [] } } }; it("can setStats", () => { expect(() => dashboard.setStats(data)).not.to.throw; dashboard.setStats(data); expect(dashboard.logText.log).to.have.been.called; expect(dashboard.modulesMenu.setLabel).to.have.been.calledWith( chalk.yellow("Modules (loading...)") ); expect(dashboard.assets.setLabel).to.have.been.calledWith( chalk.yellow("Assets (loading...)") ); expect(dashboard.problemsMenu.setLabel).to.have.been.calledWith( chalk.yellow("Problems (loading...)") ); }); it("should display stats errors if present", () => { data.value.errors = ["error"]; expect(() => dashboard.setStats(data)).not.to.throw; dashboard.setStats(data); expect(dashboard.status.setContent).to.have.been.calledWith("{red-fg}{bold}Failed{/}"); }); }); describe("setSizes", () => { const data = { value: { assets: { foo: { meta: { full: 456 }, files: [ { size: { full: 123 }, fileName: "test.js", baseName: "/home/bar/test.js" } ] }, bar: { meta: { full: 123 }, files: [] } } } }; it("can setSizes", () => { const formattedData = [ ["Name", "Size"], ["foo", "456 B"], ["bar", "123 B"], ["Total", "579 B"] ]; expect(() => dashboard.setSizes(data)).to.not.throw; dashboard.setSizes(data); expect(dashboard.assets.setLabel).to.have.been.calledWith("Assets"); expect(dashboard.assetTable.setData).to.have.been.calledWith(formattedData); expect(dashboard.modulesMenu.setLabel).to.have.been.calledWith("Modules"); expect(dashboard.modulesMenu.setItems).to.have.been.called; expect(dashboard.modulesMenu.selectTab).to.have.been.calledWith( dashboard.modulesMenu.selected ); expect(dashboard.screen.render).to.have.been.called; }); it("should call formatModules", () => { // Mock out the call to setItems to force call of formatModules. mockSetItems(); // Discard generic dashboard, create a new one with adjusted mocks. dashboard = new Dashboard(); expect(() => dashboard.setSizes(data)).to.not.throw; }); }); describe("setSizesError", () => { const err = "error"; it("can setSizesError", () => { expect(() => dashboard.setSizesError(err)).to.not.throw; dashboard.setSizesError(err); expect(dashboard.modulesMenu.setLabel).to.have.been.calledWith( chalk.red("Modules (error)") ); expect(dashboard.assets.setLabel).to.have.been.calledWith(chalk.red("Assets (error)")); expect(dashboard.logText.log).to.have.been.calledWith( chalk.red("Could not load module/asset sizes.") ); expect(dashboard.logText.log).to.have.been.calledWith(chalk.red(err)); }); }); describe("setProblems", () => { const data = { value: { duplicates: { assets: { foo: "foo", bar: "bar" } }, versions: { assets: { foo: "1.2.3", bar: "3.2.1" } } } }; it("can setProblems", () => { expect(() => dashboard.setProblems(data)).to.not.throw; dashboard.setProblems(data); expect(dashboard.problemsMenu.setLabel).to.have.been.calledWith("Problems"); expect(dashboard.problemsMenu.setItems).to.have.been.called; expect(dashboard.problemsMenu.selectTab).to.have.been.calledWith( dashboard.problemsMenu.selected ); expect(dashboard.screen.render).to.have.been.called; }); it("should call formatProblems", () => { // Mock out the call to setItems to force call of formatProblems. mockSetItems(); // Discard generic dashboard, create a new one with adjusted mocks. dashboard = new Dashboard(); expect(() => dashboard.setProblems(data)).to.not.throw; }); }); describe("setProblemsError", () => { const err = { stack: "stack" }; it("can setProblemsError", () => { expect(() => dashboard.setProblemsError(err)).to.not.throw; dashboard.setProblemsError(err); expect(dashboard.problemsMenu.setLabel).to.have.been.calledWith( chalk.red("Problems (error)") ); expect(dashboard.logText.log).to.have.been.calledWith( chalk.red("Could not analyze bundle problems.") ); expect(dashboard.logText.log).to.have.been.calledWith(chalk.red(err.stack)); }); }); describe("setLog", () => { const data = { value: "[{ log: 'log' }]" }; it("can setLog", () => { expect(() => dashboard.setLog(data)).not.to.throw; dashboard.setLog(data); expect(dashboard.logText.log).to.have.been.calledWith("[ log: 'log' ]"); }); it("should return early if the stats object has errors", () => { dashboard.stats = {}; dashboard.stats.hasErrors = () => true; expect(dashboard.setLog(data)).to.be.undefined; dashboard.setLog(data); expect(dashboard.logText.log).to.not.have.been.called; }); }); }); }); ================================================ FILE: test/plugin/index.spec.js ================================================ "use strict"; const inspectpackActions = require("inspectpack/lib/actions"); const base = require("../base.spec"); const Plugin = require("../../plugin"); const errorSerializer = require("../../utils/error-serialization"); describe("plugin", () => { const options = { port: 3000, host: "111.0.2.3" }; it("can create a new no option plugin", () => { const plugin = new Plugin(); expect(plugin).to.be.ok; expect(plugin.host).to.equal("127.0.0.1"); // eslint-disable-next-line no-magic-numbers expect(plugin.port).to.equal(9838); expect(plugin._handler).to.be.null; expect(plugin.watching).to.be.false; }); it("can create a new with options dashboard", () => { const pluginWithOptions = new Plugin(options); expect(pluginWithOptions.host).to.equal("111.0.2.3"); // eslint-disable-next-line no-magic-numbers expect(pluginWithOptions.port).to.equal(3000); }); describe("plugin methods", () => { let stats; let toJson; let compilation; let compiler; let plugin; beforeEach(() => { stats = { modules: [], assets: [] }; toJson = base.sandbox.stub().callsFake(() => stats); compilation = { errors: [], warnings: [], getStats: () => ({ toJson }), tap: base.sandbox.stub(), tapAsync: base.sandbox.stub() // this is us in webpack-dashboard }; compiler = { // mock out webpack4 compiler, since that's what we have in devDeps hooks: { compilation, emit: { intercept: base.sandbox.stub() }, watchRun: { tapAsync: base.sandbox.stub() }, run: { tapAsync: base.sandbox.stub() }, compile: { tap: base.sandbox.stub() }, failed: { tap: base.sandbox.stub() }, invalid: { tap: base.sandbox.stub() }, done: { tap: base.sandbox.stub() } } }; plugin = new Plugin(); }); it("can do a basic compilation", () => { expect(() => plugin.apply(compiler)).to.not.throw; // after instantiation, test that we can hit getMetrics expect(() => plugin.getMetrics({ toJson })).to.not.throw; }); it("can do a basic getMetrics", () => { const actions = base.sandbox.spy(inspectpackActions, "actions"); return ( plugin .getMetrics({ toJson }) // eslint-disable-next-line promise/always-return .then(() => { expect(actions).to.have.been.calledThrice; }) ); }); it("filters assets for includeAssets", () => { const actions = base.sandbox.spy(inspectpackActions, "actions"); stats = { assets: [ { name: "one.js", modules: [] }, { name: "two.js", modules: [] }, { name: "three.js", modules: [] } ] }; plugin = new Plugin({ includeAssets: [ "one", // string prefix /tw/ // regex match ] }); return ( plugin .getMetrics({ toJson }) // eslint-disable-next-line promise/always-return .then(() => { expect(actions).to.have.been.calledWith("sizes", { stats: { assets: [ { modules: [], name: "one.js" }, { modules: [], name: "two.js" } ] } }); }) ); }); it("should serialize errors when encountered", () => { const actions = base.sandbox.stub(inspectpackActions, "actions").rejects(); const serializeError = base.sandbox.spy(errorSerializer, "serializeError"); return ( plugin .getMetrics({ toJson }) // eslint-disable-next-line promise/always-return .then(() => { // All three actions called. expect(actions).to.have.been.calledThrice; // ... but since two are in Promise.all only get one rejection. expect(serializeError).to.have.been.calledTwice; }) ); }); }); }); ================================================ FILE: test/setup.js ================================================ "use strict"; const chai = require("chai"); const sinonChai = require("sinon-chai"); // Add chai plugins. chai.use(sinonChai); // Add test lib globals. global.expect = chai.expect; ================================================ FILE: test/utils/format-assets.spec.js ================================================ "use strict"; const { _getAssetSize, _getTotalSize, _printAssets } = require("../../utils/format-assets"); describe("format-assets", () => { describe("#_getAssetSize", () => { context("when asset size is present", () => { it("returns a readable file size as string", () => { const asset = { size: 500 }; expect(_getAssetSize(asset)).to.equal("500 B"); }); }); context("when no asset size is present", () => { it("returns zero in a readable file size as string", () => { const asset = { size: undefined }; expect(_getAssetSize(asset)).to.equal("0 B"); }); }); }); describe("#_getTotalSize", () => { it("returns a readable file size of all assets as a string", () => { const assets = [{ size: 500 }, { size: undefined }, { size: 1000 }]; expect(_getTotalSize(assets)).to.equal("1.46 KB"); }); }); describe("#_printAssets", () => { it("returns a nested array of assets information", () => { const assetList = [ { name: "assets1", size: 500 }, { name: "assets2", size: 0 }, { name: "assets2", size: 500 } ]; const output = [ ["Name", "Size"], ["assets1", "500 B"], ["assets2", "0 B"], ["assets2", "500 B"], ["Total", "1000 B"] ]; expect(_printAssets(assetList)).eql(output); }); }); }); ================================================ FILE: test/utils/format-modules.spec.js ================================================ "use strict"; const { normalize, sep } = require("path"); const { _formatFileName, _formatPercentage } = require("../../utils/format-modules"); describe("format-modules", () => { describe("#_formatFileName", () => { it("returns a blessed green colored file name", () => { const mod = { fileName: normalize("foo/bar/test.js") }; expect(_formatFileName(mod)).to.equal(`{green-fg}.${sep}foo${sep}bar${sep}test.js{/}`); }); context("when there is a baseName", () => { it("returns a blessed yellow colored file name", () => { const mod = { fileName: "test.js", baseName: normalize("/home/bar/test.js") }; expect(_formatFileName(mod)).to.equal("{yellow-fg}test.js{/}"); }); }); context("when node_modules is present in fileName", () => { it("returns a blessed yellow colored file name", () => { const mod = { fileName: normalize("/node_modules/@foo/test.js"), baseName: normalize("/home/bar/node_modules/@foo/test.js") }; expect(_formatFileName(mod)).to.equal( `~${sep}{yellow-fg}@foo{/}${sep}{yellow-fg}test.js{/}` ); }); }); }); describe("#_formatPercentage", () => { it("returns a precentage as a string", () => { // eslint-disable-next-line no-magic-numbers expect(_formatPercentage(30, 15)).to.equal("200%"); }); }); }); ================================================ FILE: test/utils/format-output.spec.js ================================================ "use strict"; const { _isLikelyASyntaxError, _lineJoin, _formatMessage } = require("../../utils/format-output"); describe("format-output", () => { describe("#_isLikelyASyntaxError", () => { context("when message is a syntax error", () => { it("returns true", () => { const message = "Syntax error: missing ; before statement"; expect(_isLikelyASyntaxError(message)).to.be.true; }); }); context("when message is a type error", () => { it("returns false", () => { const message = "Type error: null has no properties"; expect(_isLikelyASyntaxError(message)).to.be.false; }); }); }); describe("#_formatMessage", () => { it("returns a readable user friendly message", () => { const message1 = "Module build failed: SyntaxError: missing ; before statement"; const message2 = "/Module not found: Error: Cannot resolve 'file' or 'directory'/"; expect(_formatMessage(message1)).to.equal("Syntax error: missing ; before statement"); expect(_formatMessage(message2)).to.equal("/Module not found:/"); }); }); describe("#_lineJoin", () => { it("returns the elements of an array on a newline as a string", () => { const array = ["word", "word2", "word3"]; const output = "word\nword2\nword3"; expect(_lineJoin(array)).to.equal(output); }); }); }); ================================================ FILE: test/utils/format-versions.spec.js ================================================ "use strict"; const formatVersions = require("../../utils/format-versions"); describe("format-versions", () => { describe("when package are present", () => { const data = { packages: { foo: { "1.1.1": [ { skews: { parts: [ { name: "foo-dep", range: "^1.0.0" }, { name: "bar", range: "^3.0.2" } ] } } ] } } }; it("should return a handlebar compile template", () => { const result = // eslint-disable-next-line max-len "{yellow-fg}{underline}Version skews{/}\n\n{yellow-fg}{bold}foo{/}\n {green-fg}1.1.1{/}\n {cyan-fg}foo-dep{/}@^1.0.0 -> {cyan-fg}bar{/}@^3.0.2\n"; expect(formatVersions(data)).to.equal(result); }); }); describe("when packages are not present", () => { it("should return an empty string", () => { const data = { packages: [] }; expect(formatVersions(data)).to.equal(""); }); }); }); ================================================ FILE: utils/error-serialization.js ================================================ "use strict"; const serializeError = err => ({ code: err.code, message: err.message, stack: err.stack }); const deserializeError = serializedError => { const err = new Error(); err.code = serializedError.code; err.message = serializedError.message; err.stack = serializedError.stack; return err; }; module.exports = { serializeError, deserializeError }; ================================================ FILE: utils/format-assets.js ================================================ "use strict"; /** * Assets are the full emitted bundles. */ const filesize = require("filesize"); function _getAssetSize(asset) { return filesize(asset.size || 0); } function _getTotalSize(assetsList) { return filesize(assetsList.reduce((total, asset) => total + (asset.size || 0), 0)); } function _printAssets(assetsList) { return [["Name", "Size"]] .concat(assetsList.map(asset => [asset.name, _getAssetSize(asset)])) .concat([["Total", _getTotalSize(assetsList)]]); } function formatAssets(assets) { // Convert to list. const assetsList = Object.keys(assets).map(name => ({ name, size: assets[name].meta.full })); return _printAssets(assetsList); } module.exports = { formatAssets, _getAssetSize, _getTotalSize, _printAssets }; ================================================ FILE: utils/format-duplicates.js ================================================ "use strict"; /** * Problem: Duplicate files (same path name) in a bundle. */ const filesize = require("filesize"); const Handlebars = require("handlebars"); Handlebars.registerHelper("filesize", function (options) { // eslint-disable-next-line no-invalid-this return filesize(options.fn(this)); }); /* eslint-disable max-len*/ const template = Handlebars.compile( `{yellow-fg}{underline}Duplicate files{/} {{#each files}} - {green-fg}{{@key}}{/} (files: {{meta.extraFiles.num}}, sources: {{meta.extraSources.num}}, bytes: {{#filesize}}{{meta.extraSources.bytes}}{{/filesize}}) {{/each}} Extra duplicate files (unique): {{meta.extraFiles.num}} Extra duplicate sources (non-unique): {{meta.extraSources.num}} Wasted duplicate bytes (non-unique): {{#filesize}}{{meta.extraSources.bytes}}{{/filesize}} ` ); /* eslint-enable max-len*/ function formatDuplicates(duplicates) { const haveDups = !!Object.keys((duplicates || {}).files || {}).length; return haveDups ? template(duplicates) : ""; } module.exports = formatDuplicates; ================================================ FILE: utils/format-modules.js ================================================ "use strict"; /** * Modules are the individual files within an asset. */ const { relative, resolve, sep } = require("path"); const filesize = require("filesize"); const PERCENT_MULTIPLIER = 100; const PERCENT_PRECISION = 3; // Convert to: // - existing source file name // - the path leading up to **just** the package (not including subpath). function _formatFileName(mod) { const { fileName, baseName } = mod; // Source file. if (!baseName) { return `{green-fg}.${sep}${relative(process.cwd(), resolve(fileName))}{/}`; } // Package let parts = fileName.split(sep); // Remove starting path. const firstNmIdx = parts.indexOf("node_modules"); parts = parts.slice(firstNmIdx); // Remove trailing path after package. const lastNmIdx = parts.lastIndexOf("node_modules"); const isScoped = (parts[lastNmIdx + 1] || "").startsWith("@"); parts = parts.slice(0, lastNmIdx + (isScoped ? 3 : 2)); // eslint-disable-line no-magic-numbers return parts.map(part => (part === "node_modules" ? "~" : `{yellow-fg}${part}{/}`)).join(sep); } function _formatPercentage(modSize, assetSize) { const percentage = ((modSize / assetSize) * PERCENT_MULTIPLIER).toPrecision(PERCENT_PRECISION); return `${percentage}%`; } function formatModules(mods) { // We _could_ use the `asset.meta.full` from inspectpack, but that is for // the entire module with boilerplate included. We instead do a percentage // of the files we're counting here. const assetSize = mods.reduce((count, mod) => count + mod.size.full, 0); // First, process the modules into a map to normalize file paths. const modsMap = mods.reduce((memo, mod) => { // File name collapses to packages for dependencies. // Aggregate into object. const fileName = _formatFileName(mod); // Add in information. memo[fileName] = memo[fileName] || { fileName, num: 0, size: 0 }; memo[fileName].num += 1; memo[fileName].size += mod.size.full; return memo; }, {}); return [].concat( [["Name", "Size", "Percent"]], Object.keys(modsMap) .map(fileName => modsMap[fileName]) .sort((a, b) => a.size < b.size) // sort largest first .map(mod => [ `${mod.fileName} ${mod.num > 1 ? `(${mod.num})` : ""}`, filesize(mod.size), _formatPercentage(mod.size, assetSize) ]) ); } module.exports = { formatModules, _formatFileName, _formatPercentage }; ================================================ FILE: utils/format-output.js ================================================ "use strict"; const friendlySyntaxErrorLabel = "Syntax error:"; function _isLikelyASyntaxError(message) { return message.indexOf(friendlySyntaxErrorLabel) !== -1; } function _formatMessage(message = "") { // Handle legacy and modern webpack shapes. message = typeof message.message !== "undefined" ? message.message : message; return message .replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel) .replace(/Module not found: Error: Cannot resolve 'file' or 'directory'/, "Module not found:") .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, "") .replace("./~/css-loader!./~/postcss-loader!", ""); } function _lineJoin(arr) { return arr.join("\n"); } // eslint-disable-next-line max-statements function formatOutput(stats) { const output = []; const hasErrors = stats.hasErrors(); const hasWarnings = stats.hasWarnings(); const json = stats.toJson({ source: true // Needed for webpack5+ }); let formattedErrors = json.errors.map(message => `Error in ${_formatMessage(message)}`); const formattedWarnings = json.warnings.map(message => `Warning in ${_formatMessage(message)}`); if (hasErrors) { output.push("{red-fg}Failed to compile.{/}"); output.push(""); if (formattedErrors.some(_isLikelyASyntaxError)) { formattedErrors = formattedErrors.filter(_isLikelyASyntaxError); } formattedErrors.forEach(message => { output.push(message); output.push(""); }); return _lineJoin(output); } if (hasWarnings) { output.push("{yellow-fg}Compiled with warnings.{/yellow-fg}"); output.push(""); formattedWarnings.forEach(message => { output.push(message); output.push(""); }); return _lineJoin(output); } output.push("{green-fg}Compiled successfully!{/}"); output.push(""); return _lineJoin(output); } module.exports = { formatOutput, _formatMessage, _isLikelyASyntaxError, _lineJoin }; ================================================ FILE: utils/format-problems.js ================================================ "use strict"; const formatDuplicates = require("./format-duplicates"); const formatVersions = require("./format-versions"); function formatProblems(data) { const duplicates = formatDuplicates(data.duplicates); const versions = formatVersions(data.versions); if (!duplicates && !versions) { return "{green-fg}No problems detected!{/}"; } if (duplicates && !versions) { return `{green-fg}No version skews!{/}\n\n${duplicates}`; } if (!duplicates && versions) { return `{green-fg}No duplicate files!{/}\n\n${versions}`; } return `${duplicates}\n${versions}`; } module.exports = { formatProblems }; ================================================ FILE: utils/format-versions.js ================================================ "use strict"; const Handlebars = require("handlebars"); // From inspectpack. const pkgNamePath = pkgParts => pkgParts.reduce((m, part) => `${m}${m ? " -> " : ""}{cyan-fg}${part.name}{/}@${part.range}`, ""); Handlebars.registerHelper("skew", function (options) { // eslint-disable-next-line no-invalid-this return pkgNamePath(options.fn(this)); }); const template = Handlebars.compile( `{yellow-fg}{underline}Version skews{/} {{#each packages}} {yellow-fg}{bold}{{@key}}{/} {{#each this}} {green-fg}{{@key}}{/} {{#each this}} {{#each skews}} {{#skew}}{{{this}}}{{/skew}} {{/each}} {{/each}} {{/each}} {{/each}} ` ); function formatVersions(versions) { const haveSkews = !!Object.keys((versions || {}).packages || {}).length; return haveSkews ? template(versions) : ""; } module.exports = formatVersions;