1 | const path = require("path");
|
2 | const fs = require("fs");
|
3 | const chalk = require("chalk");
|
4 | const { WrappedPlugin } = require("./WrappedPlugin");
|
5 | const { getModuleName, getLoaderNames, prependLoader } = require("./utils");
|
6 | const {
|
7 | getHumanOutput,
|
8 | getMiscOutput,
|
9 | getPluginsOutput,
|
10 | getLoadersOutput,
|
11 | smpTag,
|
12 | } = require("./output");
|
13 |
|
14 | const NS = path.dirname(fs.realpathSync(__filename));
|
15 |
|
16 | module.exports = class SpeedMeasurePlugin {
|
17 | constructor(options) {
|
18 | this.options = options || {};
|
19 |
|
20 | this.timeEventData = {};
|
21 | this.smpPluginAdded = false;
|
22 |
|
23 | this.wrap = this.wrap.bind(this);
|
24 | this.getOutput = this.getOutput.bind(this);
|
25 | this.addTimeEvent = this.addTimeEvent.bind(this);
|
26 | this.apply = this.apply.bind(this);
|
27 | this.provideLoaderTiming = this.provideLoaderTiming.bind(this);
|
28 | }
|
29 |
|
30 | wrap(config) {
|
31 | if (this.options.disable) return config;
|
32 |
|
33 | config.plugins = (config.plugins || []).map(plugin => {
|
34 | const pluginName =
|
35 | Object.keys(this.options.pluginNames || {}).find(
|
36 | pluginName => plugin === this.options.pluginNames[pluginName]
|
37 | ) ||
|
38 | (plugin.constructor && plugin.constructor.name) ||
|
39 | "(unable to deduce plugin name)";
|
40 | return new WrappedPlugin(plugin, pluginName, this);
|
41 | });
|
42 |
|
43 | if (config.module && this.options.granularLoaderData) {
|
44 | config.module = prependLoader(config.module);
|
45 | }
|
46 |
|
47 | if (!this.smpPluginAdded) {
|
48 | config.plugins = config.plugins.concat(this);
|
49 | this.smpPluginAdded = true;
|
50 | }
|
51 |
|
52 | return config;
|
53 | }
|
54 |
|
55 | getOutput() {
|
56 | const outputObj = {};
|
57 | if (this.timeEventData.misc)
|
58 | outputObj.misc = getMiscOutput(this.timeEventData.misc);
|
59 | if (this.timeEventData.plugins)
|
60 | outputObj.plugins = getPluginsOutput(this.timeEventData.plugins);
|
61 | if (this.timeEventData.loaders)
|
62 | outputObj.loaders = getLoadersOutput(this.timeEventData.loaders);
|
63 |
|
64 | if (this.options.outputFormat === "json")
|
65 | return JSON.stringify(outputObj, null, 2);
|
66 | if (typeof this.options.outputFormat === "function")
|
67 | return this.options.outputFormat(outputObj);
|
68 | return getHumanOutput(outputObj, {
|
69 | verbose: this.options.outputFormat === "humanVerbose",
|
70 | });
|
71 | }
|
72 |
|
73 | addTimeEvent(category, event, eventType, data = {}) {
|
74 | const allowFailure = data.allowFailure;
|
75 | delete data.allowFailure;
|
76 |
|
77 | const tED = this.timeEventData;
|
78 | if (!tED[category]) tED[category] = {};
|
79 | if (!tED[category][event]) tED[category][event] = [];
|
80 | const eventList = tED[category][event];
|
81 | const curTime = new Date().getTime();
|
82 |
|
83 | if (eventType === "start") {
|
84 | data.start = curTime;
|
85 | eventList.push(data);
|
86 | } else if (eventType === "end") {
|
87 | const matchingEvent = eventList.find(e => {
|
88 | const allowOverwrite = !e.end || !data.fillLast;
|
89 | const idMatch = e.id !== undefined && e.id === data.id;
|
90 | const nameMatch =
|
91 | !data.id && e.name !== undefined && e.name === data.name;
|
92 | return allowOverwrite && (idMatch || nameMatch);
|
93 | });
|
94 | const eventToModify =
|
95 | matchingEvent || (data.fillLast && eventList.find(e => !e.end));
|
96 | if (!eventToModify) {
|
97 | console.error(
|
98 | "Could not find a matching event to end",
|
99 | category,
|
100 | event,
|
101 | data
|
102 | );
|
103 | if (allowFailure) return;
|
104 | throw new Error("No matching event!");
|
105 | }
|
106 |
|
107 | eventToModify.end = curTime;
|
108 | }
|
109 | }
|
110 |
|
111 | apply(compiler) {
|
112 | if (this.options.disable) return;
|
113 |
|
114 | compiler.plugin("compile", () => {
|
115 | this.addTimeEvent("misc", "compile", "start", { watch: false });
|
116 | });
|
117 | compiler.plugin("done", () => {
|
118 | this.addTimeEvent("misc", "compile", "end", { fillLast: true });
|
119 |
|
120 | const outputToFile = typeof this.options.outputTarget === "string";
|
121 | chalk.enabled = !outputToFile;
|
122 | const output = this.getOutput();
|
123 | chalk.enabled = true;
|
124 | if (outputToFile) {
|
125 | const writeMethod = fs.existsSync(this.options.outputTarget)
|
126 | ? fs.appendFile
|
127 | : fs.writeFile;
|
128 | writeMethod(this.options.outputTarget, output + "\n", err => {
|
129 | if (err) throw err;
|
130 | console.log(
|
131 | smpTag() + "Outputted timing info to " + this.options.outputTarget
|
132 | );
|
133 | });
|
134 | } else {
|
135 | const outputFunc = this.options.outputTarget || console.log;
|
136 | outputFunc(output);
|
137 | }
|
138 |
|
139 | this.timeEventData = {};
|
140 | });
|
141 |
|
142 | compiler.plugin("compilation", compilation => {
|
143 | compilation.plugin("normal-module-loader", loaderContext => {
|
144 | loaderContext[NS] = this.provideLoaderTiming;
|
145 | });
|
146 |
|
147 | compilation.plugin("build-module", module => {
|
148 | const name = getModuleName(module);
|
149 | if (name) {
|
150 | this.addTimeEvent("loaders", "build", "start", {
|
151 | name,
|
152 | fillLast: true,
|
153 | loaders: getLoaderNames(module.loaders),
|
154 | });
|
155 | }
|
156 | });
|
157 |
|
158 | compilation.plugin("succeed-module", module => {
|
159 | const name = getModuleName(module);
|
160 | if (name) {
|
161 | this.addTimeEvent("loaders", "build", "end", {
|
162 | name,
|
163 | fillLast: true,
|
164 | });
|
165 | }
|
166 | });
|
167 | });
|
168 | }
|
169 |
|
170 | provideLoaderTiming(info) {
|
171 | const infoData = { id: info.id };
|
172 | if (info.type !== "end") {
|
173 | infoData.loader = info.loaderName;
|
174 | infoData.name = info.module;
|
175 | }
|
176 |
|
177 | this.addTimeEvent("loaders", "build-specific", info.type, infoData);
|
178 | }
|
179 | };
|