vite-plugin-performance 0.0.1 → 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,119 +1,122 @@
1
- # `wrapPlugin` – Vite Plugin Performance Wrapper
1
+ # vite-plugin-performance
2
2
 
3
- `wrapPlugin` is a utility function designed to wrap Vite plugins and monitor the execution time of their lifecycle hooks. It helps developers gain insights into plugin performance and identify potential bottlenecks.
3
+ [中文文档](./README.zh-CN.md)
4
4
 
5
- ---
5
+ `vite-plugin-performance` wraps one or more Vite plugins and measures how long each lifecycle hook takes. It keeps the original behaviour intact while giving you actionable performance insights.
6
6
 
7
7
  ## ✨ Features
8
8
 
9
- - Automatically wraps and times specified Vite plugin hooks
10
- - Supports threshold-based logging (only logs slow hooks)
11
- - Allows custom hook selection
12
- - Provides a callback for custom performance tracking
13
- - Array merge support with override using `defu`
14
-
15
- ---
9
+ - Wrap a single plugin or an array of plugins with one call
10
+ - Ship with a sensible default hook list, or use `hooks: 'all'` to cover every function hook
11
+ - Threshold-based reporting so you only see the hooks that really hurt
12
+ - Pluggable logger, formatter, and lifecycle callback for easy monitoring integration
13
+ - Works with asynchronous hooks and still handles errors gracefully
16
14
 
17
15
  ## 📦 Installation
18
16
 
19
- ```sh
20
- npm i -D vite-plugin-performance
17
+ ```bash
18
+ pnpm add -D vite-plugin-performance
19
+ # or npm / yarn / bun
21
20
  ```
22
21
 
23
- ---
24
-
25
- ## 🚀 Usage
22
+ ## 🚀 Quick Start
26
23
 
27
24
  ```ts
28
- import someVitePlugin from 'some-vite-plugin'
29
25
  import { defineConfig } from 'vite'
26
+ import Inspect from 'vite-plugin-inspect'
30
27
  import { wrapPlugin } from 'vite-plugin-performance'
31
28
 
32
29
  export default defineConfig({
33
30
  plugins: [
34
- wrapPlugin(
35
- someVitePlugin(),
36
- {
37
- threshold: 50, // Log only hooks taking longer than 50ms
38
- onHookExecution: ({ pluginName, hookName, duration }) => {
39
- // Custom logic, e.g., send to monitoring service
40
- reportToMonitoringSystem({ pluginName, hookName, duration })
41
- },
42
- }
43
- ),
31
+ wrapPlugin(Inspect(), {
32
+ threshold: 50,
33
+ onHookExecution({ pluginName, hookName, duration }) {
34
+ reportToAPM({ pluginName, hookName, duration })
35
+ },
36
+ }),
44
37
  ],
45
38
  })
46
39
  ```
47
40
 
48
- ---
49
-
50
- ## 🧩 API
41
+ Whenever a hook crosses the threshold (default `0` ms) you will see output similar to:
51
42
 
52
- ### `wrapPlugin(plugin: Plugin, options?: Partial<WrapPluginOptions>): Plugin`
53
-
54
- #### Parameters
43
+ ```
44
+ [inspect] transform ⏱ 78.42 ms
45
+ ```
55
46
 
56
- - `plugin`: A Vite plugin object
57
- - `options`: Optional configuration object
47
+ ## ⚙️ Options
58
48
 
59
- #### `WrapPluginOptions` Interface
49
+ | Option | Type | Default | Description |
50
+ | ----------------- | ---------------------------- | --------------------------------- | ------------------------------------------------------------------------- |
51
+ | `hooks` | `PluginHookName[] \| 'all'` | `DEFAULT_PLUGIN_HOOKS` | Choose which hooks to wrap; pass `'all'` to wrap every function hook |
52
+ | `threshold` | `number` | `0` | Only hooks with a duration greater or equal to the threshold are reported |
53
+ | `silent` | `boolean` | `false` | Disable the built-in logger |
54
+ | `logger` | `(message, context) => void` | `console.log` | Custom log writer |
55
+ | `formatter` | `(context) => string` | `[plugin] transform ⏱ 12.34 ms` | Custom message formatter |
56
+ | `onHookExecution` | `(context) => void` | `undefined` | Callback invoked after a hook finishes |
57
+ | `clock` | `() => number` | `performance.now` or `Date.now` | High-resolution timer, handy for tests |
60
58
 
61
- | Option | Type | Default | Description |
62
- | ----------------- | ----------------------------------------- | ---------------------------- | -------------------------------------- |
63
- | `threshold` | `number` | `0` | Minimum duration (ms) to log a hook |
64
- | `onHookExecution` | `(params: OnHookExecutionParams) => void` | `undefined` | Callback invoked when a hook completes |
65
- | `hooks` | `PluginHooks[]` | Common hook list (see below) | List of Vite plugin hooks to wrap |
66
- | `slient` | `boolean` | `false` | Suppress default console output |
59
+ > The legacy misspelled option `slient` is still recognised and mapped to `silent`.
67
60
 
68
- #### Default Hook List:
61
+ ### Default Hook List
69
62
 
70
63
  ```ts
