vite-plugin-ops 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) suileyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # vite-plugin-ops
2
+
3
+ Vite 插件:规范打包产物命名,并按第三方依赖进行分包(支持自定义与自动回退)。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm i -D vite-plugin-ops
9
+ # 或 pnpm add -D vite-plugin-ops
10
+ # 或 yarn add -D vite-plugin-ops
11
+ ```
12
+
13
+ ## 使用
14
+
15
+ ```ts
16
+ // vite.config.ts
17
+ import { defineConfig } from 'vite'
18
+ import OPS from 'vite-plugin-ops'
19
+
20
+ export default defineConfig({
21
+ plugins: [
22
+ OPS({
23
+ // 是否覆盖现有的 rollup output 配置(对象形态)
24
+ override: false,
25
+ // 自定义分组(可选):字符串或正则匹配 node_modules 路径
26
+ // groups: {
27
+ // lodash: ['lodash'],
28
+ // react: ['react', 'react-dom'],
29
+ // lodashCore: [/node_modules\/(?:\.pnpm\/)?lodash(?=\/|@|$)/],
30
+ // },
31
+ }),
32
+ ],
33
+ })
34
+ ```
35
+
36
+ ## 行为说明
37
+
38
+ - 输出命名规范(注入到 rollupOptions.output):
39
+ - `entryFileNames: js/[name]-[hash].js`
40
+ - `chunkFileNames: js/[name]-[hash].js`
41
+ - `assetFileNames`: 按类型放入 `css/`、`img/`、`fonts/`、`assets/`
42
+ - 分包策略:
43
+ - 若提供 `groups`,按自定义规则将 `node_modules` 模块归入对应分组;
44
+ - 若未提供或传入空分组,自动读取 `package.json` 中 `dependencies`,对所有不以 `@` 开头的依赖生成同名分组;
45
+ - 未匹配到的第三方依赖统一归入 `vendor`;
46
+ - 会结合解析到的 Vite 插件名做轻量提示(例如存在 `vite:vue`/`vite:vue-jsx` 时提示 `vue`,存在 `unplugin-vue-components` 时提示 `@vueuse`)。
47
+ - 合并策略:
48
+ - 当 `override=false` 且用户已有 `build.rollupOptions.output`(对象形态)时,仅为缺失项赋默认值;
49
+ - 若 `output` 为数组形态,则不做合并,直接提供插件的对象形态默认值。
50
+ - 路径处理:
51
+ - 仅对 `node_modules` 路径进行分组判断;同时对 Windows 路径分隔符做了标准化处理。
52
+
53
+ ## 配置
54
+
55
+ ```ts
56
+ export type OPSOptions = {
57
+ // 是否强制覆盖现有 output.* 配置,默认 false
58
+ override?: boolean
59
+ // 自定义分组:键为分组名,值为字符串或正则,匹配 node_modules 路径
60
+ groups?: Record<string, (string | RegExp)[]>
61
+ }
62
+ ```
63
+
64
+ 示例:
65
+
66
+ ```ts
67
+ OPS({
68
+ override: false,
69
+ groups: {
70
+ lodash: ['lodash'],
71
+ ui: ['element-plus', 'naive-ui'],
72
+ babel: [/\/@babel\//],
73
+ },
74
+ })
75
+ ```
76
+
77
+ ## 产物结构示例
78
+
79
+ - JS 主入口与公共 chunk:`js/[name]-[hash].js`
80
+ - CSS:`css/[name]-[hash][extname]`
81
+ - 图片:`img/[name]-[hash][extname]`
82
+ - 字体:`fonts/[name]-[hash][extname]`
83
+ - 其他静态资源:`assets/[name]-[hash][extname]`
84
+
85
+ ## 许可
86
+
87
+ 本项目使用 MIT 许可证,详见 `LICENSE`。
88
+
package/dist/index.cjs ADDED
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
9
+ var path__default = /*#__PURE__*/_interopDefault(path);
10
+
11
+ // src/index.ts
12
+ var COMMON_LARGE_LIBS = {
13
+ // UI Frameworks
14
+ react: ["react", "react-dom"],
15
+ vue: ["vue", "@vue/"],
16
+ angular: ["@angular/"],
17
+ svelte: ["svelte"],
18
+ // State Management
19
+ redux: ["redux", "react-redux", "@reduxjs/toolkit"],
20
+ mobx: ["mobx", "mobx-react"],
21
+ zustand: ["zustand"],
22
+ pinia: ["pinia"],
23
+ // Routing
24
+ "react-router": ["react-router", "react-router-dom"],
25
+ "vue-router": ["vue-router"],
26
+ // UI Libraries
27
+ antd: ["antd", "@ant-design/"],
28
+ "element-plus": ["element-plus"],
29
+ "element-ui": ["element-ui"],
30
+ "naive-ui": ["naive-ui"],
31
+ "arco-design": ["@arco-design/"],
32
+ "material-ui": ["@mui/", "@material-ui/"],
33
+ chakra: ["@chakra-ui/"],
34
+ // Utility Libraries
35
+ lodash: ["lodash", "lodash-es"],
36
+ moment: ["moment"],
37
+ dayjs: ["dayjs"],
38
+ axios: ["axios"],
39
+ // Rich Text / Charts
40
+ echarts: ["echarts"],
41
+ "d3": ["d3"],
42
+ "chart.js": ["chart.js"],
43
+ quill: ["quill"],
44
+ // 3D / Game
45
+ three: ["three"],
46
+ babylon: ["@babylonjs/"]
47
+ };
48
+ var MEDIUM_LIB_GROUPS = {
49
+ "utils": ["@vueuse/", "ahooks", "react-use"],
50
+ "icons": ["@iconify/", "@ant-design/icons", "@heroicons/", "lucide-react"],
51
+ "form": ["react-hook-form", "formik", "async-validator"],
52
+ "i18n": ["i18next", "react-i18next", "vue-i18n"]
53
+ };
54
+ function normalizeId(id) {
55
+ return id.replace(/\\/g, "/").replace(/%5C/g, "/");
56
+ }
57
+ function makeNodeModulesPattern(pkg) {
58
+ if (pkg instanceof RegExp) {
59
+ return (id) => pkg.test(id);
60
+ }
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();
74
+ }
75
+ }
76
+ function buildGroupMatchers(options, projectDeps, pluginHints) {
77
+ const matchers = [];
78
+ const strategy = options.strategy || "balanced";
79
+ if (options.groups) {
80
+ for (const [name, patterns] of Object.entries(options.groups)) {
81
+ if (patterns && patterns.length) {
82
+ const sorted = patterns.slice().sort((a, b) => {
83
+ const la = typeof a === "string" ? a.length : 0;
84
+ const lb = typeof b === "string" ? b.length : 0;
85
+ return lb - la;
86
+ });
87
+ const testers = sorted.map(makeNodeModulesPattern);
88
+ matchers.push({
89
+ name,
90
+ test: (id) => testers.some((t) => t(id)),
91
+ priority: 100
92
+ });
93
+ }
94
+ }
95
+ }
96
+ const detectedGroups = {};
97
+ if (pluginHints.has("vue")) {
98
+ detectedGroups["vue"] = ["vue", "@vue/"];
99
+ }
100
+ if (pluginHints.has("vueuse")) {
101
+ detectedGroups["vueuse"] = ["@vueuse/"];
102
+ }
103
+ for (const [name, patterns] of Object.entries(detectedGroups)) {
104
+ const testers = patterns.map(makeNodeModulesPattern);
105
+ matchers.push({
106
+ name,
107
+ test: (id) => testers.some((t) => t(id)),
108
+ priority: 90
109
+ });
110
+ }
111
+ if (strategy === "aggressive") {
112
+ for (const dep of projectDeps) {
113
+ if (!dep.startsWith("@types/")) {
114
+ matchers.push({
115
+ name: dep,
116
+ test: makeNodeModulesPattern(dep),
117
+ priority: 50
118
+ });
119
+ }
120
+ }
121
+ } else if (strategy === "balanced") {
122
+ 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) {
128
+ const testers = patterns.map(makeNodeModulesPattern);
129
+ matchers.push({
130
+ name: groupName,
131
+ test: (id) => testers.some((t) => t(id)),
132
+ priority: 80
133
+ });
134
+ }
135
+ }
136
+ 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) {
143
+ const testers = patterns.map(makeNodeModulesPattern);
144
+ matchers.push({
145
+ name: groupName,
146
+ test: (id) => testers.some((t) => t(id)),
147
+ priority: 70
148
+ });
149
+ }
150
+ }
151
+ } else if (strategy === "conservative") {
152
+ const veryLargeLibs = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
153
+ 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) {
160
+ const testers = patterns.map(makeNodeModulesPattern);
161
+ matchers.push({
162
+ name: groupName,
163
+ test: (id) => testers.some((t) => t(id)),
164
+ priority: 80
165
+ });
166
+ }
167
+ }
168
+ }
169
+ }
170
+ return matchers.sort((a, b) => b.priority - a.priority);
171
+ }
172
+ function OPS(opts = {}) {
173
+ const options = {
174
+ override: opts.override ?? false,
175
+ strategy: opts.strategy ?? "balanced",
176
+ minSize: opts.minSize ?? 50,
177
+ ...opts.groups ? { groups: opts.groups } : {}
178
+ };
179
+ let groupsRef = [];
180
+ 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;
185
+ }
186
+ return "vendor";
187
+ };
188
+ 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]";
197
+ };
198
+ return {
199
+ name: "vite-plugin-ops",
200
+ enforce: "post",
201
+ config(userConfig, _env) {
202
+ const existingOutput = userConfig.build?.rollupOptions?.output;
203
+ const outputIsArray = Array.isArray(existingOutput);
204
+ const shouldMerge = !options.override && existingOutput && !outputIsArray;
205
+ const injected = {
206
+ entryFileNames: "js/[name]-[hash].js",
207
+ chunkFileNames: "js/[name]-[hash].js",
208
+ assetFileNames: assetFileNamesFn,
209
+ manualChunks
210
+ };
211
+ 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;
220
+ } else {
221
+ output = injected;
222
+ }
223
+ return {
224
+ build: {
225
+ rollupOptions: {
226
+ output
227
+ }
228
+ }
229
+ };
230
+ },
231
+ configResolved(resolved) {
232
+ const cwd = resolved.root || process.cwd();
233
+ const projectDeps = readProjectDependencies(cwd);
234
+ const pluginNames = new Set(resolved.plugins.map((p) => p.name));
235
+ const hints = /* @__PURE__ */ new Set();
236
+ if (pluginNames.has("vite:vue") || pluginNames.has("vite:vue-jsx")) {
237
+ hints.add("vue");
238
+ }
239
+ if (pluginNames.has("unplugin-vue-components")) {
240
+ hints.add("vueuse");
241
+ }
242
+ groupsRef = buildGroupMatchers(options, projectDeps, hints);
243
+ if (resolved.command === "build") {
244
+ const strategyDesc = {
245
+ aggressive: "Aggressive (split most dependencies)",
246
+ balanced: "Balanced (split large libraries)",
247
+ conservative: "Conservative (minimal splitting)"
248
+ };
249
+ console.log(`
250
+ \u{1F4E6} OPS Chunking Strategy: ${strategyDesc[options.strategy]}`);
251
+ console.log(`\u{1F4CA} Detected ${groupsRef.length} chunk groups`);
252
+ }
253
+ }
254
+ };
255
+ }
256
+
257
+ module.exports = OPS;
@@ -0,0 +1,34 @@
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 };
@@ -0,0 +1,34 @@
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 };
package/dist/index.js ADDED
@@ -0,0 +1,250 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ // src/index.ts
5
+ var COMMON_LARGE_LIBS = {
6
+ // UI Frameworks
7
+ react: ["react", "react-dom"],
8
+ vue: ["vue", "@vue/"],
9
+ angular: ["@angular/"],
10
+ svelte: ["svelte"],
11
+ // State Management
12
+ redux: ["redux", "react-redux", "@reduxjs/toolkit"],
13
+ mobx: ["mobx", "mobx-react"],
14
+ zustand: ["zustand"],
15
+ pinia: ["pinia"],
16
+ // Routing
17
+ "react-router": ["react-router", "react-router-dom"],
18
+ "vue-router": ["vue-router"],
19
+ // UI Libraries
20
+ antd: ["antd", "@ant-design/"],
21
+ "element-plus": ["element-plus"],
22
+ "element-ui": ["element-ui"],
23
+ "naive-ui": ["naive-ui"],
24
+ "arco-design": ["@arco-design/"],
25
+ "material-ui": ["@mui/", "@material-ui/"],
26
+ chakra: ["@chakra-ui/"],
27
+ // Utility Libraries
28
+ lodash: ["lodash", "lodash-es"],
29
+ moment: ["moment"],
30
+ dayjs: ["dayjs"],
31
+ axios: ["axios"],
32
+ // Rich Text / Charts
33
+ echarts: ["echarts"],
34
+ "d3": ["d3"],
35
+ "chart.js": ["chart.js"],
36
+ quill: ["quill"],
37
+ // 3D / Game
38
+ three: ["three"],
39
+ babylon: ["@babylonjs/"]
40
+ };
41
+ var MEDIUM_LIB_GROUPS = {
42
+ "utils": ["@vueuse/", "ahooks", "react-use"],
43
+ "icons": ["@iconify/", "@ant-design/icons", "@heroicons/", "lucide-react"],
44
+ "form": ["react-hook-form", "formik", "async-validator"],
45
+ "i18n": ["i18next", "react-i18next", "vue-i18n"]
46
+ };
47
+ function normalizeId(id) {
48
+ return id.replace(/\\/g, "/").replace(/%5C/g, "/");
49
+ }
50
+ function makeNodeModulesPattern(pkg) {
51
+ if (pkg instanceof RegExp) {
52
+ return (id) => pkg.test(id);
53
+ }
54
+ const scoped = pkg.startsWith("@");
55
+ const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56
+ const base = scoped ? escaped : `(?:@[^/]+/)?${escaped}`;
57
+ const re = new RegExp(`/node_modules/(?:[.]pnpm/)?(?:${base})(?:/|@|$)`, "i");
58
+ return (id) => re.test(id);
59
+ }
60
+ function readProjectDependencies(cwd) {
61
+ try {
62
+ const pkgPath = path.join(cwd, "package.json");
63
+ const json = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
64
+ return new Set(Object.keys(json.dependencies || {}));
65
+ } catch {
66
+ return /* @__PURE__ */ new Set();
67
+ }
68
+ }
69
+ function buildGroupMatchers(options, projectDeps, pluginHints) {
70
+ const matchers = [];
71
+ const strategy = options.strategy || "balanced";
72
+ if (options.groups) {
73
+ for (const [name, patterns] of Object.entries(options.groups)) {
74
+ if (patterns && patterns.length) {
75
+ const sorted = patterns.slice().sort((a, b) => {
76
+ const la = typeof a === "string" ? a.length : 0;
77
+ const lb = typeof b === "string" ? b.length : 0;
78
+ return lb - la;
79
+ });
80
+ const testers = sorted.map(makeNodeModulesPattern);
81
+ matchers.push({
82
+ name,
83
+ test: (id) => testers.some((t) => t(id)),
84
+ priority: 100
85
+ });
86
+ }
87
+ }
88
+ }
89
+ const detectedGroups = {};
90
+ if (pluginHints.has("vue")) {
91
+ detectedGroups["vue"] = ["vue", "@vue/"];
92
+ }
93
+ if (pluginHints.has("vueuse")) {
94
+ detectedGroups["vueuse"] = ["@vueuse/"];
95
+ }
96
+ for (const [name, patterns] of Object.entries(detectedGroups)) {
97
+ const testers = patterns.map(makeNodeModulesPattern);
98
+ matchers.push({
99
+ name,
100
+ test: (id) => testers.some((t) => t(id)),
101
+ priority: 90
102
+ });
103
+ }
104
+ if (strategy === "aggressive") {
105
+ for (const dep of projectDeps) {
106
+ if (!dep.startsWith("@types/")) {
107
+ matchers.push({
108
+ name: dep,
109
+ test: makeNodeModulesPattern(dep),
110
+ priority: 50
111
+ });
112
+ }
113
+ }
114
+ } else if (strategy === "balanced") {
115
+ for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
116
+ const hasAny = patterns.some((p) => {
117
+ const pkgName = p.replace(/\//g, "");
118
+ return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
119
+ });
120
+ if (hasAny) {
121
+ const testers = patterns.map(makeNodeModulesPattern);
122
+ matchers.push({
123
+ name: groupName,
124
+ test: (id) => testers.some((t) => t(id)),
125
+ priority: 80
126
+ });
127
+ }
128
+ }
129
+ for (const [groupName, patterns] of Object.entries(MEDIUM_LIB_GROUPS)) {
130
+ const hasAny = patterns.some((p) => {
131
+ return Array.from(projectDeps).some(
132
+ (dep) => dep.includes(p.replace(/\//g, "").replace(/\*/g, ""))
133
+ );
134
+ });
135
+ if (hasAny) {
136
+ const testers = patterns.map(makeNodeModulesPattern);
137
+ matchers.push({
138
+ name: groupName,
139
+ test: (id) => testers.some((t) => t(id)),
140
+ priority: 70
141
+ });
142
+ }
143
+ }
144
+ } else if (strategy === "conservative") {
145
+ const veryLargeLibs = ["react", "vue", "angular", "antd", "element-plus", "echarts", "three"];
146
+ for (const [groupName, patterns] of Object.entries(COMMON_LARGE_LIBS)) {
147
+ if (veryLargeLibs.includes(groupName)) {
148
+ const hasAny = patterns.some((p) => {
149
+ const pkgName = p.replace(/\//g, "");
150
+ return Array.from(projectDeps).some((dep) => dep.includes(pkgName));
151
+ });
152
+ if (hasAny) {
153
+ const testers = patterns.map(makeNodeModulesPattern);
154
+ matchers.push({
155
+ name: groupName,
156
+ test: (id) => testers.some((t) => t(id)),
157
+ priority: 80
158
+ });
159
+ }
160
+ }
161
+ }
162
+ }
163
+ return matchers.sort((a, b) => b.priority - a.priority);
164
+ }
165
+ function OPS(opts = {}) {
166
+ const options = {
167
+ override: opts.override ?? false,
168
+ strategy: opts.strategy ?? "balanced",
169
+ minSize: opts.minSize ?? 50,
170
+ ...opts.groups ? { groups: opts.groups } : {}
171
+ };
172
+ let groupsRef = [];
173
+ const manualChunks = (id) => {
174
+ const nid = normalizeId(id);
175
+ if (!/\/node_modules\//.test(nid)) return void 0;
176
+ for (const g of groupsRef) {
177
+ if (g.test(nid)) return g.name;
178
+ }
179
+ return "vendor";
180
+ };
181
+ const assetFileNamesFn = (assetInfo) => {
182
+ const name = assetInfo.name ?? "";
183
+ const ext = name.split(".").pop()?.toLowerCase();
184
+ if (ext === "css") return "css/[name]-[hash][extname]";
185
+ if (["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"].includes(ext ?? ""))
186
+ return "img/[name]-[hash][extname]";
187
+ if (["woff", "woff2", "eot", "ttf", "otf"].includes(ext ?? ""))
188
+ return "fonts/[name]-[hash][extname]";
189
+ return "assets/[name]-[hash][extname]";
190
+ };
191
+ return {
192
+ name: "vite-plugin-ops",
193
+ enforce: "post",
194
+ config(userConfig, _env) {
195
+ const existingOutput = userConfig.build?.rollupOptions?.output;
196
+ const outputIsArray = Array.isArray(existingOutput);
197
+ const shouldMerge = !options.override && existingOutput && !outputIsArray;
198
+ const injected = {
199
+ entryFileNames: "js/[name]-[hash].js",
200
+ chunkFileNames: "js/[name]-[hash].js",
201
+ assetFileNames: assetFileNamesFn,
202
+ manualChunks
203
+ };
204
+ let output;
205
+ if (shouldMerge) {
206
+ const base = existingOutput;
207
+ const merged = { ...base };
208
+ if (!("entryFileNames" in merged)) merged["entryFileNames"] = injected.entryFileNames;
209
+ if (!("chunkFileNames" in merged)) merged["chunkFileNames"] = injected.chunkFileNames;
210
+ if (!("assetFileNames" in merged)) merged["assetFileNames"] = injected.assetFileNames;
211
+ if (!("manualChunks" in merged)) merged["manualChunks"] = injected.manualChunks;
212
+ output = merged;
213
+ } else {
214
+ output = injected;
215
+ }
216
+ return {
217
+ build: {
218
+ rollupOptions: {
219
+ output
220
+ }
221
+ }
222
+ };
223
+ },
224
+ configResolved(resolved) {
225
+ const cwd = resolved.root || process.cwd();
226
+ const projectDeps = readProjectDependencies(cwd);
227
+ const pluginNames = new Set(resolved.plugins.map((p) => p.name));
228
+ const hints = /* @__PURE__ */ new Set();
229
+ if (pluginNames.has("vite:vue") || pluginNames.has("vite:vue-jsx")) {
230
+ hints.add("vue");
231
+ }
232
+ if (pluginNames.has("unplugin-vue-components")) {
233
+ hints.add("vueuse");
234
+ }
235
+ groupsRef = buildGroupMatchers(options, projectDeps, hints);
236
+ if (resolved.command === "build") {
237
+ const strategyDesc = {
238
+ aggressive: "Aggressive (split most dependencies)",
239
+ balanced: "Balanced (split large libraries)",
240
+ conservative: "Conservative (minimal splitting)"
241
+ };
242
+ console.log(`
243
+ \u{1F4E6} OPS Chunking Strategy: ${strategyDesc[options.strategy]}`);
244
+ console.log(`\u{1F4CA} Detected ${groupsRef.length} chunk groups`);
245
+ }
246
+ }
247
+ };
248
+ }
249
+
250
+ export { OPS as default };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "vite-plugin-ops",
3
+ "version": "0.1.0",
4
+ "description": "Vite plugin to organize build outputs and vendor chunking.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "suileyan",
8
+
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "main": "dist/index.cjs",
18
+ "types": "dist/index.d.ts",
19
+
20
+ "files": ["dist", "README.md", "LICENSE"],
21
+
22
+ "sideEffects": false,
23
+ "engines": {
24
+ "node": ">=20.19"
25
+ },
26
+
27
+ "keywords": ["vite", "vite-plugin", "rollup", "chunks", "vendor", "build"],
28
+
29
+ "repository": { "type": "git", "url": "git+https://github.com/you/vite-plugin-ops.git" },
30
+ "bugs": { "url": "https://github.com/you/vite-plugin-ops/issues" },
31
+ "homepage": "https://github.com/you/vite-plugin-ops#readme",
32
+
33
+ "peerDependencies": {
34
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.6.0",
38
+ "rimraf": "^6.0.1",
39
+ "tsup": "^8.5.0",
40
+ "typescript": "^5.9.2",
41
+ "vite": "^7.1.7",
42
+ "rollup": "^4.0.0"
43
+ },
44
+
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch --sourcemap",
48
+ "clean": "rimraf dist || rmdir /s /q dist",
49
+ "prepublishOnly": "npm run clean && npm run build"
50
+ },
51
+
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ }
56
+ }