Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Denis Bardadym
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.

View file

@ -0,0 +1,214 @@
# Rollup Plugin Visualizer
[![NPM Version](https://img.shields.io/npm/v/rollup-plugin-visualizer.svg)](https://npmjs.org/package/rollup-plugin-visualizer) [![Node.js CI](https://github.com/btd/rollup-plugin-visualizer/actions/workflows/node.js.yml/badge.svg)](https://github.com/btd/rollup-plugin-visualizer/actions/workflows/node.js.yml)
Visualize and analyze your Rollup bundle to see which modules are taking up space.
## Screenshots
![pic](https://github.com/btd/rollup-plugin-visualizer/blob/master/pics/collage.png?raw=true)
## Installation
```sh
npm install --save-dev rollup-plugin-visualizer
```
or via yarn:
```sh
yarn add --dev rollup-plugin-visualizer
```
## Usage
Import
```javascript
// es
import { visualizer } from "rollup-plugin-visualizer";
// or
// cjs
const { visualizer } = require("rollup-plugin-visualizer");
```
Usage with rollup (rollup.config.js)
```js
module.exports = {
plugins: [
// put it last
visualizer(),
],
};
```
Usage with rolldown (rolldown.config.ts)
```js
import { defineConfig, type RolldownPlugin } from 'rolldown';
export default defineConfig({
plugins: [
visualizer() as RolldownPlugin
],
})
```
Usage with vite (vite.config.js)
```js
module.exports = {
plugins: [visualizer()],
};
```
Usage with vite TypeScript (vite.config.ts)
```ts
import { defineConfig, type PluginOption } from "vite";
export default defineConfig({
plugins: [visualizer() as PluginOption],
});
```
Usage with SvelteKit (vite.config.js)
```js
const config = {
plugins: [
visualizer({
emitFile: true,
filename: "stats.html",
}),
],
};
export default config;
```
You will find 2/3 files in .svelte-kit/output dir named stats.html (see vite logs for file locations) . You can use this snippet as a starting point and change props/path.
You can also generate 3 json files and combine them to one with cli util.
## How to use generated files
Blue color used to to mark project-own files. This could be: written by you files or generated by build tool files.
Green color used to mark your dependencies.
Internally it just checks if file id prefix is `node_modules`.
All charts refresh layout on window resize.
### Sunburst
This circular hierarchical diagram can help you find huge pieces of code (aka that one huge thing). If you click on some arc it will increase its and all nested arcs size for better inspection.
### Flamegraph
This diagram is top down version of sunburst. It is used a lot by other JS developer tools and would be very familar to developers.
### Treemap
This rectangular hierarchical diagram can help you find huge pieces. Just look on biggest rectangle. But also it can help you find modules included several times, they will have the same topology and relative size. If you click on rectangle it will increase in size for further inspection.
### Network
This diagram should help you answer for the question 'why it is included?'. After force layout stabilize all nodes, you can move it back and forth by dragging with your mouse. Gray circles are treeshaken out files.
In real life scenarios, sometimes you will see terribly connected diagrams. There is no 100% working solution for everyone, it is expected you topology will look 'terrible' and not hold on screen. To make it still visually inspectable, first remove all highly connected nodes that you see (typical examples: commonjsHelpers, tslib, react etc, basically if tooltip for the node is not hold on the screen - exclude this node), after layout stabilization you will see, your layout is not that terrible anymore and most of dependencies cluster together. Move layout to find pieces you looked for.
When you click on node it will highlight nodes that are listed in tooltip (the files that imports current node).
### Raw-data
This template produce JSON output with raw data. Normally it should be used with CLI from this plugin.
### List
Output yml file with all the data, could be good idea to commit this file to track file changes.
## Options
`filename` (string, default `stats.{ext depending template}`) - name of the file with diagram to generate
`title` (string, default `Rollup Visualizer`) - title tag value
`open` (boolean, default `false`) - Open generated file in default user agent
`template` (string, default `treemap`) - Which diagram type to use: `sunburst`, `treemap`, `network`, `raw-data`, `list`, `flamegraph`.
`gzipSize` (boolean, default `false`) - Collect gzip size from source code and display it at chart.
`brotliSize` (boolean, default `false`) - Collect brotli size from source code and display it at chart.
### Advanced options (touch only if you really need it):
`emitFile` (boolean, default `false`) - Use rollup's `emitFile` to generate file. Useful if you want to control all output in one place (via rollup output options). This also could be useful with svelte as it calls vite several times.
`sourcemap` (boolean, default `false`) - Use sourcemaps to calculate sizes (e.g. after UglifyJs or Terser). **Always add plugin as last option.**
`projectRoot` (string | RegExp, default `process.cwd()`) - This is some common root(s) path to your files. This is used to cut absolute files paths out.
`include` (Filter | Filter[], default `undefined`) - Filter for inclusion
`exclude` (Filter | Filter[], default `undefined`) - Filter for exclusion
`Filter` type is `{ bundle?: picomatchPattern, file?: picomatchPattern }`
**Note about `include` and `exclude`** - If options.include is omitted or has zero length, filter will return true by default. Otherwise, an ID must match one or more of the picomatch patterns, and must not match any of the options.exclude patterns. This entries will not be included in stats at all.
#### Include and Exclude
Include and exclude filters uses glob matchers with picomatch. In UI you can do such combinations (both exclude and include):
- Filter bundle and file in one string
- `translation-*.js:*/**/index.js` - this selects all bundles that matches `translation-*.js` and all the files by all paths that name is `index.js`. `:` is separator and required only when bundle search used.
- Format for this kind of filter is `BUNDLE_GLOB:FILE_GLOB`
- Filter bundle in one string
- This is special case of bundle+file filter, you need to omit `FILE_GLOB` part (empty string)
- Filter file in one string
- **This is DEFAULT search option**
- `*/**/index.js` - select all files that name is index.js
## CLI
This plugin provides cli util `rollup-plugin-visualizer`. Add `--help` to check actual options. It can be used like:
```sh
rollup-plugin-visualizer [OPTIONS] stat1.json stat2.json ../stat3.json
```
This can be useful in case you have different config files in the same project and you want to display all of them in the same chart.
## Build plugin
For development if you need to build plugin, just exec:
```js
npm run build
```
## Disclaimer about generated files
Generated html files do not and never will contain your source code (contents of files). They can contain only js/html/css code required to build chart (plugin code) and statistical information about your source code.
This statistical information can contain:
- size of files included in bundle
- size of files included in source map
- file's paths
- files hierarchy (fs tree for your files)
## Upgrades
See CHANGELOG.md.
## Versioning
- Plugin backend (one appears in configs) are strictly follows SemVer
- Plugin frontend (generated file):
- `network`, `treemap`, `sunburst`, `flamegraph` can change does not matter of version (colors, texts, visual structure etc)
- `raw-data` format follows own `version` property
- `list` follows semver

View file

@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

View file

@ -0,0 +1,102 @@
#!/usr/bin/env node
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const open_1 = __importDefault(require("open"));
const yargs_1 = __importDefault(require("yargs"));
const helpers_1 = require("yargs/helpers");
const render_template_1 = require("../plugin/render-template");
const template_types_1 = __importDefault(require("../plugin/template-types"));
const warn_1 = require("../plugin/warn");
const version_1 = require("../plugin/version");
const argv = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
.option("filename", {
describe: "Output file name",
type: "string",
default: "./stats.html",
})
.option("title", {
describe: "Output file title",
type: "string",
default: "Rollup Visualizer",
})
.option("template", {
describe: "Template type",
type: "string",
choices: template_types_1.default,
default: "treemap",
})
.option("sourcemap", {
describe: "Provided files is sourcemaps",
type: "boolean",
default: false,
})
.option("open", {
describe: "Open generated tempate in default user agent",
type: "boolean",
default: false,
})
.help()
.parseSync();
const listOfFiles = argv._;
const runForPluginJson = async ({ title, template, filename, open }, files) => {
if (files.length === 0) {
throw new Error("Empty file list");
}
const fileContents = await Promise.all(files.map(async (file) => {
const textContent = await fs_1.promises.readFile(file, { encoding: "utf-8" });
const data = JSON.parse(textContent);
return { file, data };
}));
const tree = {
name: "root",
children: [],
};
const nodeParts = {};
const nodeMetas = {};
for (const { file, data } of fileContents) {
if (data.version !== version_1.version) {
(0, warn_1.warn)(`Version in ${file} is not supported (${data.version}). Current version ${version_1.version}. Skipping...`);
continue;
}
if (data.tree.name === "root") {
tree.children = tree.children.concat(data.tree.children);
}
else {
tree.children.push(data.tree);
}
Object.assign(nodeParts, data.nodeParts);
Object.assign(nodeMetas, data.nodeMetas);
}
const data = {
version: version_1.version,
tree,
nodeParts,
nodeMetas,
env: fileContents[0].data.env,
options: fileContents[0].data.options,
};
const fileContent = await (0, render_template_1.renderTemplate)(template, {
title,
data: JSON.stringify(data),
});
await fs_1.promises.mkdir(path_1.default.dirname(filename), { recursive: true });
try {
await fs_1.promises.unlink(filename);
}
catch (err) {
// ignore
}
await fs_1.promises.writeFile(filename, fileContent);
if (open) {
await (0, open_1.default)(filename);
}
};
runForPluginJson(argv, listOfFiles).catch((err) => {
(0, warn_1.warn)(err.message);
process.exit(1);
});

View file

@ -0,0 +1,133 @@
:root {
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--background-color: #2b2d42;
--text-color: #edf2f4;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--font-family);
}
body {
padding: 0;
margin: 0;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
svg {
vertical-align: middle;
width: 100%;
height: 100%;
max-height: 100vh;
}
main {
flex-grow: 1;
height: 100vh;
padding: 20px;
}
.tooltip {
position: absolute;
z-index: 1070;
border: 2px solid;
border-radius: 5px;
padding: 5px;
font-size: 0.875rem;
background-color: var(--background-color);
color: var(--text-color);
}
.tooltip-hidden {
visibility: hidden;
opacity: 0;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
font-size: 0.7rem;
align-items: center;
margin: 0 50px;
height: 20px;
}
.size-selectors {
display: flex;
flex-direction: row;
align-items: center;
}
.size-selector {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.size-selector input {
margin: 0 0.3rem 0 0;
}
.filters {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.module-filters {
display: flex;
flex-grow: 1;
}
.module-filter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 1;
}
.module-filter input {
flex: 1;
height: 1rem;
padding: 0.01rem;
font-size: 0.7rem;
margin-left: 0.3rem;
}
.module-filter + .module-filter {
margin-left: 0.5rem;
}
.node {
cursor: pointer;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,133 @@
:root {
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--background-color: #2b2d42;
--text-color: #edf2f4;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--font-family);
}
body {
padding: 0;
margin: 0;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
svg {
vertical-align: middle;
width: 100%;
height: 100%;
max-height: 100vh;
}
main {
flex-grow: 1;
height: 100vh;
padding: 20px;
}
.tooltip {
position: absolute;
z-index: 1070;
border: 2px solid;
border-radius: 5px;
padding: 5px;
font-size: 0.875rem;
background-color: var(--background-color);
color: var(--text-color);
}
.tooltip-hidden {
visibility: hidden;
opacity: 0;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
font-size: 0.7rem;
align-items: center;
margin: 0 50px;
height: 20px;
}
.size-selectors {
display: flex;
flex-direction: row;
align-items: center;
}
.size-selector {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.size-selector input {
margin: 0 0.3rem 0 0;
}
.filters {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.module-filters {
display: flex;
flex-grow: 1;
}
.module-filter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 1;
}
.module-filter input {
flex: 1;
height: 1rem;
padding: 0.01rem;
font-size: 0.7rem;
margin-left: 0.3rem;
}
.module-filter + .module-filter {
margin-left: 0.5rem;
}
.node {
cursor: pointer;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,165 @@
:root {
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--background-color: #2b2d42;
--text-color: #edf2f4;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--font-family);
}
body {
padding: 0;
margin: 0;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
svg {
vertical-align: middle;
width: 100%;
height: 100%;
max-height: 100vh;
}
main {
flex-grow: 1;
height: 100vh;
padding: 20px;
}
.tooltip {
position: absolute;
z-index: 1070;
border: 2px solid;
border-radius: 5px;
padding: 5px;
font-size: 0.875rem;
background-color: var(--background-color);
color: var(--text-color);
}
.tooltip-hidden {
visibility: hidden;
opacity: 0;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
font-size: 0.7rem;
align-items: center;
margin: 0 50px;
height: 20px;
}
.size-selectors {
display: flex;
flex-direction: row;
align-items: center;
}
.size-selector {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.size-selector input {
margin: 0 0.3rem 0 0;
}
.filters {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.module-filters {
display: flex;
flex-grow: 1;
}
.module-filter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 1;
}
.module-filter input {
flex: 1;
height: 1rem;
padding: 0.01rem;
font-size: 0.7rem;
margin-left: 0.3rem;
}
.module-filter + .module-filter {
margin-left: 0.5rem;
}
.node {
cursor: pointer;
}
.details {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
top: calc(50% - 85px);
left: calc(50% - 85px);
width: 170px;
height: 170px;
font-size: 14px;
text-align: center;
color: var(--font-color);
z-index: 100;
overflow: hidden;
text-overflow: ellipsis;
}
.details-size {
font-size: 0.8em;
}
.details-name {
font-weight: bold;
}
.details-percentage {
margin: 0.4em 0em;
font-size: 2.4em;
line-height: 1em;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,133 @@
:root {
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
--background-color: #2b2d42;
--text-color: #edf2f4;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--font-family);
}
body {
padding: 0;
margin: 0;
}
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
display: flex;
flex-direction: column;
}
svg {
vertical-align: middle;
width: 100%;
height: 100%;
max-height: 100vh;
}
main {
flex-grow: 1;
height: 100vh;
padding: 20px;
}
.tooltip {
position: absolute;
z-index: 1070;
border: 2px solid;
border-radius: 5px;
padding: 5px;
font-size: 0.875rem;
background-color: var(--background-color);
color: var(--text-color);
}
.tooltip-hidden {
visibility: hidden;
opacity: 0;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
flex-direction: row;
font-size: 0.7rem;
align-items: center;
margin: 0 50px;
height: 20px;
}
.size-selectors {
display: flex;
flex-direction: row;
align-items: center;
}
.size-selector {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 1rem;
}
.size-selector input {
margin: 0 0.3rem 0 0;
}
.filters {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.module-filters {
display: flex;
flex-grow: 1;
}
.module-filter {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 1;
}
.module-filter input {
flex: 1;
height: 1rem;
padding: 0.01rem;
font-size: 0.7rem;
margin-left: 0.3rem;
}
.module-filter + .module-filter {
margin-left: 0.5rem;
}
.node {
cursor: pointer;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,4 @@
import * as zlib from "zlib";
export type SizeGetter = (code: string) => Promise<number>;
export declare const createGzipSizeGetter: (options: zlib.ZlibOptions) => SizeGetter;
export declare const createBrotliSizeGetter: (options: zlib.BrotliOptions) => SizeGetter;

View file

@ -0,0 +1,70 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBrotliSizeGetter = exports.createGzipSizeGetter = void 0;
const zlib = __importStar(require("zlib"));
const util_1 = require("util");
const gzip = (0, util_1.promisify)(zlib.gzip);
const brotliCompress = (0, util_1.promisify)(zlib.brotliCompress);
const gzipOptions = (options) => ({
level: 9,
...options,
});
const brotliOptions = (options, buffer) => ({
params: {
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: buffer.length,
},
...options,
});
const createGzipCompressor = (options) => (buffer) => gzip(buffer, gzipOptions(options || {}));
const createGzipSizeGetter = (options) => {
const compress = createGzipCompressor(options);
return async (code) => {
const data = await compress(Buffer.from(code, "utf-8"));
return data.length;
};
};
exports.createGzipSizeGetter = createGzipSizeGetter;
const createBrotliCompressor = (options) => (buffer) => brotliCompress(buffer, brotliOptions(options || {}, buffer));
const createBrotliSizeGetter = (options) => {
const compress = createBrotliCompressor(options);
return async (code) => {
const data = await compress(Buffer.from(code, "utf-8"));
return data.length;
};
};
exports.createBrotliSizeGetter = createBrotliSizeGetter;

View file

@ -0,0 +1,8 @@
import { GetModuleInfo } from "rollup";
import { ModuleLengths, ModuleTree, ModuleTreeLeaf } from "../shared/types";
import { ModuleMapper } from "./module-mapper";
export declare const buildTree: (bundleId: string, modules: Array<ModuleLengths & {
id: string;
}>, mapper: ModuleMapper) => ModuleTree;
export declare const mergeTrees: (trees: Array<ModuleTree | ModuleTreeLeaf>) => ModuleTree;
export declare const addLinks: (startModuleId: string, getModuleInfo: GetModuleInfo, mapper: ModuleMapper) => void;

View file

@ -0,0 +1,121 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addLinks = exports.mergeTrees = exports.buildTree = void 0;
const types_1 = require("../shared/types");
const addToPath = (moduleId, tree, modulePath, node) => {
if (modulePath.length === 0) {
throw new Error(`Error adding node to path ${moduleId}`);
}
const [head, ...rest] = modulePath;
if (rest.length === 0) {
tree.children.push({ ...node, name: head });
return;
}
else {
let newTree = tree.children.find((folder) => folder.name === head && (0, types_1.isModuleTree)(folder));
if (!newTree) {
newTree = { name: head, children: [] };
tree.children.push(newTree);
}
addToPath(moduleId, newTree, rest, node);
return;
}
};
// TODO try to make it without recursion, but still typesafe
const mergeSingleChildTrees = (tree) => {
if (tree.children.length === 1) {
const child = tree.children[0];
const name = `${tree.name}/${child.name}`;
if ((0, types_1.isModuleTree)(child)) {
tree.name = name;
tree.children = child.children;
return mergeSingleChildTrees(tree);
}
else {
return {
name,
uid: child.uid,
};
}
}
else {
tree.children = tree.children.map((node) => {
if ((0, types_1.isModuleTree)(node)) {
return mergeSingleChildTrees(node);
}
else {
return node;
}
});
return tree;
}
};
const buildTree = (bundleId, modules, mapper) => {
const tree = {
name: bundleId,
children: [],
};
for (const { id, renderedLength, gzipLength, brotliLength } of modules) {
const bundleModuleUid = mapper.setNodePart(bundleId, id, {
renderedLength,
gzipLength,
brotliLength,
});
const trimmedModuleId = mapper.trimProjectRootId(id);
const pathParts = trimmedModuleId.split(/\\|\//).filter((p) => p !== "");
addToPath(trimmedModuleId, tree, pathParts, { uid: bundleModuleUid });
}
tree.children = tree.children.map((node) => {
if ((0, types_1.isModuleTree)(node)) {
return mergeSingleChildTrees(node);
}
else {
return node;
}
});
return tree;
};
exports.buildTree = buildTree;
const mergeTrees = (trees) => {
const newTree = {
name: "root",
children: trees,
isRoot: true,
};
return newTree;
};
exports.mergeTrees = mergeTrees;
const addLinks = (startModuleId, getModuleInfo, mapper) => {
const processedNodes = {};
const moduleIds = [startModuleId];
while (moduleIds.length > 0) {
const moduleId = moduleIds.shift();
if (processedNodes[moduleId]) {
continue;
}
else {
processedNodes[moduleId] = true;
}
const moduleInfo = getModuleInfo(moduleId);
if (!moduleInfo) {
return;
}
if (moduleInfo.isEntry) {
mapper.setNodeMeta(moduleId, { isEntry: true });
}
if (moduleInfo.isExternal) {
mapper.setNodeMeta(moduleId, { isExternal: true });
}
for (const importedId of moduleInfo.importedIds) {
mapper.addImportedByLink(importedId, moduleId);
mapper.addImportedLink(moduleId, importedId);
moduleIds.push(importedId);
}
for (const importedId of moduleInfo.dynamicallyImportedIds || []) {
mapper.addImportedByLink(importedId, moduleId);
mapper.addImportedLink(moduleId, importedId, true);
moduleIds.push(importedId);
}
}
};
exports.addLinks = addLinks;

View file

@ -0,0 +1,84 @@
import { Plugin, OutputOptions } from "rollup";
import { Options as OpenOptions } from "open";
import { TemplateType } from "./template-types";
import { Filter } from "../shared/create-filter";
export interface PluginVisualizerOptions {
/**
* The path to the template file to use. Or just a name of a file.
*
* @default "stats.html"
*/
filename?: string;
/**
* If plugin should emit json file with visualizer data. It can be used with plugin CLI
*
* @default false
* @deprecated use template 'raw-data'
*/
json?: boolean;
/**
* HTML <title> value in generated file. Ignored when `json` is true.
*
* @default "Rollup Visualizer"
*/
title?: string;
/**
* If plugin should open browser with generated file. Ignored when `json` or `emitFile` is true.
*
* @default false
*/
open?: boolean;
openOptions?: OpenOptions;
/**
* Which diagram to generate. 'sunburst' or 'treemap' can help find big dependencies or if they are repeated.
* 'network' can answer you why something was included.
* 'flamegraph' will be familar to tools that you know already.
*
* @default 'treemap'
*/
template?: TemplateType;
/**
* If plugin should also calculate sizes of gzipped files.
*
* @default false
*/
gzipSize?: boolean;
/**
* If plugin should also calculate sizes of brotlied files.
*
* @default false
*/
brotliSize?: boolean;
/**
* If plugin should use sourcemap to calculate sizes of modules. By idea it will present more accurate results.
* `gzipSize` and `brotliSize` does not make much sense with this option.
*
* @default false
*/
sourcemap?: boolean;
/**
* Absolute path where project is located. It is used to cut prefix from file's paths.
*
* @default process.cwd()
*/
projectRoot?: string | RegExp;
/**
* Use rollup .emitFile API to generate files. Could be usefull if you want to output to configured by rollup output dir.
* When this set to true, filename options must be filename and not a path.
*
* @default false
*/
emitFile?: boolean;
/**
* A valid picomatch pattern, or array of patterns. If options.include is omitted or has zero length, filter will return true by
* default. Otherwise, an ID must match one or more of the picomatch patterns, and must not match any of the options.exclude patterns.
*/
include?: Filter | Filter[];
/**
* A valid picomatch pattern, or array of patterns. If options.include is omitted or has zero length, filter will return true by
* default. Otherwise, an ID must match one or more of the picomatch patterns, and must not match any of the options.exclude patterns.
*/
exclude?: Filter | Filter[];
}
export declare const visualizer: (opts?: PluginVisualizerOptions | ((outputOptions: OutputOptions) => PluginVisualizerOptions)) => Plugin;
export default visualizer;

View file

@ -0,0 +1,164 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.visualizer = void 0;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const open_1 = __importDefault(require("open"));
const version_1 = require("./version");
const compress_1 = require("./compress");
const module_mapper_1 = require("./module-mapper");
const data_1 = require("./data");
const sourcemap_1 = require("./sourcemap");
const render_template_1 = require("./render-template");
const create_filter_1 = require("../shared/create-filter");
const WARN_SOURCEMAP_DISABLED = "rollup output configuration missing sourcemap = true. You should add output.sourcemap = true or disable sourcemap in this plugin";
const WARN_SOURCEMAP_MISSING = (id) => `${id} missing source map`;
const WARN_JSON_DEPRECATED = 'Option `json` deprecated, please use template: "raw-data"';
const ERR_FILENAME_EMIT = "When using emitFile option, filename must not be path but a filename";
const defaultSizeGetter = () => Promise.resolve(0);
const chooseDefaultFileName = (opts) => {
if (opts.filename)
return opts.filename;
if (opts.json || opts.template === "raw-data")
return "stats.json";
if (opts.template === "list")
return "stats.yml";
return "stats.html";
};
const visualizer = (opts = {}) => {
return {
name: "visualizer",
async generateBundle(outputOptions, outputBundle) {
var _a, _b, _c, _d, _e, _f, _g, _h;
opts = typeof opts === "function" ? opts(outputOptions) : opts;
if ("json" in opts) {
this.warn(WARN_JSON_DEPRECATED);
if (opts.json)
opts.template = "raw-data";
}
const filename = (_a = opts.filename) !== null && _a !== void 0 ? _a : chooseDefaultFileName(opts);
const title = (_b = opts.title) !== null && _b !== void 0 ? _b : "Rollup Visualizer";
const open = !!opts.open;
const openOptions = (_c = opts.openOptions) !== null && _c !== void 0 ? _c : {};
const template = (_d = opts.template) !== null && _d !== void 0 ? _d : "treemap";
const projectRoot = (_e = opts.projectRoot) !== null && _e !== void 0 ? _e : process.cwd();
const filter = (0, create_filter_1.createFilter)(opts.include, opts.exclude);
const gzipSize = !!opts.gzipSize && !opts.sourcemap;
const brotliSize = !!opts.brotliSize && !opts.sourcemap;
const gzipSizeGetter = gzipSize
? (0, compress_1.createGzipSizeGetter)(typeof opts.gzipSize === "object" ? opts.gzipSize : {})
: defaultSizeGetter;
const brotliSizeGetter = brotliSize
? (0, compress_1.createBrotliSizeGetter)(typeof opts.brotliSize === "object" ? opts.brotliSize : {})
: defaultSizeGetter;
const getModuleLengths = async ({ id, renderedLength, code, }, useRenderedLength = false) => {
const isCodeEmpty = code == null || code == "";
const result = {
id,
gzipLength: isCodeEmpty ? 0 : await gzipSizeGetter(code),
brotliLength: isCodeEmpty ? 0 : await brotliSizeGetter(code),
renderedLength: useRenderedLength
? renderedLength
: isCodeEmpty
? 0
: Buffer.byteLength(code, "utf-8"),
};
return result;
};
if (opts.sourcemap && !outputOptions.sourcemap) {
this.warn(WARN_SOURCEMAP_DISABLED);
}
const roots = [];
const mapper = new module_mapper_1.ModuleMapper(projectRoot);
// collect trees
for (const [bundleId, bundle] of Object.entries(outputBundle)) {
if (bundle.type !== "chunk")
continue; //only chunks
let tree;
if (opts.sourcemap) {
if (!bundle.map) {
this.warn(WARN_SOURCEMAP_MISSING(bundleId));
}
const modules = await (0, sourcemap_1.getSourcemapModules)(bundleId, bundle, (_g = (_f = outputOptions.dir) !== null && _f !== void 0 ? _f : (outputOptions.file && path_1.default.dirname(outputOptions.file))) !== null && _g !== void 0 ? _g : process.cwd());
const moduleRenderInfo = await Promise.all(Object.values(modules)
.filter(({ id }) => filter(bundleId, id))
.map(({ id, renderedLength, code }) => {
return getModuleLengths({ id, renderedLength, code: code.join("") }, true);
}));
tree = (0, data_1.buildTree)(bundleId, moduleRenderInfo, mapper);
}
else {
const modules = await Promise.all(Object.entries(bundle.modules)
.filter(([id]) => filter(bundleId, id))
.map(([id, { renderedLength, code }]) => getModuleLengths({ id, renderedLength, code }), false));
tree = (0, data_1.buildTree)(bundleId, modules, mapper);
}
if (tree.children.length === 0) {
const bundleSizes = await getModuleLengths({
id: bundleId,
renderedLength: bundle.code.length,
code: bundle.code,
}, false);
const facadeModuleId = (_h = bundle.facadeModuleId) !== null && _h !== void 0 ? _h : `${bundleId}-unknown`;
const bundleUid = mapper.setNodePart(bundleId, facadeModuleId, bundleSizes);
mapper.setNodeMeta(facadeModuleId, { isEntry: true });
const leaf = { name: bundleId, uid: bundleUid };
roots.push(leaf);
}
else {
roots.push(tree);
}
}
// after trees we process links (this is mostly for uids)
for (const [, bundle] of Object.entries(outputBundle)) {
if (bundle.type !== "chunk" || bundle.facadeModuleId == null)
continue; //only chunks
(0, data_1.addLinks)(bundle.facadeModuleId, this.getModuleInfo.bind(this), mapper);
}
const tree = (0, data_1.mergeTrees)(roots);
const data = {
version: version_1.version,
tree,
nodeParts: mapper.getNodeParts(),
nodeMetas: mapper.getNodeMetas(),
env: {
rollup: this.meta.rollupVersion,
},
options: {
gzip: gzipSize,
brotli: brotliSize,
sourcemap: !!opts.sourcemap,
},
};
const stringData = (0, module_mapper_1.replaceHashPlaceholders)(data);
const fileContent = await (0, render_template_1.renderTemplate)(template, {
title,
data: stringData,
});
if (opts.emitFile) {
// Regex checks for filenames starting with `./`, `../`, `.\` or `..\`
// to account for windows-style path separators
if (path_1.default.isAbsolute(filename) || /^\.{1,2}[/\\]/.test(filename)) {
this.error(ERR_FILENAME_EMIT);
}
this.emitFile({
type: "asset",
fileName: filename,
source: fileContent,
});
}
else {
await fs_1.promises.mkdir(path_1.default.dirname(filename), { recursive: true });
await fs_1.promises.writeFile(filename, fileContent);
if (open) {
await (0, open_1.default)(filename, openOptions);
}
}
},
};
};
exports.visualizer = visualizer;
exports.default = exports.visualizer;

View file

@ -0,0 +1,24 @@
import { ModuleMeta, ModuleLengths, ModuleUID, VisualizerData } from "../shared/types";
export declare const getDataHash: (json: string) => string;
export declare const replaceHashPlaceholders: (data: VisualizerData) => string;
export declare class ModuleMapper {
private projectRoot;
private nodeParts;
private nodeMetas;
private counter;
constructor(projectRoot: string | RegExp);
trimProjectRootId(moduleId: string): string;
uniqueId(): ModuleUID;
getModuleUid(moduleId: string): ModuleUID;
getBundleModuleUid(bundleId: string, moduleId: string): ModuleUID;
setNodePart(bundleId: string, moduleId: string, value: ModuleLengths): ModuleUID;
setNodeMeta(moduleId: string, value: {
isEntry?: boolean;
isExternal?: boolean;
}): void;
hasNodePart(bundleId: string, moduleId: string): boolean;
getNodeParts(): ModuleMapper["nodeParts"];
getNodeMetas(): Record<ModuleUID, ModuleMeta>;
addImportedByLink(targetId: string, sourceId: string): void;
addImportedLink(sourceId: string, targetId: string, dynamic?: boolean): void;
}

View file

@ -0,0 +1,164 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleMapper = exports.replaceHashPlaceholders = exports.getDataHash = void 0;
const crypto = __importStar(require("crypto"));
const HASH_PLACEHOLDER = "!{ROLLUP_VISUALIZER_HASH_PLACEHOLDER}";
const HASH_PLACEHOLDER_REGEXP = new RegExp(`"${HASH_PLACEHOLDER}-(\\d+)"`, "g");
const getDataHash = (json) => {
const hash = crypto.createHash("sha1").update(json).digest("hex");
const hashSub = hash.substring(0, 8);
return hashSub;
};
exports.getDataHash = getDataHash;
const replaceHashPlaceholders = (data) => {
const json = JSON.stringify(data);
const hash = (0, exports.getDataHash)(json);
const jsonWithHash = json.replace(HASH_PLACEHOLDER_REGEXP, (_, num) => `"${hash}-${num}"`);
return jsonWithHash;
};
exports.replaceHashPlaceholders = replaceHashPlaceholders;
class ModuleMapper {
constructor(projectRoot) {
this.projectRoot = projectRoot;
this.nodeParts = {};
this.nodeMetas = {};
this.counter = 0;
}
trimProjectRootId(moduleId) {
if (typeof this.projectRoot === "string" && moduleId.startsWith(this.projectRoot)) {
return moduleId.slice(this.projectRoot.length);
}
return moduleId.replace(this.projectRoot, "");
}
uniqueId() {
return `${HASH_PLACEHOLDER}-${this.counter++}`;
}
getModuleUid(moduleId) {
if (!(moduleId in this.nodeMetas)) {
this.nodeMetas[moduleId] = {
uid: this.uniqueId(),
meta: {
id: this.trimProjectRootId(moduleId),
moduleParts: {},
imported: new Set(),
importedBy: new Set(),
},
};
}
return this.nodeMetas[moduleId].uid;
}
getBundleModuleUid(bundleId, moduleId) {
if (!(moduleId in this.nodeMetas)) {
this.nodeMetas[moduleId] = {
uid: this.uniqueId(),
meta: {
id: this.trimProjectRootId(moduleId),
moduleParts: {},
imported: new Set(),
importedBy: new Set(),
},
};
}
if (!(bundleId in this.nodeMetas[moduleId].meta.moduleParts)) {
this.nodeMetas[moduleId].meta.moduleParts[bundleId] = this.uniqueId();
}
return this.nodeMetas[moduleId].meta.moduleParts[bundleId];
}
setNodePart(bundleId, moduleId, value) {
const uid = this.getBundleModuleUid(bundleId, moduleId);
if (uid in this.nodeParts) {
throw new Error(`Override module: bundle id ${bundleId}, module id ${moduleId}, value ${JSON.stringify(value)}, existing value: ${JSON.stringify(this.nodeParts[uid])}`);
}
this.nodeParts[uid] = { ...value, metaUid: this.getModuleUid(moduleId) };
return uid;
}
setNodeMeta(moduleId, value) {
this.getModuleUid(moduleId);
this.nodeMetas[moduleId].meta.isEntry = value.isEntry;
this.nodeMetas[moduleId].meta.isExternal = value.isExternal;
}
hasNodePart(bundleId, moduleId) {
if (!(moduleId in this.nodeMetas)) {
return false;
}
if (!(bundleId in this.nodeMetas[moduleId].meta.moduleParts)) {
return false;
}
if (!(this.nodeMetas[moduleId].meta.moduleParts[bundleId] in this.nodeParts)) {
return false;
}
return true;
}
getNodeParts() {
return this.nodeParts;
}
getNodeMetas() {
const nodeMetas = {};
for (const { uid, meta } of Object.values(this.nodeMetas)) {
nodeMetas[uid] = {
...meta,
imported: [...meta.imported].map((rawImport) => {
const [uid, dynamic] = rawImport.split(",");
const importData = { uid };
if (dynamic === "true") {
importData.dynamic = true;
}
return importData;
}),
importedBy: [...meta.importedBy].map((rawImport) => {
const [uid, dynamic] = rawImport.split(",");
const importData = { uid };
if (dynamic === "true") {
importData.dynamic = true;
}
return importData;
}),
};
}
return nodeMetas;
}
addImportedByLink(targetId, sourceId) {
const sourceUid = this.getModuleUid(sourceId);
this.getModuleUid(targetId);
this.nodeMetas[targetId].meta.importedBy.add(sourceUid);
}
addImportedLink(sourceId, targetId, dynamic = false) {
const targetUid = this.getModuleUid(targetId);
this.getModuleUid(sourceId);
this.nodeMetas[sourceId].meta.imported.add(String([targetUid, dynamic]));
}
}
exports.ModuleMapper = ModuleMapper;

View file

@ -0,0 +1,6 @@
import { TemplateType } from "./template-types";
export type RenderTemplateOptions = {
data: string;
title: string;
};
export declare const renderTemplate: (templateType: TemplateType, options: RenderTemplateOptions) => Promise<string>;

View file

@ -0,0 +1,109 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderTemplate = void 0;
/* eslint-disable @typescript-eslint/require-await */
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const htmlEscape = (str) => str
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const buildHtmlTemplate = (title, script, nodesData, style) => `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>${htmlEscape(title)}</title>
<style>
${style}
</style>
</head>
<body>
<main></main>
<script>
/*<!--*/
${script}
/*-->*/
</script>
<script>
/*<!--*/
const data = ${nodesData};
const run = () => {
const width = window.innerWidth;
const height = window.innerHeight;
const chartNode = document.querySelector("main");
drawChart.default(chartNode, data, width, height);
};
window.addEventListener('resize', run);
document.addEventListener('DOMContentLoaded', run);
/*-->*/
</script>
</body>
</html>
`;
const buildHtml = (template) => async ({ title, data }) => {
const [script, style] = await Promise.all([
fs_1.promises.readFile(path_1.default.join(__dirname, "..", "lib", `${template}.js`), "utf8"),
fs_1.promises.readFile(path_1.default.join(__dirname, "..", "lib", `${template}.css`), "utf8"),
]);
return buildHtmlTemplate(title, script, data, style);
};
const outputRawData = (strData) => {
const data = JSON.parse(strData);
return JSON.stringify(data, null, 2);
};
const outputPlainTextList = (strData) => {
var _a;
const data = JSON.parse(strData);
const bundles = {};
for (const meta of Object.values(data.nodeMetas)) {
for (const [bundleId, uid] of Object.entries(meta.moduleParts)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { metaUid: mainUid, ...lengths } = data.nodeParts[uid];
bundles[bundleId] = (_a = bundles[bundleId]) !== null && _a !== void 0 ? _a : [];
bundles[bundleId].push([meta.id, lengths]);
}
}
const bundlesEntries = Object.entries(bundles).sort((e1, e2) => e1[0].localeCompare(e2[0]));
let output = "";
const IDENT = " ";
for (const [bundleId, files] of bundlesEntries) {
output += bundleId + ":\n";
files.sort((e1, e2) => e1[0].localeCompare(e2[0]));
for (const [file, lengths] of files) {
output += IDENT + file + ":\n";
output += IDENT + IDENT + `rendered: ${String(lengths.renderedLength)}\n`;
if (data.options.gzip) {
output += IDENT + IDENT + `gzip: ${String(lengths.gzipLength)}\n`;
}
if (data.options.brotli) {
output += IDENT + IDENT + `brotli: ${String(lengths.brotliLength)}\n`;
}
}
}
return output;
};
const TEMPLATE_TYPE_RENDERED = {
network: buildHtml("network"),
sunburst: buildHtml("sunburst"),
treemap: buildHtml("treemap"),
"raw-data": async ({ data }) => outputRawData(data),
list: async ({ data }) => outputPlainTextList(data),
flamegraph: buildHtml("flamegraph"),
};
const renderTemplate = (templateType, options) => {
return TEMPLATE_TYPE_RENDERED[templateType](options);
};
exports.renderTemplate = renderTemplate;

View file

@ -0,0 +1,8 @@
import { OutputChunk } from "rollup";
interface SourceMapModuleRenderInfo {
id: string;
renderedLength: number;
code: string[];
}
export declare const getSourcemapModules: (id: string, outputChunk: OutputChunk, dir: string) => Promise<Record<string, SourceMapModuleRenderInfo>>;
export {};

View file

@ -0,0 +1,41 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSourcemapModules = void 0;
const path_1 = __importDefault(require("path"));
const source_map_1 = require("source-map");
const getBytesPerFileUsingSourceMap = (bundleId, code, map, dir) => {
const modules = {};
let line = 1;
let column = 0;
const codeChars = [...code];
for (let i = 0; i < codeChars.length; i++, column++) {
const { source } = map.originalPositionFor({
line,
column,
});
if (source != null) {
const id = path_1.default.resolve(path_1.default.dirname(path_1.default.join(dir, bundleId)), source);
const char = codeChars[i];
modules[id] = modules[id] || { id, renderedLength: 0, code: [] };
modules[id].renderedLength += Buffer.byteLength(char);
modules[id].code.push(char);
}
if (code[i] === "\n") {
line += 1;
column = -1;
}
}
return modules;
};
const getSourcemapModules = (id, outputChunk, dir) => {
if (outputChunk.map == null) {
return Promise.resolve({});
}
return source_map_1.SourceMapConsumer.with(outputChunk.map, null, (map) => {
return getBytesPerFileUsingSourceMap(id, outputChunk.code, map, dir);
});
};
exports.getSourcemapModules = getSourcemapModules;

View file

@ -0,0 +1,3 @@
export type TemplateType = "sunburst" | "treemap" | "network" | "raw-data" | "list" | "flamegraph";
declare const templates: ReadonlyArray<TemplateType>;
export default templates;

View file

@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const templates = [
"sunburst",
"treemap",
"network",
"list",
"raw-data",
"flamegraph",
];
exports.default = templates;

View file

@ -0,0 +1 @@
export declare const version = 2;

View file

@ -0,0 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = void 0;
exports.version = 2;

View file

@ -0,0 +1 @@
export declare const warn: (...args: any[]) => void;

View file

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.warn = void 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
const warn = (...args) => console.warn("[rollup-plugin-visualizer]", ...args);
exports.warn = warn;

View file

@ -0,0 +1,5 @@
export type Filter = {
bundle?: string | null | undefined;
file?: string | null | undefined;
};
export declare const createFilter: (include: Filter | Filter[] | undefined, exclude: Filter | Filter[] | undefined) => (bundleId: string, id: string) => boolean;

View file

@ -0,0 +1,56 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFilter = void 0;
const picomatch_1 = __importDefault(require("picomatch"));
function isArray(arg) {
return Array.isArray(arg);
}
function ensureArray(thing) {
if (isArray(thing))
return thing;
if (thing == null)
return [];
return [thing];
}
const globToTest = (glob) => {
const pattern = glob;
const fn = (0, picomatch_1.default)(pattern, { dot: true });
return {
test: (what) => {
const result = fn(what);
return result;
},
};
};
const testFalse = {
test: () => false,
};
const testTrue = {
test: () => true,
};
const getMatcher = (filter) => {
const bundleTest = "bundle" in filter && filter.bundle != null ? globToTest(filter.bundle) : testTrue;
const fileTest = "file" in filter && filter.file != null ? globToTest(filter.file) : testTrue;
return { bundleTest, fileTest };
};
const createFilter = (include, exclude) => {
const includeMatchers = ensureArray(include).map(getMatcher);
const excludeMatchers = ensureArray(exclude).map(getMatcher);
return (bundleId, id) => {
for (let i = 0; i < excludeMatchers.length; ++i) {
const { bundleTest, fileTest } = excludeMatchers[i];
if (bundleTest.test(bundleId) && fileTest.test(id))
return false;
}
for (let i = 0; i < includeMatchers.length; ++i) {
const { bundleTest, fileTest } = includeMatchers[i];
if (bundleTest.test(bundleId) && fileTest.test(id))
return true;
}
return !includeMatchers.length;
};
};
exports.createFilter = createFilter;

View file

@ -0,0 +1 @@
export {};

View file

@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const create_filter_1 = require("./create-filter");
(0, vitest_1.describe)("createFilter", () => {
(0, vitest_1.it)("should return true when input and output is empty", () => {
const isIncluded = (0, create_filter_1.createFilter)([], []);
(0, vitest_1.expect)(isIncluded("bundle", "file")).toBe(true);
});
(0, vitest_1.describe)("Bundle", () => {
vitest_1.it.each([
[[{ bundle: "b1.js" }], "b1.js", "file", false],
[[{ bundle: "b1.js" }], "b2.js", "file", true],
[[{ bundle: "translation-*.js" }], "b2.js", "file", true],
[[{ bundle: "translation-*.js" }], "translation-de.js", "file", false],
])("%# should exclude %j bundle %j file %j - %j", (exclude, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)([], exclude);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
vitest_1.it.each([
[[{ bundle: "b1.js" }], "b1.js", "file", true],
[[{ bundle: "b1.js" }], "b2.js", "file", false],
[[{ bundle: "translation-*.js" }], "b2.js", "file", false],
[[{ bundle: "translation-*.js" }], "translation-de.js", "file", true],
])("%# should include %j bundle %j file %j - %j", (include, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)(include, []);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
vitest_1.it.each([
[
[{ bundle: "translation-*.js" }],
[{ bundle: "translation-de.js" }],
"translation-de.js",
"file",
false,
],
[
[{ bundle: "translation-*.js" }],
[{ bundle: "translation-de.js" }],
"translation-en.js",
"file",
true,
],
])("%# should exclude included %j %j bundle %j file %j - %j", (include, exclude, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)(include, exclude);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
});
(0, vitest_1.describe)("File", () => {
vitest_1.it.each([
[[{ file: "b1.js" }], "bundle", "b1.js", false],
[[{ file: "b1.js" }], "bundle", "b2.js", true],
[[{ file: "translation-*.js" }], "bundle", "b2.js", true],
[[{ file: "translation-*.js" }], "bundle", "translation-de.js", false],
])("%# should exclude %j bundle %j file %j - %j", (exclude, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)([], exclude);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
vitest_1.it.each([
[[{ file: "b1.js" }], "bundle", "b1.js", true],
[[{ file: "b1.js" }], "bundle", "b2.js", false],
[[{ file: "translation-*.js" }], "bundle", "b2.js", false],
[[{ file: "translation-*.js" }], "bundle", "translation-de.js", true],
])("%# should include %j bundle %j file %j - %j", (include, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)(include, []);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
vitest_1.it.each([
[
[{ file: "translations/**" }],
[{ file: "translations/de.js" }],
"bundle",
"translations/de.js",
false,
],
[
[{ file: "translations/**" }],
[{ file: "translations/de.js" }],
"bundle",
"translations/en.js",
true,
],
])("%# should exclude included %j %j bundle %j file %j - %j", (include, exclude, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)(include, exclude);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
});
(0, vitest_1.describe)("File in Bundle", () => {
vitest_1.it.each([
[[{ bundle: "b1.js", file: "f1.js" }], "b1.js", "f1.js", false],
[[{ bundle: "b1.js", file: "f1.js" }], "b2.js", "f1.js", true],
[[{ bundle: "b1.js", file: "f1.js" }], "b1.js", "f2.js", true],
])("%# should exclude %j bundle %j file %j - %j", (exclude, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)([], exclude);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
vitest_1.it.each([
[[{ bundle: "b1.js", file: "f1.js" }], "b1.js", "f1.js", true],
[[{ bundle: "b1.js", file: "f1.js" }], "b2.js", "f1.js", false],
[[{ bundle: "b1.js", file: "f1.js" }], "b1.js", "f2.js", false],
])("%# should include %j bundle %j file %j - %j", (include, bundle, file, result) => {
const isIncluded = (0, create_filter_1.createFilter)(include, []);
(0, vitest_1.expect)(isIncluded(bundle, file)).toBe(result);
});
});
});

View file

@ -0,0 +1,46 @@
export type SizeKey = "renderedLength" | "gzipLength" | "brotliLength";
export declare const isModuleTree: (mod: ModuleTree | ModuleTreeLeaf) => mod is ModuleTree;
export type ModuleUID = string;
export type BundleId = string;
export interface ModuleTreeLeaf {
name: string;
uid: ModuleUID;
}
export interface ModuleTree {
name: string;
children: Array<ModuleTree | ModuleTreeLeaf>;
}
export type ModulePart = {
metaUid: ModuleUID;
} & ModuleLengths;
export type ModuleImport = {
uid: ModuleUID;
dynamic?: boolean;
};
export type ModuleMeta = {
moduleParts: Record<BundleId, ModuleUID>;
importedBy: ModuleImport[];
imported: ModuleImport[];
isEntry?: boolean;
isExternal?: boolean;
id: string;
};
export interface ModuleLengths {
renderedLength: number;
gzipLength: number;
brotliLength: number;
}
export interface VisualizerData {
version: number;
tree: ModuleTree;
nodeParts: Record<ModuleUID, ModulePart>;
nodeMetas: Record<ModuleUID, ModuleMeta>;
env: {
[key: string]: unknown;
};
options: {
gzip: boolean;
brotli: boolean;
sourcemap: boolean;
};
}

View file

@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isModuleTree = void 0;
const isModuleTree = (mod) => "children" in mod;
exports.isModuleTree = isModuleTree;

View file

@ -0,0 +1,96 @@
{
"name": "rollup-plugin-visualizer",
"version": "6.0.5",
"main": "./dist/plugin/index.js",
"author": "Denis Bardadym <bardadymchik@gmail.com>",
"license": "MIT",
"bin": "./dist/bin/cli.js",
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git@github.com:btd/rollup-plugin-visualizer.git"
},
"homepage": "https://github.com/btd/rollup-plugin-visualizer",
"bugs": {
"url": "https://github.com/btd/rollup-plugin-visualizer/issues"
},
"scripts": {
"lint": "oxlint -c .oxlintrc.json plugin src",
"format": "prettier plugin src --write --list-different",
"build": "run-p build:*",
"build:plugin": "tsc",
"build:frontend": "rollup -c rollup.config.js",
"build-dev": "rollup -c rollup.config-dev.js",
"clean": "del-cli dist",
"prebuild": "npm run clean",
"test": "vitest --run"
},
"dependencies": {
"open": "^8.0.0",
"source-map": "^0.7.4",
"yargs": "^17.5.1",
"picomatch": "^4.0.2"
},
"peerDependencies": {
"rolldown": "1.x || ^1.0.0-beta",
"rollup": "2.x || 3.x || 4.x"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"rolldown": {
"optional": true
}
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.0.0",
"@types/bytes": "^3.1.1",
"@types/d3-array": "^3.0.3",
"@types/d3-color": "^3.1.0",
"@types/d3-force": "^3.0.3",
"@types/d3-hierarchy": "^3.1.0",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/node": "^20.0.0",
"@types/picomatch": "^4.0.0",
"@types/yargs": "^17.0.10",
"bytes": "^3.1.2",
"d3-array": "^3.1.6",
"d3-color": "^3.1.0",
"d3-force": "^3.0.0",
"d3-hierarchy": "^3.1.2",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"del-cli": "^6.0.0",
"npm-run-all": "^4.1.5",
"oxlint": "^0.18.0",
"postcss": "^8.4.14",
"postcss-url": "^10.1.3",
"preact": "^10.7.2",
"prettier": "^3.1.0",
"rolldown": "^1.0.0-beta.1",
"rollup": "^4.5.2",
"rollup-plugin-postcss": "^4.0.2",
"sass": "^1.52.1",
"tslib": "^2.8.1",
"typescript": "~5.8.3",
"vitest": "^3.1.4"
},
"engines": {
"node": ">=18"
},
"keywords": [
"rollup-plugin",
"visualizer",
"network",
"treemap",
"sunburst",
"diagram"
]
}