71
- [
72
- 'options',
73
- 'buildStart',
74
- 'resolveId',
75
- 'load',
76
- 'transform',
77
- 'buildEnd',
78
- 'generateBundle',
79
- 'renderChunk',
80
- 'writeBundle',
81
- ]
64
+ import { DEFAULT_PLUGIN_HOOKS } from 'vite-plugin-performance'
65
+ // [
66
+ // 'options',
67
+ // 'config',
68
+ // 'configResolved',
69
+ // 'configureServer',
70
+ // 'buildStart',
71
+ // 'resolveId',
72
+ // 'load',
73
+ // 'transform',
74
+ // 'buildEnd',
75
+ // 'generateBundle',
76
+ // 'renderChunk',
77
+ // 'writeBundle',
78
+ // ]
82
79
  ```
83
80
 
84
- ---
81
+ ## 🧠 Advanced Usage
85
82
 
86
- ## 🧪 Example Output
83
+ ### Wrap Multiple Plugins
87
84
 
88
- When a hook's execution time exceeds the threshold, output will be shown in the console:
85
+ ```ts
86
+ const pluginA = ...
87
+ const pluginB = ...
89
88
 
89
+ export default defineConfig({
90
+ plugins: wrapPlugin([pluginA, pluginB], { threshold: 20 }),
91
+ })
90
92
  ```
91
- [my-plugin] transform ⏱ 132.57 ms
92
- ```
93
-
94
- ---
95
93
 
96
- ## 📘 Types
97
-
98
- ### `OnHookExecutionParams`
94
+ ### Custom Log Format
99
95
 
100
96
  ```ts
101
- interface OnHookExecutionParams {
102
- pluginName: string
103
- hookName: string
104
- args: any[]
105
- duration: number
106
- }
97
+ wrapPlugin(plugin, {
98
+ formatter({ pluginName, hookName, duration }) {
99
+ return `${pluginName}:${hookName} took ${duration}ms`
100
+ },
101
+ logger(message) {
102
+ myLogger.info(message)
103
+ },
104
+ })
107
105
  ```
108
106
 
109
- ---
107
+ ### Limit to Specific Hooks
110
108
 
111
- ## ⚠️ Notes
109
+ ```ts
110
+ wrapPlugin(plugin, {
111
+ hooks: ['resolveId', 'load', 'transform'],
112
+ })
113
+ ```
112
114
 
113
- - This utility only works with object-based plugin definitions. It does not support plugins returned as arrays.
114
- - Async hooks are automatically awaited to ensure accurate timing.
115
+ ## 🧪 Testing
115
116
 
116
- ---
117
+ ```bash
118
+ pnpm --filter vite-plugin-performance test
119
+ ```
117
120
 
118
121
  ## 📄 License
119
122
 
