docs: add readme, default sample data

This commit is contained in:
Connor Peet 2019-06-07 10:33:40 -07:00
Родитель ca1973d331
Коммит 3a9258bc42
17 изменённых файлов: 295 добавлений и 39 удалений

45
package-lock.json сгенерированный
Просмотреть файл

@ -925,6 +925,17 @@
"tweetnacl": "^0.14.3"
}
},
"bfj": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz",
"integrity": "sha512-+GUNvzHR4nRyGybQc2WpNJL4MJazMuvf92ueIyA0bIkPRwhhQu3IfZQ2PSoVPpCBJfmoSdOxu5rnotfFLlvYRQ==",
"requires": {
"bluebird": "^3.5.1",
"check-types": "^7.3.0",
"hoopy": "^0.1.2",
"tryer": "^1.0.0"
}
},
"big.js": {
"version": "5.2.2",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/big.js/-/big.js-5.2.2.tgz",
@ -949,8 +960,7 @@
"bluebird": {
"version": "3.5.5",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/bluebird/-/bluebird-3.5.5.tgz",
"integrity": "sha1-qNCv1zJR7/u9X+OEp31zADwXpx8=",
"dev": true
"integrity": "sha1-qNCv1zJR7/u9X+OEp31zADwXpx8="
},
"bn.js": {
"version": "4.11.8",
@ -1289,6 +1299,11 @@
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"check-types": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-7.4.0.tgz",
"integrity": "sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg=="
},
"chokidar": {
"version": "2.1.6",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/chokidar/-/chokidar-2.1.6.tgz",
@ -2411,8 +2426,7 @@
"event-lite": {
"version": "0.1.2",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/event-lite/-/event-lite-0.1.2.tgz",
"integrity": "sha1-g4o+D93e+MyQ8SgAbI5VpOTkwRs=",
"dev": true
"integrity": "sha1-g4o+D93e+MyQ8SgAbI5VpOTkwRs="
},
"eventemitter3": {
"version": "3.1.2",
@ -4024,6 +4038,11 @@
"parse-passwd": "^1.0.0"
}
},
"hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
"integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ=="
},
"hosted-git-info": {
"version": "2.7.1",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
@ -4342,8 +4361,7 @@
"ieee754": {
"version": "1.1.13",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha1-7BaFWOlaoYH9h9N/VcMrvLZwi4Q=",
"dev": true
"integrity": "sha1-7BaFWOlaoYH9h9N/VcMrvLZwi4Q="
},
"iferr": {
"version": "0.1.5",
@ -4425,8 +4443,7 @@
"int64-buffer": {
"version": "0.1.10",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/int64-buffer/-/int64-buffer-0.1.10.tgz",
"integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=",
"dev": true
"integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM="
},
"internal-ip": {
"version": "4.3.0",
@ -4707,8 +4724,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
@ -4741,8 +4757,7 @@
"js-base64": {
"version": "2.5.1",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/js-base64/-/js-base64-2.5.1.tgz",
"integrity": "sha1-Hvo57yxfeYC7F4St5KivLeMpESE=",
"dev": true
"integrity": "sha1-Hvo57yxfeYC7F4St5KivLeMpESE="
},
"js-tokens": {
"version": "4.0.0",
@ -5299,7 +5314,6 @@
"version": "0.1.26",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
"integrity": "sha1-3TxQsm8FnyXn7e42REGDWOKprYk=",
"dev": true,
"requires": {
"event-lite": "^0.1.1",
"ieee754": "^1.1.8",
@ -8073,6 +8087,11 @@
"glob": "^7.1.2"
}
},
"tryer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA=="
},
"ts-loader": {
"version": "6.0.2",
"resolved": "https://watchmixer.pkgs.visualstudio.com/_packaging/mixer/npm/registry/ts-loader/-/ts-loader-6.0.2.tgz",

Просмотреть файл

@ -2,14 +2,15 @@
"name": "@mixer/webpack-bundle-compare",
"version": "0.1.0",
"description": "Tool to compare webpack bundle files",
"main": "index.js",
"main": "dist/index.js",
"scripts": {
"fmt": "prettier --write \"src/**/*.{tsx,ts}\" && npm run test:lint -- --fix",
"test": "mocha --opts mocha.opts && npm run test:fmt && npm run test:lint",
"test:fmt": "prettier --list-different \"src/**/*.{tsx,ts}\" || echo \"Run npm run fmt to fix formatting on these files\"",
"test:lint": "tslint --project tsconfig.json \"src/**/*.{ts,tsx}\"",
"build": "webpack --config webpack.production.js",
"start": "webpack-dev-server"
"start": "webpack-dev-server",
"prepare": "tsc -p tsconfig.package.json"
},
"repository": {
"type": "git",
@ -28,6 +29,11 @@
"url": "https://github.com/mixer/webpack-bundle-compare/issues"
},
"homepage": "https://github.com/mixer/webpack-bundle-compare#readme",
"dependencies": {
"bfj": "^6.1.1",
"js-base64": "^2.5.1",
"msgpack-lite": "^0.1.26"
},
"devDependencies": {
"@mixer/retrieval": "^2.0.2",
"@types/chai": "^4.1.7",
@ -58,9 +64,7 @@
"flexboxgrid": "^6.3.1",
"fs-extra": "^8.0.1",
"html-webpack-plugin": "^3.2.0",
"js-base64": "^2.5.1",
"mocha": "^6.1.4",
"msgpack-lite": "^0.1.26",
"node-sass": "^4.12.0",
"normalize.css": "^8.0.1",
"pako": "^1.0.10",

Просмотреть файл

@ -0,0 +1,15 @@
The samples in this folder are generated from builds of Spectrum (https://github.com/withspectrum/spectrum), which carry the following license:
---
Copyright 2018 Space Program Inc.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Двоичные данные
public/samples/spectrum1.msp.gz Normal file

Двоичный файл не отображается.

Двоичные данные
public/samples/spectrum2.msp.gz Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -1,17 +1,68 @@
# @mixer/webpack-bundle-compare
This is a tool that allows you to compare webpack bundle analysis files over time. Check it out [here](https://webpackbundlecomparison.z5.web.core.windows.net).
![](./screenshot.png)
## Usage
The bundle comparison tool takes URLs of webpack stat outputs and displays them. You can use the JSON output from the [webpack-bundle-analyzer](github.com/webpack-contrib/webpack-bundle-analyzer), but we also include a plugin here if you don't use that package.
### Using the Plugin
First, install the plugin.
```
npm install --save-dev @mixer/webpack-bundle-compare
```
Then, add it to your webpack.config.js file.
```diff
+const { BundleComparisonPlugin } = require('@mixer/webpack-bundle-compare');
...
plugins: [
+ new BundleComparisonPlugin()
]
```
By default, this'll output a file named `stats.msp.gz` in your build output. This is a gzipped msgpack file--for large projects webpack bundle stats can get pretty big and slow down your build, so we try to make it fast. You can configure the output format and filename by passing options to the plugin:
```js
new BundleComparisonPlugin({
// File to create, relative to the webpack build output path:
file: 'myStatsFile.msp.gz',
// Format to output, "msgpack" by default:
format: 'msgpack' | 'json',
// Whether to gzip the output, defaults to true:
gzip: true | false,
})
```
Once you have this file, you can upload it somewhere such as Azure blob storage, and serve it in the tool. The module exposes a convenient way to get a direct preloaded link in the tool:
```js
const { getComparisonAddress } = require('@mixer/webpack-bundle-compare');
const link = getComparisonAddress([
'http://example.com/stats1.msp.gz',
'http://example.com/stats1.msp.gz',
])
console.log('See your comparison at:', link);
```
## Contributing
### Architecture
Most logic is contained within the UI. We pull webpack analysis JSON down from URLs, process them, and output UI. State is driven via Redux, and the heavy processing of the (potentially very large) webpack stat files happen in a webworker. Actions sent in the redux state are mirrored to the webworker, and in turn the webworker and send actions which get fired back to the host application.
It's a React/Redux app. We pull webpack analysis JSON down from URLs, process them, and output UI. State is driven via Redux, and the parsing and unzipping of the (potentially very large) webpack stat files happen in a webworker. Actions sent in the redux state are mirrored to the webworker, and in turn the webworker and send actions which get fired back to the host application. The work we do atop the stats file is not particularly interesting--an assortment of parsing, walking, and graph generation functions.
### Iteration
To develop against the UI:
1. Create a folder called "public/samples", and place JSON files in there.
1. Create a folder called "public/samples", and place JSON files in there. Or use the ones we already have preloaded.
2. Set the `WBC_FILES` environment variable to a comma-delimited list of the filenames you placed in there.
3. Running the webpack dev server via `npm start` will now serve the files you have placed in there.
### Limitations
The main degraded area in our reporting deals with concatenated modules. Essentially, webpack gives us a list of modules and concatenated modules at the top level of their stats output. The concatenated modules are nested inside the parent module, and will contain the full set of module information. However, they don't have module IDs, and when Webpack reports that a given module was imported, it only reports the top-level concatenation. So we can drill into single modules and concatenated bundles fairly well, but we can't cross-reference imports cleanly.
3. Running the webpack dev server via `npm start` will now serve the files you have placed in there.

Двоичные данные
screenshot.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 182 KiB

Просмотреть файл

@ -14,6 +14,7 @@ export const replaceLoaderInIdentifier = (identifier?: string) => {
const index = identifier.lastIndexOf('!');
return index === -1 ? identifier : identifier.slice(index + 1);
};
/**
* Normalizes an identifier so that it carries over time: removing the
* hash from the end of concatenated module identifiers.
@ -22,11 +23,14 @@ export const normalizeIdentifier = (identifier?: string) =>
identifier ? identifier.replace(/ [a-z0-9]+$/, '') : '';
/**
* Normalizes an identifier so that it carries over time.
* Higher-order function that caches input to the wrapped function by argument.
* It's expected that the first argument to the function is a reference type,
* and that the function is not variadic.
*
* This works pretty simply and gracefully. JavaScript gives us the number of
* arguments a given function takes. We use a WeakMap on the first argument,
* and then store recursive maps for each subsequent argument.
*/
const humanReadableIdentifier = (identifier: string) =>
replaceLoaderInIdentifier(identifier).replace(/ [a-z0-9]+$/, '');
// tslint:disable-next-line
const cacheByArg = <T extends Function>(fn: T): T => {
const cacheMap = new WeakMap<any, any>();
@ -261,7 +265,6 @@ export const identifyModuleType = (id: string): ModuleType => {
*/
export interface IWebpackModuleComparisonOutput {
identifier: string;
readableId: string;
name: string;
type: ModuleType;
fromSize: number;
@ -287,7 +290,6 @@ export const compareAllModules = (
const normalized = normalizeIdentifier(m.identifier);
output[normalized] = {
identifier: normalized,
readableId: humanReadableIdentifier(m.identifier),
name: replaceLoaderInIdentifier(m.name),
type: identifyModuleType(m.identifier),
nodeModule: getNodeModuleFromIdentifier(m.identifier) || undefined,
@ -305,7 +307,6 @@ export const compareAllModules = (
} else {
output[normalized] = {
identifier: normalized,
readableId: humanReadableIdentifier(m.identifier),
name: replaceLoaderInIdentifier(m.name),
type: identifyModuleType(m.identifier),
nodeModule: getNodeModuleFromIdentifier(m.identifier) || undefined,

14
src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,14 @@
import { Base64 } from 'js-base64';
export * from './plugin';
/**
* A link to the bundle comparison tool preloaded to pull the stats from
* the provided URLs.
*/
export function getComparisonAddress(
bundleStatUrls: string[],
toolUrl: string = 'https://webpackbundlecomparison.z5.web.core.windows.net',
) {
return `${toolUrl}?urls=${bundleStatUrls.map(Base64.encodeURI).join(',')}`;
}

135
src/plugin.ts Normal file
Просмотреть файл

@ -0,0 +1,135 @@
import { IBfjOptions, streamify } from 'bfj';
import { createWriteStream } from 'fs';
import { createEncodeStream } from 'msgpack-lite';
import { resolve } from 'path';
import { PassThrough } from 'stream';
import { Compiler, Stats } from 'webpack';
import { createGzip } from 'zlib';
export const enum StatsFormat {
Json = 'json',
MsgPack = 'msgpack',
}
export interface IPluginOptions {
/**
* Filename to output.
*/
file: string;
/**
* Whether to gzip the stats file. Defaults to true.
*/
gzip: boolean;
/**
* Format of the stats output. Defaults to msgpack.
*/
format: StatsFormat;
/**
* Options to pass to bfj, when using the "JSON" message format.
*/
bfjOptions?: IBfjOptions;
}
const defaultFilename = (gzip: boolean, format: StatsFormat) => {
let filename = 'stats';
if (format === StatsFormat.MsgPack) {
filename += '.msp';
} else {
filename += '.json';
}
if (gzip) {
filename += '.gz';
}
return filename;
};
const writeToStream = (
options: IPluginOptions,
data: Stats.ToJsonOutput,
stream: NodeJS.WritableStream,
) => {
if (options.format === StatsFormat.MsgPack) {
const encode = createEncodeStream();
encode.pipe(stream);
encode.write(data);
encode.end();
} else {
const encode = streamify(data, {
promises: 'ignore',
buffers: 'ignore',
maps: 'ignore',
iterables: 'ignore',
circular: 'ignore',
...options.bfjOptions,
});
encode.pipe(stream);
}
};
/**
* Default listener used in the compiler. Webpack 4 will have a callback
* function, but webpack 3 doesn't provide one, so we use this.
*/
const defaultListener = (err?: Error) => {
if (err) {
// tslint:disable-next-line
console.error(`Error in the ${BundleComparisonPlugin.name}: ${err.stack || err.message}`);
}
};
/**
* Plugin that writes stat information to the compilation output.
*/
export class BundleComparisonPlugin {
private readonly options: IPluginOptions;
constructor({
gzip = true,
format = StatsFormat.MsgPack,
...defaults
}: Partial<IPluginOptions> = {}) {
this.options = {
gzip,
format,
file: defaultFilename(gzip, format),
...defaults,
};
}
public apply(compiler: Compiler) {
const handler = (stats: Stats, callback: (err?: Error) => void = defaultListener) => {
const target = createWriteStream(resolve(compiler.outputPath, this.options.file));
target
.on('end', () => callback())
.on('finish', () => callback())
.on('error', callback);
const jsonData = stats.toJson({
source: false,
chunkModules: false,
});
if (this.options.gzip) {
const compressor = this.options.gzip ? createGzip() : new PassThrough();
compressor.pipe(target);
writeToStream(this.options, jsonData, compressor);
} else {
writeToStream(this.options, jsonData, target);
}
};
if (compiler.hooks) {
compiler.hooks.done.tapAsync(BundleComparisonPlugin.name, handler);
} else {
compiler.plugin('done', handler);
}
}
}

16
src/types/bfj.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,16 @@
declare module 'bfj' {
export interface IBfjOptions {
space?: string | number;
promises?: 'ignore';
buffers?: 'ignore';
maps?: 'ignore';
iterables?: 'ignore';
circular?: 'ignore';
bufferLength?: number;
highWaterMark?: number;
yieldRate?: number;
Promise?: PromiseConstructorLike;
}
export function streamify(data: any, options?: IBfjOptions): NodeJS.ReadableStream;
}

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -1,8 +1,10 @@
{
"extends": "./tsconfig.cli.json",
"extends": "./tsconfig.package.json",
"compilerOptions": {
"module": "esnext",
"jsx": "react",
"lib": ["es6", "es7", "dom", "webworker"],
}
},
"include": ["src"],
"exclude": []
}

Просмотреть файл

@ -14,9 +14,8 @@
"target": "es6",
"lib": ["es6", "es7"],
"outDir": "dist",
"types": [
"node",
"mocha"
]
}
"types": ["node", "mocha"]
},
"include": ["src"],
"exclude": ["src/client"]
}

Просмотреть файл

@ -52,7 +52,7 @@ module.exports = {
new DefinePlugin({
INITIAL_FILES: process.env.WBC_FILES
? JSON.stringify(process.env.WBC_FILES.split(','))
: '[]',
: JSON.stringify(['public/samples/spectrum1.msg.gz', 'public/samples/spectrum2.msg.gz']),
}),
],
devServer: {