vite-plugin-ops 0.1.1 → 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,31 +53,74 @@ var MEDIUM_LIB_GROUPS = {
51
53
  "form": ["react-hook-form", "formik", "async-validator"],
52
54
  "i18n": ["i18next", "react-i18next", "vue-i18n"]
53
55
  };
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
+ };
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
+ };
54
80
  function normalizeId(id) {
55
81
  return id.replace(/\\/g, "/").replace(/%5C/g, "/");
56
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
+ }
57
91
  function makeNodeModulesPattern(pkg) {
58
92
  if (pkg instanceof RegExp) {
59
- return (id) => pkg.test(id);
93
+ return (id) => {
94
+ if (id.length > MAX_PATH_LENGTH) {
95
+ logger.warn(`Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
96
+ return false;
97
+ }
98
+ return pkg.test(id);
99
+ };
60
100
  }
61
- const scoped = pkg.startsWith("@");
62
- const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
- const base = scoped ? escaped : `(?:@[^/]+/)?${escaped}`;
64
- const re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, "i");
65
- return (id) => re.test(id);
66
- }
67
- function readProjectDependencies(cwd) {
68
- try {
69
- const pkgPath = path__default.default.join(cwd, "package.json");
70
- const json = JSON.parse(fs__default.default.readFileSync(pkgPath, "utf8"));
71
- return new Set(Object.keys(json.dependencies || {}));
72
- } catch {
73
- return /* @__PURE__ */ new Set();
101
+ let re = patternCache.get(pkg);
102
+ if (!re) {
103
+ manageCache();
104
+ const scoped = pkg.startsWith("@");
105
+ const escaped = pkg.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
106
+ const base = scoped ? escaped : `(?:@[^/]+/)?${escaped}`;
107
+ const flags = process.platform === "win32" ? "i" : "";
108
+ re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, flags);
109
+ patternCache.set(pkg, re);
110
+ cacheKeys.push(pkg);
74
111
  }
112
+ return (id) => {
113
+ if (id.length > MAX_PATH_LENGTH) {
114
+ logger.warn(`Path too long (${id.length} chars), skipping: ${id.slice(0, 50)}...`);
115
+ return false;
116
+ }
117
+ return re.test(id);
118
+ };
75
119
  }
76
120
  function buildGroupMatchers(options, projectDeps, pluginHints) {
77
121
  const matchers = [];
78
- const strategy = options.strategy || "balanced";
122
+ const strategy = options.strategy;
123
+ const depsArray = Array.from(projectDeps);
79
124
  if (options.groups) {
80
125
  for (const [name, patterns] of Object.entries(options.groups)) {
81
126
  if (patterns && patterns.length) {
@@ -88,7 +133,7 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
88
133
  matchers.push({
89
134
  name,
90
135
  test: (id) => testers.some((t) => t(id)),
91
- priority: 100
136
+ priority: PRIORITIES.CUSTOM
92
137
  });
93
138
  }
94
139
  }
@@ -105,63 +150,49 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
105
150
  matchers.push({
106
151
  name,
107
152
  test: (id) => testers.some((t) => t(id)),
108
- priority: 90
153
+ priority: PRIORITIES.DETECTED
109
154
  });
110
155
  }
111
156
  if (strategy === "aggressive") {
112
- for (const dep of projectDeps) {
157
+ for (const dep of depsArray) {
113
158
  if (!dep.startsWith("@types/")) {
114
159
  matchers.push({
115
160
  name: dep,
116
161
  test: makeNodeModulesPattern(dep),
117
- priority: 50
162
+ priority: PRIORITIES.ALL_DEPS
118
163
  });
119
164
  }
120
165
  }
121
166
  } else if (strategy === "balanced") {
122
167
  for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
123
- const hasAny = patterns.some((p) => {
124
- const pkgName = p.replace(/\//g, "");
125
- return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
126
- });
127
- if (hasAny) {
168
+ if (hasDependencyMatch(patterns, depsArray)) {
128
169
  const testers = patterns.map(makeNodeModulesPattern);
129
170
  matchers.push({
130
171
  name: groupName,
131
172
  test: (id) => testers.some((t) => t(id)),
132
- priority: 80
173
+ priority: PRIORITIES.LARGE_LIB
133
174
  });
134
175
  }
135
176
  }
136
177
  for (const [groupName, patterns] of Object.entries(MEDIUM_LIB_GROUPS)) {
137
- const hasAny = patterns.some((p) => {
138
- return Array.from(projectDeps).some(
139
- (dep) => dep.includes(p.replace(/\//g, "").replace(/\*/g, ""))
140
- );
141
- });
142
- if (hasAny) {
178
+ if (hasDependencyMatch(patterns, depsArray)) {
143
179
  const testers = patterns.map(makeNodeModulesPattern);
144
180
  matchers.push({
145
181
  name: groupName,
146
182
  test: (id) => testers.some((t) => t(id)),
147
- priority: 70
183
+ priority: PRIORITIES.MEDIUM_LIB
148
184
  });
149
185
  }
150
186
  }
151
187
  } else if (strategy === "conservative") {
152
- const veryLargeLibs = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
153
188
  for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
154
- if (veryLargeLibs.includes(groupName)) {
155
- const hasAny = patterns.some((p) => {
156
- const pkgName = p.replace(/\//g, "");
157
- return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
158
- });
159
- if (hasAny) {
189
+ if (VERY_LARGE_LIBS.includes(groupName)) {
190
+ if (hasDependencyMatch(patterns, depsArray)) {
160
191
  const testers = patterns.map(makeNodeModulesPattern);
161
192
  matchers.push({
162
193
  name: groupName,
163
194
  test: (id) => testers.some((t) => t(id)),
164
- priority: 80
195
+ priority: PRIORITIES.LARGE_LIB
165
196
  });
166
197
  }
167
198
  }
@@ -169,6 +200,61 @@ function buildGroupMatchers(options, projectDeps, pluginHints) {
169
200
  }
170
201
  return matchers.sort((a, b) => b.priority - a.priority);
171
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
172
258
  function OPS(opts = {}) {
173
259
  const options = {
174
260
  override: opts.override ?? false,
@@ -178,22 +264,32 @@ function OPS(opts = {}) {
178
264
  };
179
265
  let groupsRef = [];
180
266
  const manualChunks = (id) => {
181
- const nid = normalizeId(id);
182
- if (!/\/node_modules\//.test(nid)) return void 0;
183
- for (const g of groupsRef) {
184
- 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";
185
277
  }
186
- return "vendor";
187
278
  };
188
279
  const assetFileNamesFn = (assetInfo) => {
189
- const name = assetInfo.name ?? "";
190
- const ext = name.split(".").pop()?.toLowerCase();
191
- if (ext === "css") return "css/[name]-[hash][extname]";
192
- if (["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"].includes(ext ?? ""))
193
- return "img/[name]-[hash][extname]";
194
- if (["woff", "woff2", "eot", "ttf", "otf"].includes(ext ?? ""))
195
- return "fonts/[name]-[hash][extname]";
196
- 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
+ }
197
293
  };
198
294
  return {
199
295
  name: "vite-plugin-ops",
@@ -209,14 +305,14 @@ function OPS(opts = {}) {
209
305
  manualChunks
210
306
  };
211
307
  let output;
212
- if (shouldMerge) {
213
- const base = existingOutput;
214
- const merged = { ...base };
215
- if (!("entryFileNames" in merged)) merged["entryFileNames"] = injected.entryFileNames;
216
- if (!("chunkFileNames" in merged)) merged["chunkFileNames"] = injected.chunkFileNames;
217
- if (!("assetFileNames" in merged)) merged["assetFileNames"] = injected.assetFileNames;
218
- if (!("manualChunks" in merged)) merged["manualChunks"] = injected.manualChunks;
219
- 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
+ };
220
316
  } else {
221
317
  output = injected;
222
318
  }
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from 'vite';
2
+ export { Plugin as OPSPlugin } from 'vite';
2
3
 
3
4
  type SplitStrategy = 'aggressive' | 'balanced' | 'conservative';
4
5
  type OPSOptions = {
@@ -29,6 +30,19 @@ type OPSOptions = {
29
30
  */
30
31
  groups?: Record<string, (string | RegExp)[]>;
31
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
+ */
32
46
  declare function OPS(opts?: OPSOptions): Plugin;
33
47
 
34
- 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,34 +1,9 @@
1
- import { Plugin } from 'vite';
2
-
3
- type SplitStrategy = 'aggressive' | 'balanced' | 'conservative';
4
- type OPSOptions = {
5
- /**
6
- * If true, overwrite existing `build.rollupOptions.output.*` fields.
7
- * If false, only fill in fields that are not already provided by the user.
8
- * Default: false
9
- */
10
- override?: boolean;
11
- /**
12
- * Chunking strategy:
13
- * - 'aggressive': Split almost all dependencies into separate chunks
14
- * - 'balanced': Split large dependencies and common frameworks (default)
15
- * - 'conservative': Minimal splitting, only very large dependencies
16
- * Default: 'balanced'
17
- */
18
- strategy?: SplitStrategy;
19
- /**
20
- * Minimum size (in KB) for a dependency to be split into its own chunk.
21
- * Only applies when strategy is 'balanced' or 'conservative'.
22
- * Default: 50
23
- */
24
- minSize?: number;
25
- /**
26
- * Additional custom chunk groups. Keys are chunk names; values are string or RegExp
27
- * matchers to detect a module path in node_modules. Example:
28
- * { three: ['three'], lodash: [/node_modules\\/lodash(?!-)/] }
29
- */
30
- groups?: Record<string, (string | RegExp)[]>;
31
- };
32
- declare function OPS(opts?: OPSOptions): Plugin;
33
-
34
- 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"}