@@ -0,0 +1,123 @@
1
+ # vite-plugin-performance
2
+
3
+ [English Documentation](./README.md)
4
+
5
+ 一个用于包裹 Vite 插件、统计生命周期钩子耗时的小工具,帮助你快速定位构建链路中的性能瓶颈。插件原有行为保持不变,我们只在钩子执行完成后采集数据并按需输出或上报。
6
+
7
+ ## ✨ 特性
8
+
9
+ - 支持同时包裹单个插件或插件数组
10
+ - 内置常用钩子列表,可设置 `hooks: 'all'` 包裹所有函数钩子
11
+ - 阈值过滤机制,只关注真正慢的钩子
12
+ - 自定义日志、格式化与回调,方便接入监控体系
13
+ - 兼容异步钩子,异常也会统计耗时
14
+
15
+ ## 📦 安装
16
+
17
+ ```bash
18
+ pnpm add -D vite-plugin-performance
19
+ # 或 npm / yarn / bun
20
+ ```
21
+
22
+ ## 🚀 快速上手
23
+
24
+ ```ts
25
+ import { defineConfig } from 'vite'
26
+ import Inspect from 'vite-plugin-inspect'
27
+ import { wrapPlugin } from 'vite-plugin-performance'
28
+
29
+ export default defineConfig({
30
+ plugins: [
31
+ wrapPlugin(Inspect(), {
32
+ threshold: 50,
33
+ onHookExecution({ pluginName, hookName, duration }) {
34
+ reportToAPM({ pluginName, hookName, duration })
35
+ },
36
+ }),
37
+ ],
38
+ })
39
+ ```
40
+
41
+ 当某个钩子耗时超过设置的阈值(默认 0 ms)时,控制台会输出:
42
+
43
+ ```
44
+ [inspect] transform ⏱ 78.42 ms
45
+ ```
46
+
47
+ ## ⚙️ 选项
48
+
49
+ | 选项 | 类型 | 默认值 | 说明 |
50
+ | ----------------- | ---------------------------- | --------------------------------- | --------------------------------------------- |
51
+ | `hooks` | `PluginHookName[] \| 'all'` | `DEFAULT_PLUGIN_HOOKS` | 指定需要包裹的钩子;传 `all` 包裹所有函数钩子 |
52
+ | `threshold` | `number` | `0` | 只有耗时大于等于该值的钩子才会被记录 |
53
+ | `silent` | `boolean` | `false` | 关闭内置日志输出 |
54
+ | `logger` | `(message, context) => void` | `console.log` | 自定义日志输出函数 |
55
+ | `formatter` | `(context) => string` | `[plugin] transform ⏱ 12.34 ms` | 自定义日志内容格式 |
56
+ | `onHookExecution` | `(context) => void` | `undefined` | 钩子执行完毕后触发,可用于上报 |
57
+ | `clock` | `() => number` | `performance.now` 或 `Date.now` | 高精度计时器,便于测试或自定义时间源 |
58
+
59
+ > 兼容早期误拼写的 `slient` 选项,会自动映射为 `silent`。
60
+
61
+ ### 默认钩子列表
62
+
63
+ ```ts
64
+ import { DEFAULT_PLUGIN_HOOKS } from 'vite-plugin-performance'
65
+ // [
66
+ // 'options',
67
+ // 'config',
68
+ // 'configResolved',
69
+ // 'configureServer',
70
+ // 'buildStart',
71
+ // 'resolveId',
72
+ // 'load',
73
+ // 'transform',
74
+ // 'buildEnd',
75
+ // 'generateBundle',
76
+ // 'renderChunk',
77
+ // 'writeBundle',
78
+ // ]
79
+ ```
80
+
81
+ ## 🧠 进阶用法
82
+
83
+ ### 包裹多个插件
84
+
85
+ ```ts
86
+ const pluginA = ...
87
+ const pluginB = ...
88
+
89
+ export default defineConfig({
90
+ plugins: wrapPlugin([pluginA, pluginB], { threshold: 20 }),
91
+ })
92
+ ```
93
+
94
+ ### 自定义日志格式
95
+
96
+ ```ts
97
+ wrapPlugin(plugin, {
98
+ formatter({ pluginName, hookName, duration }) {
99
+ return `${pluginName}:${hookName} took ${duration}ms`
100
+ },
101
+ logger(message) {
102
+ myLogger.info(message)
103
+ },
104
+ })
105
+ ```
106
+
107
+ ### 仅关注特定钩子
108
+
109
+ ```ts
110
+ wrapPlugin(plugin, {
111
+ hooks: ['resolveId', 'load', 'transform'],
112
+ })
113
+ ```
114
+
115
+ ## 🧪 测试
116
+
117
+ ```bash
118
+ pnpm --filter vite-plugin-performance test
119
+ ```
120
+
121
+ ## 📄 许可
122
+
123
+ MIT
package/dist/index.cjs CHANGED
@@ -1,122 +1,127 @@
1
1
  'use strict';
2
2
 
