vite-plugin-ops 0.1.2 → 1.0.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/dist/deps.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 读取项目依赖列表
3
+ * @param cwd 项目根目录
4
+ * @returns 依赖名称集合
5
+ */
6
+ export declare function readProjectDependencies(cwd: string): Set<string>;
7
+ /**
8
+ * 检查模式列表是否匹配项目依赖
9
+ * 改进的匹配逻辑:避免误匹配(如 vue 匹配到 vuex)
10
+ * @param patterns 匹配模式列表
11
+ * @param depsArray 依赖数组
12
+ */
13
+ export declare function hasDependencyMatch(patterns: string[], depsArray: string[]): boolean;
14
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../src/deps.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA8BhE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAmBnF"}
package/dist/index.cjs CHANGED
@@ -8,7 +8,9 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
  var fs__default = /*#__PURE__*/_interopDefault(fs);
9
9
  var path__default = /*#__PURE__*/_interopDefault(path);
10
10
 
11
- // src/index.ts
11
+ // src/deps.ts
12
+
13
+ // src/presets.ts
12
14
  var COMMON_LARGE_LIBS = {
13
15
  // UI Frameworks
14
16
  react: ["react", "react-dom"],
@@ -51,16 +53,46 @@ var MEDIUM_LIB_GROUPS = {
51
53
  "form": ["react-hook-form", "formik", "async-validator"],
52
54
  "i18n": ["i18next", "react-i18next", "vue-i18n"]
53
55
  };
54
- var patternCache = /* @__PURE__ */ new Map();
56
+ var VERY_LARGE_LIBS = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
57
+
58
+ // src/matcher.ts
59
+ var PRIORITIES = {
60
+ CUSTOM: 100,
61
+ // 用户自定义分组
62
+ DETECTED: 90,
63
+ // 插件检测到的框架
64
+ LARGE_LIB: 80,
65
+ // 大型库
66
+ MEDIUM_LIB: 70,
67
+ // 中型库
68
+ ALL_DEPS: 50
69
+ // 所有依赖(aggressive 模式)
70
+ };
55
71
  var MAX_PATH_LENGTH = 2e3;
72
+ var MAX_CACHE_SIZE = 100;
73
+ var patternCache = /* @__PURE__ */ new Map();
74
+ var cacheKeys = [];
75
+ var logger = {
76
+ warn: (msg) => console.warn(`[vite-plugin-ops] ${msg}`),
77
+ error: (msg) => console.error(`[vite-plugin-ops] ERROR: ${msg}`),
78
+ debug: (msg) => process.env.DEBUG && console.log(`[vite-plugin-ops] DEBUG: ${msg}`)
79
+ };
56
80
  function normalizeId(id) {
57
81
  return id.replace(/\\/g, "/").replace(/%5C/g, "/");
58
82
  }
83
+ function manageCache() {
84
+ while (cacheKeys.length >= MAX_CACHE_SIZE) {
85
+ const oldestKey = cacheKeys.shift();
86
+ if (oldestKey) {
87
+ patternCache.delete(oldestKey);
88
+ }
89
+ }
90
+ }
59
91
  function makeNodeModulesPattern(pkg) {
60
92
  if (pkg instanceof RegExp) {
61
93
  return (id) => {
62
94
  if (id.length > MAX_PATH_LENGTH) {
63
- console.warn(`[vite-plugin-ops] Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
95
+ logger.warn(`Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
64
96
  return false;
65
97
  }
66
98
  return pkg.test(id);
@@ -68,55 +100,27 @@ function makeNodeModulesPattern(pkg) {
68
100
  }
69
101
  let re = patternCache.get(pkg);
70
102
  if (!re) {
103
+ manageCache();
71
104
  const scoped = pkg.startsWith("@");
72
105
  const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
73
106
  const base = scoped ? escaped : `(?:@[^/]+/)?${escaped}`;
74
- re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, "i");
107
+ const flags = process.platform === "win32" ? "i" : "";
108
+ re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, flags);
75
109
  patternCache.set(pkg, re);
110
+ cacheKeys.push(pkg);
76
111
  }
77
112
  return (id) => {
78
113
  if (id.length > MAX_PATH_LENGTH) {
79
- console.warn(`[vite-plugin-ops] Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
114
+ logger.warn(`Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
80
115
  return false;
81
116
  }
82
117
  return re.test(id);
83
118
  };
84
119
  }
85
- function isValidPackageJson(obj) {
86
- if (typeof obj !== "object" || obj === null) return false;
87
- const json = obj;
88
- if (json.dependencies !== void 0) {
89
- if (typeof json.dependencies !== "object" || json.dependencies === null) return false;
90
- for (const val of Object.values(json.dependencies)) {
91
- if (typeof val !== "string") return false;
92
- }
93
- }
94
- return true;
95
- }
96
- function readProjectDependencies(cwd) {
97
- try {
98
- const pkgPath = path__default.default.join(cwd, "package.json");
99
- const content = fs__default.default.readFileSync(pkgPath, "utf8");
100
- const json = JSON.parse(content);
101
- if (!isValidPackageJson(json)) {
102
- console.warn("[vite-plugin-ops] Invalid package.json format: dependencies field is malformed");
103
- return /* @__PURE__ */ new Set();
104
- }
105
- return new Set(Object.keys(json.dependencies || {}));
106
- } catch (error) {
107
- if (error instanceof SyntaxError) {
108
- console.warn("[vite-plugin-ops] Failed to parse package.json: Invalid JSON format");
109
- } else if (error.code === "ENOENT") {
110
- console.warn("[vite-plugin-ops] package.json not found in:", cwd);
111
- } else if (process.env.DEBUG) {
112
- console.warn("[vite-plugin-ops] Failed to read package.json:", error);
113
- }
114
- return /* @__PURE__ */ new Set();
115
- }
116
- }
117
120
  function buildGroupMatchers(options, projectDeps, pluginHints) {
118
121
  const matchers = [];
119
- const strategy = options.strategy || "balanced";
122
+ const strategy = options.strategy;
123
+ const depsArray = Array.from(projectDeps);
120
124
  if (options.groups) {
121
125
  for (const [name, patterns] of Object.entries(options.groups)) {
122
126
  if (patterns && patterns.length) {
@@ -129,7 +133,7 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
129
133
  matchers.push({
130
134
  name,
131
135
  test: (id) => testers.some((t) => t(id)),
132
- priority: 100
136
+ priority: PRIORITIES.CUSTOM
133
137
  });
134
138
  }
135
139
  }
@@ -146,63 +150,49 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
146
150
  matchers.push({
147
151
  name,
148
152
  test: (id) => testers.some((t) => t(id)),
149
- priority: 90
153
+ priority: PRIORITIES.DETECTED
150
154
  });
151
155
  }
152
156
  if (strategy === "aggressive") {
153
- for (const dep of projectDeps) {
157
+ for (const dep of depsArray) {
154
158
  if (!dep.startsWith("@types/")) {
155
159
  matchers.push({
156
160
  name: dep,
157
161
  test: makeNodeModulesPattern(dep),
158
- priority: 50
162
+ priority: PRIORITIES.ALL_DEPS
159
163
  });
160
164
  }
161
165
  }
162
166
  } else if (strategy === "balanced") {
163
167
  for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
164
- const hasAny = patterns.some((p) => {
165
- const pkgName = p.replace(/\//g, "");
166
- return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
167
- });
168
- if (hasAny) {
168
+ if (hasDependencyMatch(patterns, depsArray)) {
169
169
  const testers = patterns.map(makeNodeModulesPattern);
170
170
  matchers.push({
171
171
  name: groupName,
172
172
  test: (id) => testers.some((t) => t(id)),
173
- priority: 80
173
+ priority: PRIORITIES.LARGE_LIB
174
174
  });
175
175
  }
176
176
  }
177
177
  for (const [groupName, patterns] of Object.entries(MEDIUM_LIB_GROUPS)) {
178
- const hasAny = patterns.some((p) => {
179
- return Array.from(projectDeps).some(
180
- (dep) => dep.includes(p.replace(/\//g, "").replace(/\*/g, ""))
181
- );
182
- });
183
- if (hasAny) {
178
+ if (hasDependencyMatch(patterns, depsArray)) {
184
179
  const testers = patterns.map(makeNodeModulesPattern);
185
180
  matchers.push({
186
181
  name: groupName,
187
182
  test: (id) => testers.some((t) => t(id)),
188
- priority: 70
183
+ priority: PRIORITIES.MEDIUM_LIB
189
184
  });
190
185
  }
191
186
  }
192
187
  } else if (strategy === "conservative") {
193
- const veryLargeLibs = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
194
188
  for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
195
- if (veryLargeLibs.includes(groupName)) {
196
- const hasAny = patterns.some((p) => {
197
- const pkgName = p.replace(/\//g, "");
198
- return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
199
- });
200
- if (hasAny) {
189
+ if (VERY_LARGE_LIBS.includes(groupName)) {
190
+ if (hasDependencyMatch(patterns, depsArray)) {
201
191
  const testers = patterns.map(makeNodeModulesPattern);
202
192
  matchers.push({
203
193
  name: groupName,
204
194
  test: (id) => testers.some((t) => t(id)),
205
- priority: 80
195
+ priority: PRIORITIES.LARGE_LIB
206
196
  });
207
197
  }
208
198
  }
@@ -210,6 +200,61 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
210
200
  }
211
201
  return matchers.sort((a, b) => b.priority - a.priority);
212
202
  }
203
+
204
+ // src/deps.ts
205
+ var MAX_PACKAGE_JSON_SIZE = 1024 * 1024;
206
+ function isValidPackageJson(obj) {
207
+ if (typeof obj !== "object" || obj === null) return false;
208
+ const json = obj;
209
+ if (json.dependencies !== void 0) {
210
+ if (typeof json.dependencies !== "object" || json.dependencies === null) return false;
211
+ for (const val of Object.values(json.dependencies)) {
212
+ if (typeof val !== "string") return false;
213
+ }
214
+ }
215
+ return true;
216
+ }
217
+ function readProjectDependencies(cwd) {
218
+ try {
219
+ const pkgPath = path__default.default.join(cwd, "package.json");
220
+ const stats = fs__default.default.statSync(pkgPath);
221
+ if (stats.size > MAX_PACKAGE_JSON_SIZE) {
222
+ logger.warn(`package.json too large (${stats.size} bytes), skipping`);
223
+ return /* @__PURE__ */ new Set();
224
+ }
225
+ const content = fs__default.default.readFileSync(pkgPath, "utf8");
226
+ const json = JSON.parse(content);
227
+ if (!isValidPackageJson(json)) {
228
+ logger.warn("Invalid package.json format: dependencies field is malformed");
229
+ return /* @__PURE__ */ new Set();
230
+ }
231
+ return new Set(Object.keys(json.dependencies || {}));
232
+ } catch (error) {
233
+ if (error instanceof SyntaxError) {
234
+ logger.warn("Failed to parse package.json: Invalid JSON format");
235
+ } else if (error.code === "ENOENT") {
236
+ logger.warn(`package.json not found in: ${cwd}`);
237
+ } else if (process.env.DEBUG) {
238
+ logger.debug(`Failed to read package.json: ${error}`);
239
+ }
240
+ return /* @__PURE__ */ new Set();
241
+ }
242
+ }
243
+ function hasDependencyMatch(patterns, depsArray) {
244
+ const depsSet = new Set(depsArray);
245
+ return patterns.some((pattern) => {
246
+ if (pattern.includes("/")) {
247
+ return depsArray.some((dep) => dep.startsWith(pattern));
248
+ }
249
+ return depsSet.has(pattern) || depsArray.some((dep) => {
250
+ if (dep === pattern) return true;
251
+ if (dep.startsWith(`@${pattern}/`)) return true;
252
+ return false;
253
+ });
254
+ });
255
+ }
256
+
257
+ // src/index.ts
213
258
  function OPS(opts = {}) {
214
259
  const options = {
215
260
  override: opts.override ?? false,
@@ -219,22 +264,32 @@ function OPS(opts = {}) {
219
264
  };
220
265
  let groupsRef = [];
221
266
  const manualChunks = (id) => {
222
- const nid = normalizeId(id);
223
- if (!/\/node_modules\//.test(nid)) return void 0;
224
- for (const g of groupsRef) {
225
- if (g.test(nid)) return g.name;
267
+ try {
268
+ const nid = normalizeId(id);
269
+ if (!/\/node_modules\//.test(nid)) return void 0;
270
+ for (const g of groupsRef) {
271
+ if (g.test(nid)) return g.name;
272
+ }
273
+ return "vendor";
274
+ } catch (error) {
275
+ logger.warn(`Error in manualChunks for ${id}: ${error}`);
276
+ return "vendor";
226
277
  }
227
- return "vendor";
228
278
  };
229
279
  const assetFileNamesFn = (assetInfo) => {
230
- const name = assetInfo.name ?? "";
231
- const ext = name.split(".").pop()?.toLowerCase();
232
- if (ext === "css") return "css/[name]-[hash][extname]";
233
- if (["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"].includes(ext ?? ""))
234
- return "img/[name]-[hash][extname]";
235
- if (["woff", "woff2", "eot", "ttf", "otf"].includes(ext ?? ""))
236
- return "fonts/[name]-[hash][extname]";
237
- return "assets/[name]-[hash][extname]";
280
+ try {
281
+ const name = assetInfo.name ?? "";
282
+ const ext = name.split(".").pop()?.toLowerCase();
283
+ if (ext === "css") return "css/[name]-[hash][extname]";
284
+ if (["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"].includes(ext ?? ""))
285
+ return "img/[name]-[hash][extname]";
286
+ if (["woff", "woff2", "eot", "ttf", "otf"].includes(ext ?? ""))
287
+ return "fonts/[name]-[hash][extname]";
288
+ return "assets/[name]-[hash][extname]";
289
+ } catch (error) {
290
+ logger.warn(`Error in assetFileNames: ${error}`);
291
+ return "assets/[name]-[hash][extname]";
292
+ }
238
293
  };
239
294
  return {
240
295
  name: "vite-plugin-ops",
@@ -250,14 +305,14 @@ function OPS(opts = {}) {
250
305
  manualChunks
251
306
  };
252
307
  let output;
253
- if (shouldMerge) {
254
- const base = existingOutput;
255
- const merged = { ...base };
256
- if (!("entryFileNames" in merged)) merged["entryFileNames"] = injected.entryFileNames;
257
- if (!("chunkFileNames" in merged)) merged["chunkFileNames"] = injected.chunkFileNames;
258
- if (!("assetFileNames" in merged)) merged["assetFileNames"] = injected.assetFileNames;
259
- if (!("manualChunks" in merged)) merged["manualChunks"] = injected.manualChunks;
260
- output = merged;
308
+ if (shouldMerge && existingOutput) {
309
+ const existing = existingOutput;
310
+ output = {
311
+ entryFileNames: existing?.entryFileNames ?? injected.entryFileNames,
312
+ chunkFileNames: existing?.chunkFileNames ?? injected.chunkFileNames,
313
+ assetFileNames: existing?.assetFileNames ?? injected.assetFileNames,
314
+ manualChunks: existing?.manualChunks ?? injected.manualChunks
315
+ };
261
316
  } else {
262
317
  output = injected;
263
318
  }
package/dist/index.d.cts CHANGED
@@ -2,7 +2,6 @@ import { Plugin } from 'vite';
2
2
  export { Plugin as OPSPlugin } from 'vite';
3
3
 
4
4
  type SplitStrategy = 'aggressive' | 'balanced' | 'conservative';
5
-
6
5
  type OPSOptions = {
7
6
  /**
8
7
  * If true, overwrite existing `build.rollupOptions.output.*` fields.
@@ -31,6 +30,19 @@ type OPSOptions = {
31
30
  */
32
31
  groups?: Record<string, (string | RegExp)[]>;
33
32
  };
33
+ type ResolvedOptions = Required<Pick<OPSOptions, 'override' | 'strategy' | 'minSize'>> & {
34
+ groups?: OPSOptions['groups'];
35
+ };
36
+ type GroupMatcher = {
37
+ name: string;
38
+ test: (id: string) => boolean;
39
+ priority: number;
40
+ };
41
+
42
+ /**
43
+ * OPS - Optimized Packaging Strategy
44
+ * 一个智能的 Vite 分包优化插件
45
+ */
34
46
  declare function OPS(opts?: OPSOptions): Plugin;
35
47
 
36
- export { type OPSOptions, type SplitStrategy, OPS as default };
48
+ export { type GroupMatcher, type OPSOptions, type ResolvedOptions, type SplitStrategy, OPS as default };
package/dist/index.d.ts CHANGED
@@ -1,36 +1,9 @@
1
- import { Plugin } from 'vite';
2
- export { Plugin as OPSPlugin } from 'vite';
3
-
4
- type SplitStrategy = 'aggressive' | 'balanced' | 'conservative';
5
-
6
- type OPSOptions = {
7
- /**
8
- * If true, overwrite existing `build.rollupOptions.output.*` fields.
9
- * If false, only fill in fields that are not already provided by the user.
10
- * Default: false
11
- */
12
- override?: boolean;
13
- /**
14
- * Chunking strategy:
15
- * - 'aggressive': Split almost all dependencies into separate chunks
16
- * - 'balanced': Split large dependencies and common frameworks (default)
17
- * - 'conservative': Minimal splitting, only very large dependencies
18
- * Default: 'balanced'
19
- */
20
- strategy?: SplitStrategy;
21
- /**
22
- * Minimum size (in KB) for a dependency to be split into its own chunk.
23
- * Only applies when strategy is 'balanced' or 'conservative'.
24
- * Default: 50
25
- */
26
- minSize?: number;
27
- /**
28
- * Additional custom chunk groups. Keys are chunk names; values are string or RegExp
29
- * matchers to detect a module path in node_modules. Example:
30
- * { three: ['three'], lodash: [/node_modules\\/lodash(?!-)/] }
31
- */
32
- groups?: Record<string, (string | RegExp)[]>;
33
- };
34
- declare function OPS(opts?: OPSOptions): Plugin;
35
-
36
- export { type OPSOptions, type SplitStrategy, OPS as default };
1
+ import type { Plugin } from 'vite';
2
+ import type { OPSOptions } from './types';
3
+ export type { OPSPlugin, OPSOptions, SplitStrategy, ResolvedOptions, GroupMatcher } from './types';
4
+ /**
5
+ * OPS - Optimized Packaging Strategy
6
+ * 一个智能的 Vite 分包优化插件
7
+ */
8
+ export default function OPS(opts?: OPSOptions): Plugin;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,MAAM,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAA2E,MAAM,SAAS,CAAA;AAIlH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAUlG;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAAC,IAAI,GAAE,UAAe,GAAG,MAAM,CAgHzD"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
 
4
- // src/index.ts
4
+ // src/deps.ts
5
+
6
+ // src/presets.ts
5
7
  var COMMON_LARGE_LIBS = {
6
8
  // UI Frameworks
7
9
  react: ["react", "react-dom"],
@@ -44,16 +46,46 @@ var MEDIUM_LIB_GROUPS = {
44
46
  "form": ["react-hook-form", "formik", "async-validator"],
45
47
  "i18n": ["i18next", "react-i18next", "vue-i18n"]
46
48
  };
47
- var patternCache = /* @__PURE__ */ new Map();
49
+ var VERY_LARGE_LIBS = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
50
+
51
+ // src/matcher.ts
52
+ var PRIORITIES = {
53
+ CUSTOM: 100,
54
+ // 用户自定义分组
55
+ DETECTED: 90,
56
+ // 插件检测到的框架
57
+ LARGE_LIB: 80,
58
+ // 大型库
59
+ MEDIUM_LIB: 70,
60
+ // 中型库
61
+ ALL_DEPS: 50
62
+ // 所有依赖(aggressive 模式)
63
+ };
48
64
  var MAX_PATH_LENGTH = 2e3;
65
+ var MAX_CACHE_SIZE = 100;
66
+ var patternCache = /* @__PURE__ */ new Map();
67
+ var cacheKeys = [];
68
+ var logger = {
69
+ warn: (msg) => console.warn(`[vite-plugin-ops] ${msg}`),
70
+ error: (msg) => console.error(`[vite-plugin-ops] ERROR: ${msg}`),
71
+ debug: (msg) => process.env.DEBUG && console.log(`[vite-plugin-ops] DEBUG: ${msg}`)
72
+ };
49
73
  function normalizeId(id) {
50
74
  return id.replace(/\\/g, "/").replace(/%5C/g, "/");
51
75
  }
76
+ function manageCache() {
77
+ while (cacheKeys.length >= MAX_CACHE_SIZE) {
78
+ const oldestKey = cacheKeys.shift();
79
+ if (oldestKey) {
80
+ patternCache.delete(oldestKey);
81
+ }
82
+ }
83
+ }
52
84
  function makeNodeModulesPattern(pkg) {
53
85
  if (pkg instanceof RegExp) {
54
86
  return (id) => {
55
87
  if (id.length > MAX_PATH_LENGTH) {
56
- console.warn(`[vite-plugin-ops] Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
88
+ logger.warn(`Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
57
89
  return false;
58
90
  }
59
91
  return pkg.test(id);
@@ -61,55 +93,27 @@ function makeNodeModulesPattern(pkg) {
61
93
  }
62
94
  let re = patternCache.get(pkg);
63
95
  if (!re) {
96
+ manageCache();
64
97
  const scoped = pkg.startsWith("@");
65
98
  const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
66
99
  const base = scoped ? escaped : `(?:@[^/]+/)?${escaped}`;
67
- re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, "i");
100
+ const flags = process.platform === "win32" ? "i" : "";
101
+ re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, flags);
68
102
  patternCache.set(pkg, re);
103
+ cacheKeys.push(pkg);
69
104
  }
70
105
  return (id) => {
71
106
  if (id.length > MAX_PATH_LENGTH) {
72
- console.warn(`[vite-plugin-ops] Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
107
+ logger.warn(`Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
73
108
  return false;
74
109
  }
75
110
  return re.test(id);
76
111
  };
77
112
  }
78
- function isValidPackageJson(obj) {
79
- if (typeof obj !== "object" || obj === null) return false;
80
- const json = obj;
81
- if (json.dependencies !== void 0) {
82
- if (typeof json.dependencies !== "object" || json.dependencies === null) return false;
83
- for (const val of Object.values(json.dependencies)) {
84
- if (typeof val !== "string") return false;
85
- }
86
- }
87
- return true;
88
- }
89
- function readProjectDependencies(cwd) {
90
- try {
91
- const pkgPath = path.join(cwd, "package.json");
92
- const content = fs.readFileSync(pkgPath, "utf8");
93
- const json = JSON.parse(content);
94
- if (!isValidPackageJson(json)) {
95
- console.warn("[vite-plugin-ops] Invalid package.json format: dependencies field is malformed");
96
- return /* @__PURE__ */ new Set();
97
- }
98
- return new Set(Object.keys(json.dependencies || {}));
99
- } catch (error) {
100
- if (error instanceof SyntaxError) {
101
- console.warn("[vite-plugin-ops] Failed to parse package.json: Invalid JSON format");
102
- } else if (error.code === "ENOENT") {
103
- console.warn("[vite-plugin-ops] package.json not found in:", cwd);
104
- } else if (process.env.DEBUG) {
105
- console.warn("[vite-plugin-ops] Failed to read package.json:", error);
106
- }
107
- return /* @__PURE__ */ new Set();
108
- }
109
- }
110
113
  function buildGroupMatchers(options, projectDeps, pluginHints) {
111
114
  const matchers = [];
112
- const strategy = options.strategy || "balanced";
115
+ const strategy = options.strategy;
116
+ const depsArray = Array.from(projectDeps);
113
117
  if (options.groups) {
114
118
  for (const [name, patterns] of Object.entries(options.groups)) {
115
119
  if (patterns && patterns.length) {
@@ -122,7 +126,7 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
122
126
  matchers.push({
123
127
  name,
124
128
  test: (id) => testers.some((t) => t(id)),
125
- priority: 100
129
+ priority: PRIORITIES.CUSTOM
126
130
  });
127
131
  }
128
132
  }
@@ -139,63 +143,49 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
139
143
  matchers.push({
140
144
  name,
141
145
  test: (id) => testers.some((t) => t(id)),
142
- priority: 90
146
+ priority: PRIORITIES.DETECTED
143
147
  });
144
148
  }
145
149
  if (strategy === "aggressive") {
146
- for (const dep of projectDeps) {
150
+ for (const dep of depsArray) {
147
151
  if (!dep.startsWith("@types/")) {
148
152
  matchers.push({
149
153
  name: dep,
150
154
  test: makeNodeModulesPattern(dep),
151
- priority: 50
155
+ priority: PRIORITIES.ALL_DEPS
152
156
  });
153
157
  }
154
158
  }
155
159
  } else if (strategy === "balanced") {
156
160
  for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
157
- const hasAny = patterns.some((p) => {
158
- const pkgName = p.replace(/\//g, "");
159
- return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
160
- });
161
- if (hasAny) {
161
+ if (hasDependencyMatch(patterns, depsArray)) {
162
162
  const testers = patterns.map(makeNodeModulesPattern);
163
163
  matchers.push({
164
164
  name: groupName,
165
165
  test: (id) => testers.some((t) => t(id)),
166
- priority: 80
166
+ priority: PRIORITIES.LARGE_LIB
167
167
  });
168
168
  }
169
169
  }
170
170
  for (const [groupName, patterns] of Object.entries(MEDIUM_LIB_GROUPS)) {
171
- const hasAny = patterns.some((p) => {
172
- return Array.from(projectDeps).some(
173
- (dep) => dep.includes(p.replace(/\//g, "").replace(/\*/g, ""))
174
- );
175
- });
176
- if (hasAny) {
171
+ if (hasDependencyMatch(patterns, depsArray)) {
177
172
  const testers = patterns.map(makeNodeModulesPattern);
178
173
  matchers.push({
179
174
  name: groupName,
180
175
  test: (id) => testers.some((t) => t(id)),
181
- priority: 70
176
+ priority: PRIORITIES.MEDIUM_LIB
182
177
  });
183
178
  }
184
179
  }
185
180
  } else if (strategy === "conservative") {
186
- const veryLargeLibs = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
187
181
  for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
188
- if (veryLargeLibs.includes(groupName)) {
189
- const hasAny = patterns.some((p) => {
190
- const pkgName = p.replace(/\//g, "");
191
- return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
192
- });
193
- if (hasAny) {
182
+ if (VERY_LARGE_LIBS.includes(groupName)) {
183
+ if (hasDependencyMatch(patterns, depsArray)) {
194
184
  const testers = patterns.map(makeNodeModulesPattern);
195
185
  matchers.push({
196
186
  name: groupName,
197
187
  test: (id) => testers.some((t) => t(id)),
198
- priority: 80
188
+ priority: PRIORITIES.LARGE_LIB
199
189
  });
200
190
  }
201
191
  }
@@ -203,6 +193,61 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
203
193
  }
204
194
  return matchers.sort((a, b) => b.priority - a.priority);
205
195
  }
196
+
197
+ // src/deps.ts
198
+ var MAX_PACKAGE_JSON_SIZE = 1024 * 1024;
199
+ function isValidPackageJson(obj) {
200
+ if (typeof obj !== "object" || obj === null) return false;
201
+ const json = obj;
202
+ if (json.dependencies !== void 0) {
203
+ if (typeof json.dependencies !== "object" || json.dependencies === null) return false;
204
+ for (const val of Object.values(json.dependencies)) {
205
+ if (typeof val !== "string") return false;
206
+ }
207
+ }
208
+ return true;
209
+ }
210
+ function readProjectDependencies(cwd) {
211
+ try {
212
+ const pkgPath = path.join(cwd, "package.json");
213
+ const stats = fs.statSync(pkgPath);
214
+ if (stats.size > MAX_PACKAGE_JSON_SIZE) {
215
+ logger.warn(`package.json too large (${stats.size} bytes), skipping`);
216
+ return /* @__PURE__ */ new Set();
217
+ }
218
+ const content = fs.readFileSync(pkgPath, "utf8");
219
+ const json = JSON.parse(content);
220
+ if (!isValidPackageJson(json)) {
221
+ logger.warn("Invalid package.json format: dependencies field is malformed");
222
+ return /* @__PURE__ */ new Set();
223
+ }
224
+ return new Set(Object.keys(json.dependencies || {}));
225
+ } catch (error) {
226
+ if (error instanceof SyntaxError) {
227
+ logger.warn("Failed to parse package.json: Invalid JSON format");
228
+ } else if (error.code === "ENOENT") {
229
+ logger.warn(`package.json not found in: ${cwd}`);
230
+ } else if (process.env.DEBUG) {
231
+ logger.debug(`Failed to read package.json: ${error}`);
232
+ }
233
+ return /* @__PURE__ */ new Set();
234
+ }
235
+ }
236
+ function hasDependencyMatch(patterns, depsArray) {
237
+ const depsSet = new Set(depsArray);
238
+ return patterns.some((pattern) => {
239
+ if (pattern.includes("/")) {
240
+ return depsArray.some((dep) => dep.startsWith(pattern));
241
+ }
242
+ return depsSet.has(pattern) || depsArray.some((dep) => {
243
+ if (dep === pattern) return true;
244
+ if (dep.startsWith(`@${pattern}/`)) return true;
245
+ return false;
246
+ });
247
+ });
248
+ }
249
+
250
+ // src/index.ts
206
251
  function OPS(opts = {}) {
207
252
  const options = {
208
253
  override: opts.override ?? false,
@@ -212,22 +257,32 @@ function OPS(opts = {}) {
212
257
  };
213
258
  let groupsRef = [];
214
259
  const manualChunks = (id) => {
215
- const nid = normalizeId(id);
216
- if (!/\/node_modules\//.test(nid)) return void 0;
217
- for (const g of groupsRef) {
218
- if (g.test(nid)) return g.name;
260
+ try {
261
+ const nid = normalizeId(id);
262
+ if (!/\/node_modules\//.test(nid)) return void 0;
263
+ for (const g of groupsRef) {
264
+ if (g.test(nid)) return g.name;
265
+ }
266
+ return "vendor";
267
+ } catch (error) {
268
+ logger.warn(`Error in manualChunks for ${id}: ${error}`);
269
+ return "vendor";
219
270
  }
220
- return "vendor";
221
271
  };
222
272
  const assetFileNamesFn = (assetInfo) => {
223
- const name = assetInfo.name ?? "";
224
- const ext = name.split(".").pop()?.toLowerCase();
225
- if (ext === "css") return "css/[name]-[hash][extname]";
226
- if (["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"].includes(ext ?? ""))
227
- return "img/[name]-[hash][extname]";
228
- if (["woff", "woff2", "eot", "ttf", "otf"].includes(ext ?? ""))
229
- return "fonts/[name]-[hash][extname]";
230
- return "assets/[name]-[hash][extname]";
273
+ try {
274
+ const name = assetInfo.name ?? "";
275
+ const ext = name.split(".").pop()?.toLowerCase();
276
+ if (ext === "css") return "css/[name]-[hash][extname]";
277
+ if (["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"].includes(ext ?? ""))
278
+ return "img/[name]-[hash][extname]";
279
+ if (["woff", "woff2", "eot", "ttf", "otf"].includes(ext ?? ""))
280
+ return "fonts/[name]-[hash][extname]";
281
+ return "assets/[name]-[hash][extname]";
282
+ } catch (error) {
283
+ logger.warn(`Error in assetFileNames: ${error}`);
284
+ return "assets/[name]-[hash][extname]";
285
+ }
231
286
  };
232
287
  return {
233
288
  name: "vite-plugin-ops",
@@ -243,14 +298,14 @@ function OPS(opts = {}) {
243
298
  manualChunks
244
299
  };
245
300
  let output;
246
- if (shouldMerge) {
247
- const base = existingOutput;
248
- const merged = { ...base };
249
- if (!("entryFileNames" in merged)) merged["entryFileNames"] = injected.entryFileNames;
250
- if (!("chunkFileNames" in merged)) merged["chunkFileNames"] = injected.chunkFileNames;
251
- if (!("assetFileNames" in merged)) merged["assetFileNames"] = injected.assetFileNames;
252
- if (!("manualChunks" in merged)) merged["manualChunks"] = injected.manualChunks;
253
- output = merged;
301
+ if (shouldMerge && existingOutput) {
302
+ const existing = existingOutput;
303
+ output = {
304
+ entryFileNames: existing?.entryFileNames ?? injected.entryFileNames,
305
+ chunkFileNames: existing?.chunkFileNames ?? injected.chunkFileNames,
306
+ assetFileNames: existing?.assetFileNames ?? injected.assetFileNames,
307
+ manualChunks: existing?.manualChunks ?? injected.manualChunks
308
+ };
254
309
  } else {
255
310
  output = injected;
256
311
  }
@@ -0,0 +1,43 @@
1
+ import type { GroupMatcher, ResolvedOptions } from './types';
2
+ /**
3
+ * 优先级常量:确保分组匹配的优先级顺序
4
+ */
5
+ export declare const PRIORITIES: {
6
+ readonly CUSTOM: 100;
7
+ readonly DETECTED: 90;
8
+ readonly LARGE_LIB: 80;
9
+ readonly MEDIUM_LIB: 70;
10
+ readonly ALL_DEPS: 50;
11
+ };
12
+ /**
13
+ * 统一的日志系统
14
+ */
15
+ declare const logger: {
16
+ warn: (msg: string) => void;
17
+ error: (msg: string) => void;
18
+ debug: (msg: string) => void | "" | undefined;
19
+ };
20
+ /**
21
+ * 路径标准化:统一使用正斜杠
22
+ */
23
+ export declare function normalizeId(id: string): string;
24
+ /**
25
+ * 创建 node_modules 匹配函数
26
+ * @param pkg 包名或正则表达式
27
+ * @returns 匹配函数
28
+ */
29
+ export declare function makeNodeModulesPattern(pkg: string | RegExp): (id: string) => boolean;
30
+ /**
31
+ * 构建分组匹配器
32
+ * @param options 插件选项
33
+ * @param projectDeps 项目依赖集合
34
+ * @param pluginHints 插件检测提示
35
+ * @returns 排序后的匹配器数组
36
+ */
37
+ export declare function buildGroupMatchers(options: Pick<ResolvedOptions, 'groups' | 'strategy' | 'minSize'>, projectDeps: Set<string>, pluginHints: Set<string>): GroupMatcher[];
38
+ /**
39
+ * 清除缓存(用于测试)
40
+ */
41
+ export declare function clearCache(): void;
42
+ export { logger };
43
+ //# sourceMappingURL=matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAM5D;;GAEG;AACH,eAAO,MAAM,UAAU;;;;;;CAMb,CAAA;AAgBV;;GAEG;AACH,QAAA,MAAM,MAAM;gBACE,MAAM;iBACL,MAAM;iBACN,MAAM;CACpB,CAAA;AAID;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE9C;AAcD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAuCpF;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,EACjE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GACvB,YAAY,EAAE,CAmGhB;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAGjC;AAGD,OAAO,EAAE,MAAM,EAAE,CAAA"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 预设配置:大型库分组
3
+ * Common large libraries that should typically be split
4
+ */
5
+ export declare const COMMON_LARGE_LIBS: Record<string, string[]>;
6
+ /**
7
+ * 预设配置:中型库分组
8
+ * Medium-sized libraries that should be grouped together
9
+ */
10
+ export declare const MEDIUM_LIB_GROUPS: Record<string, string[]>;
11
+ /**
12
+ * 保守策略下只分离的超大型库
13
+ */
14
+ export declare const VERY_LARGE_LIBS: readonly ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
15
+ //# sourceMappingURL=presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAyCtD,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAKtD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,eAAe,kFAAmF,CAAA"}
@@ -0,0 +1,43 @@
1
+ import type { Plugin, UserConfig } from 'vite';
2
+ export type SplitStrategy = 'aggressive' | 'balanced' | 'conservative';
3
+ export type OPSOptions = {
4
+ /**
5
+ * If true, overwrite existing `build.rollupOptions.output.*` fields.
6
+ * If false, only fill in fields that are not already provided by the user.
7
+ * Default: false
8
+ */
9
+ override?: boolean;
10
+ /**
11
+ * Chunking strategy:
12
+ * - 'aggressive': Split almost all dependencies into separate chunks
13
+ * - 'balanced': Split large dependencies and common frameworks (default)
14
+ * - 'conservative': Minimal splitting, only very large dependencies
15
+ * Default: 'balanced'
16
+ */
17
+ strategy?: SplitStrategy;
18
+ /**
19
+ * Minimum size (in KB) for a dependency to be split into its own chunk.
20
+ * Only applies when strategy is 'balanced' or 'conservative'.
21
+ * Default: 50
22
+ */
23
+ minSize?: number;
24
+ /**
25
+ * Additional custom chunk groups. Keys are chunk names; values are string or RegExp
26
+ * matchers to detect a module path in node_modules. Example:
27
+ * { three: ['three'], lodash: [/node_modules\\/lodash(?!-)/] }
28
+ */
29
+ groups?: Record<string, (string | RegExp)[]>;
30
+ };
31
+ export type ResolvedOptions = Required<Pick<OPSOptions, 'override' | 'strategy' | 'minSize'>> & {
32
+ groups?: OPSOptions['groups'];
33
+ };
34
+ export type GroupMatcher = {
35
+ name: string;
36
+ test: (id: string) => boolean;
37
+ priority: number;
38
+ };
39
+ type OutputOptions = NonNullable<NonNullable<UserConfig['build']>['rollupOptions']>['output'] extends infer O ? O extends any[] ? O[number] : O : never;
40
+ export type ManualChunksOption = NonNullable<OutputOptions>['manualChunks'];
41
+ export type AssetFileNamesOption = NonNullable<OutputOptions>['assetFileNames'];
42
+ export type { Plugin as OPSPlugin };
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAE9C,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,UAAU,GAAG,cAAc,CAAA;AAEtE,MAAM,MAAM,UAAU,GAAG;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAA;IACxB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;CAC7C,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC,CAAC,GAAG;IAC9F,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;IAC7B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAGD,KAAK,aAAa,GAChB,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,MAAM,CAAC,GACtF,CAAC,SAAS,GAAG,EAAE,GACf,CAAC,CAAC,MAAM,CAAC,GACT,CAAC,GACD,KAAK,CAAA;AAET,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAAC,aAAa,CAAC,CAAC,cAAc,CAAC,CAAA;AAC3E,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAAC,aAAa,CAAC,CAAC,gBAAgB,CAAC,CAAA;AAG/E,YAAY,EAAE,MAAM,IAAI,SAAS,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "vite-plugin-ops",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "Vite plugin to organize build outputs and vendor chunking.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "suileyan",
8
-
9
8
  "exports": {
10
9
  ".": {
11
10
  "types": "./dist/index.d.ts",
@@ -16,39 +15,53 @@
16
15
  },
17
16
  "main": "dist/index.cjs",
18
17
  "types": "dist/index.d.ts",
19
-
20
- "files": ["dist", "README.md", "LICENSE"],
21
-
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
22
23
  "sideEffects": false,
23
24
  "engines": {
24
25
  "node": ">=20.19"
25
26
  },
26
-
27
- "keywords": ["vite", "vite-plugin", "rollup", "chunks", "vendor", "build"],
28
-
29
- "repository": { "type": "git", "url": "git+https://github.com/suileyan/vite-plugin-ops.git" },
30
- "bugs": { "url": "https://github.com/suileyan/vite-plugin-ops/issues" },
27
+ "keywords": [
28
+ "vite",
29
+ "vite-plugin",
30
+ "rollup",
31
+ "chunks",
32
+ "vendor",
33
+ "build"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/suileyan/vite-plugin-ops.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/suileyan/vite-plugin-ops/issues"
41
+ },
31
42
  "homepage": "https://github.com/suileyan/vite-plugin-ops#readme",
32
-
33
43
  "peerDependencies": {
34
44
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
35
45
  },
36
46
  "devDependencies": {
37
47
  "@types/node": "^24.6.0",
38
48
  "rimraf": "^6.0.1",
49
+ "rollup": "^4.0.0",
39
50
  "tsup": "^8.5.0",
40
51
  "typescript": "^5.9.2",
41
52
  "vite": "^7.3.1",
42
- "rollup": "^4.0.0"
53
+ "vitest": "^3.0.0"
43
54
  },
44
-
45
55
  "scripts": {
46
- "build": "tsup",
56
+ "build": "tsup && tsc --emitDeclarationOnly --declaration --outDir dist",
57
+ "build:js": "tsup",
47
58
  "dev": "tsup --watch --sourcemap",
48
59
  "clean": "rimraf dist || rmdir /s /q dist",
49
- "prepublishOnly": "npm run clean && npm run build"
60
+ "prepublishOnly": "npm run clean && npm run build",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "test:coverage": "vitest run --coverage"
50
64
  },
51
-
52
65
  "publishConfig": {
53
66
  "access": "public",
54
67
  "registry": "https://registry.npmjs.org/"