3
- function getDefaultOptions() {
3
+ const DEFAULT_PLUGIN_HOOKS = [
4
+ "options",
5
+ "config",
6
+ "configResolved",
7
+ "configureServer",
8
+ "buildStart",
9
+ "resolveId",
10
+ "load",
11
+ "transform",
12
+ "buildEnd",
13
+ "generateBundle",
14
+ "renderChunk",
15
+ "writeBundle"
16
+ ];
17
+ const DEFAULT_THRESHOLD = 0;
18
+ const ANONYMOUS_PLUGIN_NAME = "anonymous-plugin";
19
+ const DEFAULT_FORMATTER = ({ pluginName, hookName, duration }) => {
20
+ const paddedHook = hookName.padEnd(20);
21
+ const formattedDuration = duration.toFixed(2).padStart(7);
22
+ return `[${pluginName}] ${paddedHook} \u23F1 ${formattedDuration} ms`;
23
+ };
24
+ const DEFAULT_LOGGER = (message) => {
25
+ console.log(message);
26
+ };
27
+
28
+ const hasPerformanceNow = typeof globalThis !== "undefined" && typeof globalThis.performance?.now === "function";
29
+ const defaultClock = hasPerformanceNow ? () => globalThis.performance.now() : () => Date.now();
30
+ function resolveOptions(options = {}) {
31
+ const {
32
+ hooks = DEFAULT_PLUGIN_HOOKS,
33
+ threshold = DEFAULT_THRESHOLD,
34
+ silent = options.slient ?? false,
35
+ logger = DEFAULT_LOGGER,
36
+ formatter = DEFAULT_FORMATTER,
37
+ onHookExecution,
38
+ clock = defaultClock
39
+ } = options;
4
40
  return {
5
- threshold: 0,
6
- hooks: [
7
- "options",
8
- "buildStart",
9
- "resolveId",
10
- "load",
11
- "transform",
12
- "buildEnd",
13
- "generateBundle",
14
- "renderChunk",
15
- "writeBundle"
16
- ],
17
- slient: false
41
+ hooks,
42
+ threshold,
43
+ silent,
44
+ logger,
45
+ formatter,
46
+ onHookExecution,
47
+ clock
18
48
  };
19
49
  }
20
-
21
- function isPlainObject(value) {
22
- if (value === null || typeof value !== "object") {
23
- return false;
24
- }
25
- const prototype = Object.getPrototypeOf(value);
26
- if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {
27
- return false;
28
- }
29
- if (Symbol.iterator in value) {
30
- return false;
31
- }
32
- if (Symbol.toStringTag in value) {
33
- return Object.prototype.toString.call(value) === "[object Module]";
34
- }
35
- return true;
50
+ function resolvePluginName(rawName) {
51
+ return rawName && rawName.length > 0 ? rawName : ANONYMOUS_PLUGIN_NAME;
36
52
  }
37
53
 
38
- function _defu(baseObject, defaults, namespace = ".", merger) {
39
- if (!isPlainObject(defaults)) {
40
- return _defu(baseObject, {}, namespace, merger);
41
- }
42
- const object = Object.assign({}, defaults);
43
- for (const key in baseObject) {
44
- if (key === "__proto__" || key === "constructor") {
45
- continue;
46
- }
47
- const value = baseObject[key];
48
- if (value === null || value === void 0) {
49
- continue;
50
- }
51
- if (merger && merger(object, key, value, namespace)) {
52
- continue;
53
- }
54
- if (Array.isArray(value) && Array.isArray(object[key])) {
55
- object[key] = [...value, ...object[key]];
56
- } else if (isPlainObject(value) && isPlainObject(object[key])) {
57
- object[key] = _defu(
58
- value,
59
- object[key],
60
- (namespace ? `${namespace}.` : "") + key.toString(),
61
- merger
62
- );
63
- } else {
64
- object[key] = value;
65
- }
66
- }
67
- return object;
54
+ function ensureArray(value) {
55
+ return Array.isArray(value) ? value : [value];
68
56
  }
69
- function createDefu(merger) {
70
- return (...arguments_) => (
71
- // eslint-disable-next-line unicorn/no-array-reduce
72
- arguments_.reduce((p, c) => _defu(p, c, "", merger), {})
73
- );
57
+ function isPromiseLike(value) {
58
+ return typeof value === "object" && value !== null && "then" in value;
74
59
  }
75
60
 
76
- const defuOverrideArray = createDefu((obj, key, value) => {
77
- if (Array.isArray(obj[key]) && Array.isArray(value)) {
78
- obj[key] = value;
79
- return true;
80
- }
81
- });
82
-
83
61
  function wrapPlugin(maybePlugin, options) {
84
- const isArr = Array.isArray(maybePlugin);
85
- const plugins = isArr ? maybePlugin : [maybePlugin];
86
- const wrappedPlugins = plugins.map((plugin) => {
87
- const wrapped = { ...plugin };
88
- const { threshold = 0, onHookExecution, hooks, slient } = defuOverrideArray(
89
- options,
90
- getDefaultOptions()
91
- );
92
- if (Array.isArray(hooks)) {
93
- for (const hook of hooks) {
94
- if (typeof plugin[hook] === "function") {
95
- wrapped[hook] = async function(...args) {
96
- const start = performance.now();
97
- const result = await plugin[hook]?.apply(this, args);
98
- const end = performance.now();
99
- const duration = Math.round(end - start);
100
- if (duration >= threshold) {
101
- const pluginName = plugin.name;
102
- if (!slient) {
103
- console.log(`[${pluginName}] ${hook.padEnd(20)} \u23F1 ${duration.toFixed(2).padStart(6)} ms`);
104
- }
105
- onHookExecution?.({
106
- pluginName,
107
- hookName: hook,
108
- args,
109
- duration
110
- });
111
- }
112
- return result;
113
- };
62
+ const resolvedOptions = resolveOptions(options);
63
+ const plugins = ensureArray(maybePlugin);
64
+ const wrapped = plugins.map((plugin) => wrapSinglePlugin(plugin, resolvedOptions));
65
+ return Array.isArray(maybePlugin) ? wrapped : wrapped[0];
66
+ }
67
+ function wrapSinglePlugin(plugin, options) {
68
+ const wrapped = { ...plugin };
69
+ const pluginName = resolvePluginName(plugin.name);
70
+ const hooksToWrap = resolveHooks(plugin, options.hooks);
71
+ for (const hookName of hooksToWrap) {
72
+ const original = plugin[hookName];
73
+ if (typeof original !== "function") {
74
+ continue;
75
+ }
76
+ wrapped[hookName] = function performanceHook(...args) {
77
+ const start = options.clock();
78
+ const invoke = () => original.apply(this, args);
79
+ const finalize = (result) => {
80
+ reportExecution({ pluginName, hookName, args, start, options });
81
+ return result;
82
+ };
83
+ const handleError = (error) => {
84
+ reportExecution({ pluginName, hookName, args, start, options });
85
+ throw error;
86
+ };
87
+ try {
88
+ const result = invoke();
89
+ if (isPromiseLike(result)) {
90
+ return result.then(finalize, handleError);
114
91
  }
92
+ return finalize(result);
93
+ } catch (error) {
94
+ return handleError(error);
115
95
  }
96
+ };
97
+ }
98
+ return wrapped;
99
+ }
100
+ function resolveHooks(plugin, hooks) {
101
+ if (hooks === "all") {
102
+ return Object.keys(plugin).filter((key) => typeof plugin[key] === "function");
103
+ }
104
+ return [...hooks];
105
+ }
106
+ function reportExecution(params) {
107
+ const { pluginName, hookName, args, start, options } = params;
108
+ const duration = Math.max(0, options.clock() - start);
109
+ const context = {
110
+ pluginName,
111
+ hookName,
112
+ args,
113
+ duration
114
+ };
115
+ if (duration >= options.threshold) {
116
+ if (!options.silent) {
117
+ const message = options.formatter(context);
118
+ options.logger(message, context);
116
119
  }
117
- return wrapped;
118
- });
119
- return isArr ? wrappedPlugins : wrappedPlugins[0];
120
+ options.onHookExecution?.(context);
121
+ }
120
122
  }
121
123
 
124
+ exports.DEFAULT_PLUGIN_HOOKS = DEFAULT_PLUGIN_HOOKS;
125
+ exports.DEFAULT_THRESHOLD = DEFAULT_THRESHOLD;
126
+ exports.resolveOptions = resolveOptions;
122
127
  exports.wrapPlugin = wrapPlugin;
package/dist/index.d.cts CHANGED
@@ -1,20 +1,50 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- type PluginHooks = keyof Plugin;
4
- interface OnHookExecutionParams {
3
+ type PluginHookName = Extract<keyof Plugin, string>;
4
+ interface HookExecutionContext {
5
5
  pluginName: string;
6
- hookName: string;
7
- args: any[];
6
+ hookName: PluginHookName;
7
+ args: unknown[];
8
8
  duration: number;
9
9
  }
10
+ type HookLogger = (message: string, context: HookExecutionContext) => void;
11
+ type HookFormatter = (context: HookExecutionContext) => string;
12
+ type HookClock = () => number;
10
13
  interface WrapPluginOptions {
14
+ /** Wrap only these hooks. Use `all` to wrap every function hook on the plugin. */
15
+ hooks?: PluginHookName[] | 'all';
16
+ /** Minimum duration (ms) required to log or emit notifications. */
11
17
  threshold?: number;
12
- onHookExecution?: (params: OnHookExecutionParams) => void;
13
- hooks?: PluginHooks[];
18
+ /** Disable the default logger output. */
19
+ silent?: boolean;
20
+ /** Custom logger called with the formatted message. Defaults to `console.log`. */
21
+ logger?: HookLogger;
22
+ /** Custom formatter for the log message. */
23
+ formatter?: HookFormatter;
24
+ /** Lifecycle callback invoked after a hook has completed. */
25
+ onHookExecution?: (context: HookExecutionContext) => void;
26
+ /** Provide a custom high-resolution clock implementation. */
27
+ clock?: HookClock;
28
+ /** @deprecated backwards compatibility typo */
14
29
  slient?: boolean;
15
30
  }
31
+ interface ResolvedWrapPluginOptions {
32
+ hooks: PluginHookName[] | 'all';
33
+ threshold: number;
34
+ silent: boolean;
35
+ logger: HookLogger;
36
+ formatter: HookFormatter;
37
+ onHookExecution?: (context: HookExecutionContext) => void;
38
+ clock: HookClock;
39
+ }
40
+
41
+ declare const DEFAULT_PLUGIN_HOOKS: PluginHookName[];
42
+ declare const DEFAULT_THRESHOLD = 0;
43
+
44
+ declare function resolveOptions(options?: Partial<WrapPluginOptions>): ResolvedWrapPluginOptions;
16
45
 
17
46
  declare function wrapPlugin(maybePlugin: Plugin, options?: Partial<WrapPluginOptions>): Plugin;
18
47
  declare function wrapPlugin(maybePlugin: Plugin[], options?: Partial<WrapPluginOptions>): Plugin[];
19
48
 
20
- export { type OnHookExecutionParams, type PluginHooks, type WrapPluginOptions, wrapPlugin };
49
+ export { DEFAULT_PLUGIN_HOOKS, DEFAULT_THRESHOLD, resolveOptions, wrapPlugin };
50
+ export type { HookClock, HookExecutionContext, HookFormatter, HookLogger, PluginHookName, ResolvedWrapPluginOptions, WrapPluginOptions };
package/dist/index.d.mts CHANGED
@@ -1,20 +1,50 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- type PluginHooks = keyof Plugin;
4
- interface OnHookExecutionParams {
3
+ type PluginHookName = Extract<keyof Plugin, string>;
4
+ interface HookExecutionContext {
5
5
  pluginName: string;
6
- hookName: string;
7
- args: any[];
6
+ hookName: PluginHookName;
7
+ args: unknown[];
8
8
  duration: number;
9
9
  }
10
+ type HookLogger = (message: string, context: HookExecutionContext) => void;
11
+ type HookFormatter = (context: HookExecutionContext) => string;
12
+ type HookClock = () => number;
10
13
  interface WrapPluginOptions {
14
+ /** Wrap only these hooks. Use `all` to wrap every function hook on the plugin. */
15
+ hooks?: PluginHookName[] | 'all';
16
+ /** Minimum duration (ms) required to log or emit notifications. */
11
17
  threshold?: number;
12
- onHookExecution?: (params: OnHookExecutionParams) => void;
13
- hooks?: PluginHooks[];
18
+ /** Disable the default logger output. */
19
+ silent?: boolean;
20
+ /** Custom logger called with the formatted message. Defaults to `console.log`. */
21
+ logger?: HookLogger;
22
+ /** Custom formatter for the log message. */
23
+ formatter?: HookFormatter;
24
+ /** Lifecycle callback invoked after a hook has completed. */
25
+ onHookExecution?: (context: HookExecutionContext) => void;
26
+ /** Provide a custom high-resolution clock implementation. */
27
+ clock?: HookClock;
28
+ /** @deprecated backwards compatibility typo */
14
29
  slient?: boolean;
15
30
  }
31
+ interface ResolvedWrapPluginOptions {
32
+ hooks: PluginHookName[] | 'all';
33
+ threshold: number;
34
+ silent: boolean;
35
+ logger: HookLogger;
36
+ formatter: HookFormatter;
37
+ onHookExecution?: (context: HookExecutionContext) => void;
38
+ clock: HookClock;
39
+ }
40
+
41
+ declare const DEFAULT_PLUGIN_HOOKS: PluginHookName[];
42
+ declare const DEFAULT_THRESHOLD = 0;
43
+
44
+ declare function resolveOptions(options?: Partial<WrapPluginOptions>): ResolvedWrapPluginOptions;
16
45
 
17
46
  declare function wrapPlugin(maybePlugin: Plugin, options?: Partial<WrapPluginOptions>): Plugin;
18
47
  declare function wrapPlugin(maybePlugin: Plugin[], options?: Partial<WrapPluginOptions>): Plugin[];
19
48
 
20
- export { type OnHookExecutionParams, type PluginHooks, type WrapPluginOptions, wrapPlugin };
49
+ export { DEFAULT_PLUGIN_HOOKS, DEFAULT_THRESHOLD, resolveOptions, wrapPlugin };
50
+ export type { HookClock, HookExecutionContext, HookFormatter, HookLogger, PluginHookName, ResolvedWrapPluginOptions, WrapPluginOptions };
package/dist/index.d.ts CHANGED
@@ -1,20 +1,50 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
- type PluginHooks = keyof Plugin;
4
- interface OnHookExecutionParams {
3
+ type PluginHookName = Extract<keyof Plugin, string>;
4
+ interface HookExecutionContext {
5
5
  pluginName: string;
6
- hookName: string;
7
- args: any[];
6
+ hookName: PluginHookName;
7
+ args: unknown[];
8
8
  duration: number;
9
9
  }
10
+ type HookLogger = (message: string, context: HookExecutionContext) => void;
11
+ type HookFormatter = (context: HookExecutionContext) => string;
12
+ type HookClock = () => number;
10
13
  interface WrapPluginOptions {
14
+ /** Wrap only these hooks. Use `all` to wrap every function hook on the plugin. */
15
+ hooks?: PluginHookName[] | 'all';
16
+ /** Minimum duration (ms) required to log or emit notifications. */
11
17
  threshold?: number;
12
- onHookExecution?: (params: OnHookExecutionParams) => void;
13
- hooks?: PluginHooks[];
18
+ /** Disable the default logger output. */
19
+ silent?: boolean;
20
+ /** Custom logger called with the formatted message. Defaults to `console.log`. */
21
+ logger?: HookLogger;
22
+ /** Custom formatter for the log message. */
23
+ formatter?: HookFormatter;
24
+ /** Lifecycle callback invoked after a hook has completed. */
25
+ onHookExecution?: (context: HookExecutionContext) => void;
26
+ /** Provide a custom high-resolution clock implementation. */
27
+ clock?: HookClock;
28
+ /** @deprecated backwards compatibility typo */
14
29
  slient?: boolean;
15
30
  }
31
+ interface ResolvedWrapPluginOptions {
32
+ hooks: PluginHookName[] | 'all';
33
+ threshold: number;
34
+ silent: boolean;
35
+ logger: HookLogger;
36
+ formatter: HookFormatter;
37
+ onHookExecution?: (context: HookExecutionContext) => void;
38
+ clock: HookClock;
39
+ }
40
+
41
+ declare const DEFAULT_PLUGIN_HOOKS: PluginHookName[];
42
+ declare const DEFAULT_THRESHOLD = 0;
43
+
44
+ declare function resolveOptions(options?: Partial<WrapPluginOptions>): ResolvedWrapPluginOptions;
16
45
 
17
46
  declare function wrapPlugin(maybePlugin: Plugin, options?: Partial<WrapPluginOptions>): Plugin;
18
47
  declare function wrapPlugin(maybePlugin: Plugin[], options?: Partial<WrapPluginOptions>): Plugin[];
19
48
 
20
- export { type OnHookExecutionParams, type PluginHooks, type WrapPluginOptions, wrapPlugin };
49
+ export { DEFAULT_PLUGIN_HOOKS, DEFAULT_THRESHOLD, resolveOptions, wrapPlugin };
50
+ export type { HookClock, HookExecutionContext, HookFormatter, HookLogger, PluginHookName, ResolvedWrapPluginOptions, WrapPluginOptions };
package/dist/index.mjs CHANGED
@@ -1,120 +1,122 @@
1
- function getDefaultOptions() {
1
+ const DEFAULT_PLUGIN_HOOKS = [
2
+ "options",
3
+ "config",
4
+ "configResolved",
5
+ "configureServer",
6
+ "buildStart",
7
+ "resolveId",
8
+ "load",
9
+ "transform",
10
+ "buildEnd",
11
+ "generateBundle",
12
+ "renderChunk",
13
+ "writeBundle"
14
+ ];
15
+ const DEFAULT_THRESHOLD = 0;
16
+ const ANONYMOUS_PLUGIN_NAME = "anonymous-plugin";
17
+ const DEFAULT_FORMATTER = ({ pluginName, hookName, duration }) => {
18
+ const paddedHook = hookName.padEnd(20);
19
+ const formattedDuration = duration.toFixed(2).padStart(7);
20
+ return `[${pluginName}] ${paddedHook} \u23F1 ${formattedDuration} ms`;
21
+ };
22
+ const DEFAULT_LOGGER = (message) => {
23
+ console.log(message);
24
+ };
25
+
26
+ const hasPerformanceNow = typeof globalThis !== "undefined" && typeof globalThis.performance?.now === "function";
27
+ const defaultClock = hasPerformanceNow ? () => globalThis.performance.now() : () => Date.now();
28
+ function resolveOptions(options = {}) {
29
+ const {
30
+ hooks = DEFAULT_PLUGIN_HOOKS,
31
+ threshold = DEFAULT_THRESHOLD,
32
+ silent = options.slient ?? false,
33
+ logger = DEFAULT_LOGGER,
34
+ formatter = DEFAULT_FORMATTER,
35
+ onHookExecution,
36
+ clock = defaultClock
37
+ } = options;
2
38
  return {
3
- threshold: 0,
4
- hooks: [
5
- "options",
6
- "buildStart",
7
- "resolveId",
8
- "load",
9
- "transform",
10
- "buildEnd",
11
- "generateBundle",
12
- "renderChunk",
13
- "writeBundle"
14
- ],
15
- slient: false
39
+ hooks,
40
+ threshold,
41
+ silent,
42
+ logger,
43
+ formatter,
44
+ onHookExecution,
45
+ clock
16
46
  };
17
47
  }
18
-
19
- function isPlainObject(value) {
20
- if (value === null || typeof value !== "object") {
21
- return false;
22
- }
23
- const prototype = Object.getPrototypeOf(value);
24
- if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {
25
- return false;
26
- }
27
- if (Symbol.iterator in value) {
28
- return false;
29
- }
30
- if (Symbol.toStringTag in value) {
31
- return Object.prototype.toString.call(value) === "[object Module]";
32
- }
33
- return true;
48
+ function resolvePluginName(rawName) {
49
+ return rawName && rawName.length > 0 ? rawName : ANONYMOUS_PLUGIN_NAME;
34
50
  }
35
51
 
36
- function _defu(baseObject, defaults, namespace = ".", merger) {
37
- if (!isPlainObject(defaults)) {
38
- return _defu(baseObject, {}, namespace, merger);
39
- }
40
- const object = Object.assign({}, defaults);
41
- for (const key in baseObject) {
42
- if (key === "__proto__" || key === "constructor") {
43
- continue;
44
- }
45
- const value = baseObject[key];
46
- if (value === null || value === void 0) {
47
- continue;
48
- }
49
- if (merger && merger(object, key, value, namespace)) {
50
- continue;
51
- }
52
- if (Array.isArray(value) && Array.isArray(object[key])) {
53
- object[key] = [...value, ...object[key]];
54
- } else if (isPlainObject(value) && isPlainObject(object[key])) {
55
- object[key] = _defu(
56
- value,
57
- object[key],
58
- (namespace ? `${namespace}.` : "") + key.toString(),
59
- merger
60
- );
61
- } else {
62
- object[key] = value;
63
- }
64
- }
65
- return object;
52
+ function ensureArray(value) {
53
+ return Array.isArray(value) ? value : [value];
66
54
  }
67
- function createDefu(merger) {
68
- return (...arguments_) => (
69
- // eslint-disable-next-line unicorn/no-array-reduce
70
- arguments_.reduce((p, c) => _defu(p, c, "", merger), {})
71
- );
55
+ function isPromiseLike(value) {
56
+ return typeof value === "object" && value !== null && "then" in value;
72
57
  }
73
58
 
74
- const defuOverrideArray = createDefu((obj, key, value) => {
75
- if (Array.isArray(obj[key]) && Array.isArray(value)) {
76
- obj[key] = value;
77
- return true;
78
- }
79
- });
80
-
81
59
  function wrapPlugin(maybePlugin, options) {
82
- const isArr = Array.isArray(maybePlugin);
83
- const plugins = isArr ? maybePlugin : [maybePlugin];
84
- const wrappedPlugins = plugins.map((plugin) => {
85
- const wrapped = { ...plugin };
86
- const { threshold = 0, onHookExecution, hooks, slient } = defuOverrideArray(
87
- options,
88
- getDefaultOptions()
89
- );
90
- if (Array.isArray(hooks)) {
91
- for (const hook of hooks) {
92
- if (typeof plugin[hook] === "function") {
93
- wrapped[hook] = async function(...args) {
94
- const start = performance.now();
95
- const result = await plugin[hook]?.apply(this, args);
96
- const end = performance.now();
97
- const duration = Math.round(end - start);
98
- if (duration >= threshold) {
99
- const pluginName = plugin.name;
100
- if (!slient) {
101
- console.log(`[${pluginName}] ${hook.padEnd(20)} \u23F1 ${duration.toFixed(2).padStart(6)} ms`);
102
- }
103
- onHookExecution?.({
104
- pluginName,
105
- hookName: hook,
106
- args,
107
- duration
108
- });
109
- }
110
- return result;
111
- };
60
+ const resolvedOptions = resolveOptions(options);
61
+ const plugins = ensureArray(maybePlugin);
62
+ const wrapped = plugins.map((plugin) => wrapSinglePlugin(plugin, resolvedOptions));
63
+ return Array.isArray(maybePlugin) ? wrapped : wrapped[0];
64
+ }
65
+ function wrapSinglePlugin(plugin, options) {
66
+ const wrapped = { ...plugin };
67
+ const pluginName = resolvePluginName(plugin.name);
68
+ const hooksToWrap = resolveHooks(plugin, options.hooks);
69
+ for (const hookName of hooksToWrap) {
70
+ const original = plugin[hookName];
71
+ if (typeof original !== "function") {
72
+ continue;
73
+ }
74
+ wrapped[hookName] = function performanceHook(...args) {
75
+ const start = options.clock();
76
+ const invoke = () => original.apply(this, args);
77
+ const finalize = (result) => {
78
+ reportExecution({ pluginName, hookName, args, start, options });
79
+ return result;
80
+ };
81
+ const handleError = (error) => {
82
+ reportExecution({ pluginName, hookName, args, start, options });
83
+ throw error;
84
+ };
85
+ try {
86
+ const result = invoke();
87
+ if (isPromiseLike(result)) {
88
+ return result.then(finalize, handleError);
112
89
  }
90
+ return finalize(result);
91
+ } catch (error) {
92
+ return handleError(error);
113
93
  }
94
+ };
95
+ }
96
+ return wrapped;
97
+ }
98
+ function resolveHooks(plugin, hooks) {
99
+ if (hooks === "all") {
100
+ return Object.keys(plugin).filter((key) => typeof plugin[key] === "function");
101
+ }
102
+ return [...hooks];
103
+ }
104
+ function reportExecution(params) {
105
+ const { pluginName, hookName, args, start, options } = params;
106
+ const duration = Math.max(0, options.clock() - start);
107
+ const context = {
108
+ pluginName,
109
+ hookName,
110
+ args,
111
+ duration
112
+ };
113
+ if (duration >= options.threshold) {
114
+ if (!options.silent) {
115
+ const message = options.formatter(context);
116
+ options.logger(message, context);
114
117
  }
115
- return wrapped;
116
- });
117
- return isArr ? wrappedPlugins : wrappedPlugins[0];
118
+ options.onHookExecution?.(context);
119
+ }
118
120
  }
119
121
 
120
- export { wrapPlugin };
122
+ export { DEFAULT_PLUGIN_HOOKS, DEFAULT_THRESHOLD, resolveOptions, wrapPlugin };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vite-plugin-performance",
3
3
  "type": "module",
4
- "version": "0.0.1",
4
+ "version": "1.0.0-alpha.0",
5
5
  "description": "vite-plugin-performance",
6
6
  "author": "ice breaker <1324318532@qq.com>",
7
7
  "license": "MIT",