@omnimod/plugin-webpack-to-vite 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) 2026 salnika
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,26 @@
1
+ # @omnimod/plugin-webpack-to-vite
2
+
3
+ [![npm](https://img.shields.io/npm/v/%40omnimod%2Fplugin-webpack-to-vite?label=npm)](https://www.npmjs.com/package/@omnimod/plugin-webpack-to-vite)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/Salnika/omnimod/ci.yml?branch=master&label=ci)](https://github.com/Salnika/omnimod/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/github/license/Salnika/omnimod)](../../LICENSE)
6
+
7
+ omnimod plugin that scaffolds a Vite config from a Webpack config.
8
+
9
+ ## Use With The CLI
10
+
11
+ ```bash
12
+ omnimod run webpack-to-vite "webpack.config.{js,ts,cjs,mjs}"
13
+ omnimod run webpack-to-vite "webpack.config.{js,ts,cjs,mjs}" --write
14
+ ```
15
+
16
+ ## Use As A Library
17
+
18
+ ```ts
19
+ import { webpackToVite } from "@omnimod/plugin-webpack-to-vite";
20
+ ```
21
+
22
+ ## Notes
23
+
24
+ The plugin extracts supported Webpack configuration fields and emits a Vite
25
+ configuration scaffold. Project-specific loader, plugin, and runtime behavior is
26
+ reported for manual migration.
@@ -0,0 +1,12 @@
1
+ //#region src/plugin.d.ts
2
+ type WebpackToViteOptions = Record<string, unknown>;
3
+ /**
4
+ * Scaffold a Vite config from a Webpack config. Webpack configs are arbitrary
5
+ * JS, so this never transforms the original file. Instead it reads what it can
6
+ * *statically* (resolve.alias/extensions, DefinePlugin, devServer.port/proxy,
7
+ * entry/output, module.rules) and emits a sibling `vite.config.js` skeleton plus
8
+ * a `WEBPACK_MIGRATION.md` agent guide, then reports where to look next.
9
+ */
10
+ declare const webpackToVite: import("@omnimod/core").Plugin<WebpackToViteOptions, unknown>;
11
+ //#endregion
12
+ export { type WebpackToViteOptions, webpackToVite as default, webpackToVite };
package/dist/index.mjs ADDED
@@ -0,0 +1,496 @@
1
+ import { definePlugin } from "@omnimod/core";
2
+ import { cast } from "@omnimod/plugin-utils";
3
+ //#region src/extract.ts
4
+ /** Read a property's key name (identifier or string literal). */
5
+ function keyName(key) {
6
+ if (key.type === "Identifier") return cast(key).name;
7
+ if (key.type === "Literal") {
8
+ const value = cast(key).value;
9
+ return typeof value === "string" ? value : String(value);
10
+ }
11
+ return null;
12
+ }
13
+ /** Find a property by name on an object expression. */
14
+ function prop(obj, name) {
15
+ for (const p of obj.properties) {
16
+ if (p.type !== "Property") continue;
17
+ const property = cast(p);
18
+ if (keyName(property.key) === name) return property;
19
+ }
20
+ return null;
21
+ }
22
+ function asObject(node) {
23
+ if (!node || node.type !== "ObjectExpression") return null;
24
+ return cast(node);
25
+ }
26
+ function stringLiteral(node) {
27
+ if (!node || node.type !== "Literal") return null;
28
+ const value = cast(node).value;
29
+ return typeof value === "string" ? value : null;
30
+ }
31
+ /**
32
+ * Find the exported config object: `module.exports = {…}` or
33
+ * `export default {…}`. Function/array-returning configs are not resolved
34
+ * statically (they yield `found: false`).
35
+ */
36
+ function findConfigObject(program) {
37
+ for (const stmt of program.body) if (stmt.type === "ExpressionStatement") {
38
+ const expr = cast(stmt).expression;
39
+ if (expr.type !== "AssignmentExpression") continue;
40
+ const assign = cast(expr);
41
+ if (!isModuleExports(assign.left)) continue;
42
+ const obj = asObject(assign.right);
43
+ if (obj) return obj;
44
+ } else if (stmt.type === "ExportDefaultDeclaration") {
45
+ const decl = cast(stmt).declaration;
46
+ const obj = asObject(decl);
47
+ if (obj) return obj;
48
+ }
49
+ return null;
50
+ }
51
+ /** True when `node` is the `module.exports` member expression. */
52
+ function isModuleExports(node) {
53
+ if (node.type !== "MemberExpression") return false;
54
+ const member = cast(node);
55
+ if (member.object.type !== "Identifier" || member.property.type !== "Identifier") return false;
56
+ return cast(member.object).name === "module" && cast(member.property).name === "exports";
57
+ }
58
+ /** Collect alias entries from `resolve.alias`. */
59
+ function readAliases(aliasObj, source) {
60
+ const aliases = [];
61
+ let haveHelpers = false;
62
+ for (const p of aliasObj.properties) {
63
+ if (p.type !== "Property") continue;
64
+ const property = cast(p);
65
+ const key = keyName(property.key);
66
+ if (key === null) continue;
67
+ const literal = stringLiteral(property.value);
68
+ if (literal === null) haveHelpers = true;
69
+ aliases.push({
70
+ key,
71
+ valueSource: source.slice(property.value.start, property.value.end),
72
+ literal
73
+ });
74
+ }
75
+ return {
76
+ aliases,
77
+ haveHelpers
78
+ };
79
+ }
80
+ /** Read a string array like `extensions: [".js", ".ts"]`. */
81
+ function readStringArray(node) {
82
+ if (!node || node.type !== "ArrayExpression") return null;
83
+ const arr = cast(node);
84
+ const out = [];
85
+ for (const el of arr.elements) {
86
+ if (!el) continue;
87
+ const str = stringLiteral(el);
88
+ if (str !== null) out.push(str);
89
+ }
90
+ return out;
91
+ }
92
+ /** Read `plugins: [ new webpack.DefinePlugin({...}) ]` into define entries. */
93
+ function readDefine(pluginsNode, source) {
94
+ if (!pluginsNode || pluginsNode.type !== "ArrayExpression") return [];
95
+ const arr = cast(pluginsNode);
96
+ const out = [];
97
+ for (const el of arr.elements) {
98
+ if (!el || el.type !== "NewExpression") continue;
99
+ const created = cast(el);
100
+ if (!isDefinePlugin(created.callee)) continue;
101
+ const arg = created.arguments[0];
102
+ const obj = asObject(arg ?? null);
103
+ if (!obj) continue;
104
+ for (const p of obj.properties) {
105
+ if (p.type !== "Property") continue;
106
+ const property = cast(p);
107
+ const key = keyName(property.key);
108
+ if (key === null) continue;
109
+ out.push({
110
+ key,
111
+ valueSource: source.slice(property.value.start, property.value.end)
112
+ });
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ /** True when a callee resolves to `DefinePlugin` (bare or `webpack.DefinePlugin`). */
118
+ function isDefinePlugin(callee) {
119
+ if (callee.type === "Identifier") return cast(callee).name === "DefinePlugin";
120
+ if (callee.type === "MemberExpression") {
121
+ const member = cast(callee);
122
+ return member.property.type === "Identifier" && cast(member.property).name === "DefinePlugin";
123
+ }
124
+ return false;
125
+ }
126
+ /** Read `module.rules: [...]` into a list of test + loader names. */
127
+ function readRules(moduleNode, source) {
128
+ const moduleObj = asObject(moduleNode);
129
+ if (!moduleObj) return [];
130
+ const rulesProp = prop(moduleObj, "rules");
131
+ if (!rulesProp || rulesProp.value.type !== "ArrayExpression") return [];
132
+ const rules = cast(rulesProp.value);
133
+ const out = [];
134
+ for (const el of rules.elements) {
135
+ const ruleObj = asObject(el ?? null);
136
+ if (!ruleObj) continue;
137
+ const testProp = prop(ruleObj, "test");
138
+ const test = testProp ? source.slice(testProp.value.start, testProp.value.end) : null;
139
+ out.push({
140
+ test,
141
+ loaders: readLoaders(ruleObj)
142
+ });
143
+ }
144
+ return out;
145
+ }
146
+ /** Gather loader names from a rule's `use` / `loader` fields. */
147
+ function readLoaders(ruleObj) {
148
+ const loaders = [];
149
+ const loaderProp = prop(ruleObj, "loader");
150
+ if (loaderProp) {
151
+ const str = stringLiteral(loaderProp.value);
152
+ if (str !== null) loaders.push(str);
153
+ }
154
+ const useProp = prop(ruleObj, "use");
155
+ if (useProp) collectUse(useProp.value, loaders);
156
+ return loaders;
157
+ }
158
+ /** `use` can be a string, an array of strings/objects, or `{ loader }` objects. */
159
+ function collectUse(node, out) {
160
+ const str = stringLiteral(node);
161
+ if (str !== null) {
162
+ out.push(str);
163
+ return;
164
+ }
165
+ if (node.type === "ArrayExpression") {
166
+ for (const el of cast(node).elements) if (el) collectUse(el, out);
167
+ return;
168
+ }
169
+ const obj = asObject(node);
170
+ if (obj) {
171
+ const loaderProp = prop(obj, "loader");
172
+ if (loaderProp) {
173
+ const value = stringLiteral(loaderProp.value);
174
+ if (value !== null) out.push(value);
175
+ }
176
+ }
177
+ }
178
+ /** Parse the webpack config into the statically-readable fields we support. */
179
+ function extractConfig(program, source) {
180
+ const empty = {
181
+ found: false,
182
+ aliases: [],
183
+ aliasesHaveHelpers: false,
184
+ extensions: null,
185
+ define: [],
186
+ devServerPort: null,
187
+ devServerProxy: null,
188
+ entry: null,
189
+ output: null,
190
+ rules: []
191
+ };
192
+ const config = findConfigObject(program);
193
+ if (!config) return empty;
194
+ const resolveObj = asObject(prop(config, "resolve")?.value ?? null);
195
+ let aliases = [];
196
+ let aliasesHaveHelpers = false;
197
+ let extensions = null;
198
+ if (resolveObj) {
199
+ const aliasObj = asObject(prop(resolveObj, "alias")?.value ?? null);
200
+ if (aliasObj) {
201
+ const read = readAliases(aliasObj, source);
202
+ aliases = read.aliases;
203
+ aliasesHaveHelpers = read.haveHelpers;
204
+ }
205
+ extensions = readStringArray(prop(resolveObj, "extensions")?.value ?? null);
206
+ }
207
+ const define = readDefine(prop(config, "plugins")?.value ?? null, source);
208
+ const devServerObj = asObject(prop(config, "devServer")?.value ?? null);
209
+ let devServerPort = null;
210
+ let devServerProxy = null;
211
+ if (devServerObj) {
212
+ const portProp = prop(devServerObj, "port");
213
+ if (portProp) devServerPort = source.slice(portProp.value.start, portProp.value.end);
214
+ const proxyProp = prop(devServerObj, "proxy");
215
+ if (proxyProp) devServerProxy = source.slice(proxyProp.value.start, proxyProp.value.end);
216
+ }
217
+ const entryProp = prop(config, "entry");
218
+ const entry = entryProp ? source.slice(entryProp.value.start, entryProp.value.end) : null;
219
+ const outputProp = prop(config, "output");
220
+ const output = outputProp ? source.slice(outputProp.value.start, outputProp.value.end) : null;
221
+ const rules = readRules(prop(config, "module")?.value ?? null, source);
222
+ return {
223
+ found: true,
224
+ aliases,
225
+ aliasesHaveHelpers,
226
+ extensions,
227
+ define,
228
+ devServerPort,
229
+ devServerProxy,
230
+ entry,
231
+ output,
232
+ rules
233
+ };
234
+ }
235
+ //#endregion
236
+ //#region src/generate.ts
237
+ /** JSON-quote a string for use as an object key/value. */
238
+ function quote(value) {
239
+ return JSON.stringify(value);
240
+ }
241
+ /** Render the mapped `vite.config.js` skeleton from a parsed webpack config. */
242
+ function renderViteConfig(config) {
243
+ const body = [];
244
+ const resolveLines = [];
245
+ if (config.aliases.length > 0) {
246
+ const aliasLines = config.aliases.map((alias) => {
247
+ if (alias.literal !== null) return ` ${quote(alias.key)}: ${quote(alias.literal)},`;
248
+ return ` ${quote(alias.key)}: ${alias.valueSource}, // TODO(omnimod): review webpack-only helper; Vite resolves relative to the config file`;
249
+ });
250
+ resolveLines.push(" alias: {", ...aliasLines, " },");
251
+ }
252
+ if (config.extensions && config.extensions.length > 0) {
253
+ const exts = config.extensions.map((ext) => quote(ext)).join(", ");
254
+ resolveLines.push(` extensions: [${exts}],`);
255
+ }
256
+ if (resolveLines.length > 0) body.push(" resolve: {", ...resolveLines, " },");
257
+ if (config.define.length > 0) {
258
+ const defineLines = config.define.map((entry) => ` ${quote(entry.key)}: ${entry.valueSource},`);
259
+ body.push(" define: {", ...defineLines, " },");
260
+ }
261
+ const serverLines = [];
262
+ if (config.devServerPort !== null) serverLines.push(` port: ${config.devServerPort},`);
263
+ if (config.devServerProxy !== null) serverLines.push(` proxy: ${config.devServerProxy}, // TODO(omnimod): Vite proxy uses the same http-proxy options; verify keys`);
264
+ if (serverLines.length > 0) body.push(" server: {", ...serverLines, " },");
265
+ const notes = [];
266
+ if (config.entry !== null) notes.push(` // TODO(omnimod): webpack entry was ${inlineComment(config.entry)}. Vite uses index.html as the entry point; move your entry <script> there.`);
267
+ if (config.output !== null) notes.push(` // TODO(omnimod): webpack output was ${inlineComment(config.output)}. Configure Vite output via build.outDir / build.rollupOptions if needed.`);
268
+ if (config.rules.length > 0) notes.push(" // TODO(omnimod): module.rules (loaders) were not translated. See WEBPACK_MIGRATION.md for the Vite equivalents.");
269
+ notes.push(" // TODO(omnimod): add your framework plugin, e.g. `plugins: [react()]` from @vitejs/plugin-react.");
270
+ return `import { defineConfig } from "vite";\n\nexport default defineConfig({\n${[...body, ...notes].join("\n")}\n});\n`;
271
+ }
272
+ /** Collapse a source snippet into a single-line, comment-safe fragment. */
273
+ function inlineComment(source) {
274
+ return source.replace(/\s+/g, " ").replace(/\*\//g, "* /").trim();
275
+ }
276
+ //#endregion
277
+ //#region src/migration.ts
278
+ /** Known webpack loader → Vite equivalent, keyed by a substring of the loader name. */
279
+ const LOADER_MAP = [
280
+ {
281
+ match: "babel-loader",
282
+ label: "babel-loader",
283
+ vite: "Built in via esbuild. Remove; no config needed for standard JS/JSX/TS."
284
+ },
285
+ {
286
+ match: "ts-loader",
287
+ label: "ts-loader",
288
+ vite: "Built in via esbuild. Remove; Vite transpiles TypeScript out of the box (type-check separately with `tsc --noEmit`)."
289
+ },
290
+ {
291
+ match: "style-loader",
292
+ label: "style-loader",
293
+ vite: "Built in. Remove; Vite injects CSS automatically."
294
+ },
295
+ {
296
+ match: "css-loader",
297
+ label: "css-loader",
298
+ vite: "Built in. Remove; `import \"./x.css\"` just works. Use `*.module.css` for CSS Modules."
299
+ },
300
+ {
301
+ match: "sass-loader",
302
+ label: "sass-loader",
303
+ vite: "Install `sass` as a dev dependency; Vite compiles `.scss`/`.sass` automatically."
304
+ },
305
+ {
306
+ match: "postcss-loader",
307
+ label: "postcss-loader",
308
+ vite: "Add a `postcss.config.js`; Vite runs PostCSS automatically."
309
+ },
310
+ {
311
+ match: "less-loader",
312
+ label: "less-loader",
313
+ vite: "Install `less`; Vite compiles `.less` automatically."
314
+ },
315
+ {
316
+ match: "file-loader",
317
+ label: "file-loader",
318
+ vite: "Use asset imports: `import url from \"./x.png?url\"`, or put static files under `public/`."
319
+ },
320
+ {
321
+ match: "url-loader",
322
+ label: "url-loader",
323
+ vite: "Use `?url` / `?inline` import suffixes; small assets are inlined automatically."
324
+ },
325
+ {
326
+ match: "raw-loader",
327
+ label: "raw-loader",
328
+ vite: "Use the `?raw` import suffix: `import src from \"./x.txt?raw\"`."
329
+ },
330
+ {
331
+ match: "svg",
332
+ label: "svg loader",
333
+ vite: "Use `?url`/`?raw`, or add `vite-plugin-svgr` for React components."
334
+ }
335
+ ];
336
+ /** Best-effort mapping of a single loader name to its Vite guidance. */
337
+ function mapLoader(loader) {
338
+ for (const entry of LOADER_MAP) if (loader.includes(entry.match)) return {
339
+ label: entry.label,
340
+ vite: entry.vite
341
+ };
342
+ return {
343
+ label: loader,
344
+ vite: "No direct Vite equivalent detected. Check if a Vite plugin exists, or handle in a custom plugin."
345
+ };
346
+ }
347
+ /** Render the loader-mapping rows for the config's `module.rules`. */
348
+ function renderLoaderTable(rules) {
349
+ const seen = /* @__PURE__ */ new Set();
350
+ const rows = [];
351
+ for (const rule of rules) for (const loader of rule.loaders) {
352
+ const { label, vite } = mapLoader(loader);
353
+ const test = rule.test ? ` (\`${rule.test}\`)` : "";
354
+ const key = `${label}${test}`;
355
+ if (seen.has(key)) continue;
356
+ seen.add(key);
357
+ rows.push(`| \`${label}\`${test} | ${vite} |`);
358
+ }
359
+ if (rows.length === 0) rows.push("| _(no loaders detected)_ | — |");
360
+ return [
361
+ "| Webpack loader | Vite equivalent |",
362
+ "| --- | --- |",
363
+ ...rows
364
+ ].join("\n");
365
+ }
366
+ /** Render the full agent-ready `WEBPACK_MIGRATION.md`. */
367
+ function renderMigrationGuide(config, viteConfigName) {
368
+ const lines = [];
369
+ lines.push("# Finish the Webpack → Vite migration");
370
+ lines.push("");
371
+ lines.push(`omnimod scaffolded a \`${viteConfigName}\` next to your \`webpack.config\`. The original webpack config was **left untouched**. Follow the steps below to complete the migration, then delete the webpack config and its loaders/plugins.`);
372
+ lines.push("");
373
+ lines.push("## One-time setup");
374
+ lines.push("");
375
+ lines.push("1. Install Vite and your framework plugin as dev dependencies:");
376
+ lines.push("");
377
+ lines.push(" ```sh");
378
+ lines.push(" npm install -D vite");
379
+ lines.push(" # then the plugin for your framework, e.g.:");
380
+ lines.push(" # React: npm install -D @vitejs/plugin-react");
381
+ lines.push(" # Vue: npm install -D @vitejs/plugin-vue");
382
+ lines.push(" # Svelte: npm install -D @sveltejs/vite-plugin-svelte");
383
+ lines.push(" ```");
384
+ lines.push("");
385
+ lines.push(`2. Register the plugin in \`${viteConfigName}\` (uncomment/adjust the \`plugins\` TODO).`);
386
+ lines.push("");
387
+ lines.push("## Entry point");
388
+ lines.push("");
389
+ lines.push("Vite is served from an `index.html` at the project root (not a JS `entry`). Create `index.html` and reference your entry module directly:");
390
+ lines.push("");
391
+ lines.push("```html");
392
+ lines.push("<!doctype html>");
393
+ lines.push("<html>");
394
+ lines.push(" <head>");
395
+ lines.push(" <meta charset=\"utf-8\" />");
396
+ lines.push(" <title>App</title>");
397
+ lines.push(" </head>");
398
+ lines.push(" <body>");
399
+ lines.push(" <div id=\"root\"></div>");
400
+ lines.push(" <script type=\"module\" src=\"/src/index.js\"><\/script>");
401
+ lines.push(" </body>");
402
+ lines.push("</html>");
403
+ lines.push("```");
404
+ if (config.entry) {
405
+ lines.push("");
406
+ lines.push(`> Detected webpack \`entry\`: \`${config.entry.replace(/\s+/g, " ")}\``);
407
+ }
408
+ lines.push("");
409
+ lines.push("## Environment variables");
410
+ lines.push("");
411
+ lines.push("Vite only exposes env vars prefixed with `VITE_`, accessed via `import.meta.env.VITE_*` (not `process.env.*`).");
412
+ lines.push("");
413
+ lines.push("- Rename `MY_VAR` → `VITE_MY_VAR` in your `.env` files.");
414
+ lines.push("- Replace `process.env.MY_VAR` → `import.meta.env.VITE_MY_VAR` in source.");
415
+ if (config.define.length > 0) lines.push(`- The following \`DefinePlugin\` keys were mapped into \`define\` in the generated config: ${config.define.map((entry) => `\`${entry.key}\``).join(", ")}.`);
416
+ lines.push("");
417
+ lines.push("## Loaders → Vite");
418
+ lines.push("");
419
+ lines.push(renderLoaderTable(config.rules));
420
+ lines.push("");
421
+ lines.push("## npm scripts");
422
+ lines.push("");
423
+ lines.push("Replace your webpack scripts with Vite's:");
424
+ lines.push("");
425
+ lines.push("```jsonc");
426
+ lines.push("{");
427
+ lines.push(" \"scripts\": {");
428
+ lines.push(" \"dev\": \"vite\", // was: \"webpack serve\"");
429
+ lines.push(" \"build\": \"vite build\", // was: \"webpack\" / \"webpack --mode production\"");
430
+ lines.push(" \"preview\": \"vite preview\"");
431
+ lines.push(" }");
432
+ lines.push("}");
433
+ lines.push("```");
434
+ lines.push("");
435
+ lines.push("## Verify");
436
+ lines.push("");
437
+ lines.push("- [ ] `npm run dev` boots and the app loads.");
438
+ lines.push("- [ ] `npm run build` produces a working `dist/`.");
439
+ lines.push("- [ ] All aliases and env vars resolve.");
440
+ lines.push("- [ ] The old `webpack.config` and webpack deps are removed.");
441
+ lines.push("");
442
+ return `${lines.join("\n")}`;
443
+ }
444
+ //#endregion
445
+ //#region src/plugin.ts
446
+ /** Directory portion of an absolute POSIX/Windows path (no node:path dep needed). */
447
+ function dirOf(path) {
448
+ const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
449
+ return slash === -1 ? "" : path.slice(0, slash);
450
+ }
451
+ /** Join a directory and a file name with the directory's separator. */
452
+ function join(dir, name) {
453
+ if (dir === "") return name;
454
+ return `${dir}${dir.includes("\\") && !dir.includes("/") ? "\\" : "/"}${name}`;
455
+ }
456
+ /**
457
+ * Scaffold a Vite config from a Webpack config. Webpack configs are arbitrary
458
+ * JS, so this never transforms the original file. Instead it reads what it can
459
+ * *statically* (resolve.alias/extensions, DefinePlugin, devServer.port/proxy,
460
+ * entry/output, module.rules) and emits a sibling `vite.config.js` skeleton plus
461
+ * a `WEBPACK_MIGRATION.md` agent guide, then reports where to look next.
462
+ */
463
+ const webpackToVite = definePlugin({
464
+ name: "webpack-to-vite",
465
+ description: "Scaffold a Vite config from a Webpack config.",
466
+ include: ["**/webpack.config.{js,ts,cjs,mjs}"],
467
+ transform(file) {
468
+ const config = extractConfig(file.program, file.source);
469
+ const dir = dirOf(file.path);
470
+ const viteConfigName = "vite.config.js";
471
+ const viteConfigPath = join(dir, viteConfigName);
472
+ const migrationPath = join(dir, "WEBPACK_MIGRATION.md");
473
+ if (!config.found) file.report({
474
+ severity: "warn",
475
+ message: "Could not statically read a `module.exports = {…}` / `export default {…}` object (function- or array-returning config?). Emitted a starter vite.config.js and WEBPACK_MIGRATION.md; you'll need to port the fields by hand."
476
+ });
477
+ if (config.aliasesHaveHelpers) file.report({
478
+ severity: "warn",
479
+ message: "Some `resolve.alias` values use webpack-only helpers (e.g. path.resolve). They were copied verbatim into vite.config.js with a // TODO — Vite resolves relative to the config file, so review them."
480
+ });
481
+ file.emit({
482
+ path: viteConfigPath,
483
+ contents: renderViteConfig(config)
484
+ });
485
+ file.emit({
486
+ path: migrationPath,
487
+ contents: renderMigrationGuide(config, viteConfigName)
488
+ });
489
+ file.report({
490
+ severity: "info",
491
+ message: `Emitted ${viteConfigName} and WEBPACK_MIGRATION.md next to this webpack config. The webpack config was left untouched.`
492
+ });
493
+ }
494
+ });
495
+ //#endregion
496
+ export { webpackToVite as default, webpackToVite };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@omnimod/plugin-webpack-to-vite",
3
+ "version": "0.1.0",
4
+ "description": "omnimod plugin: scaffold a Vite config from a Webpack config.",
5
+ "keywords": [
6
+ "codemod",
7
+ "migration",
8
+ "omnimod-plugin",
9
+ "vite",
10
+ "webpack"
11
+ ],
12
+ "homepage": "https://salnika.github.io/omnimod/",
13
+ "bugs": "https://github.com/salnika/omnimod/issues",
14
+ "license": "MIT",
15
+ "author": "salnika",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/salnika/omnimod.git",
19
+ "directory": "packages/plugin-webpack-to-vite"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./dist/index.mjs",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "@omnimod/core": "0.1.0",
34
+ "@omnimod/plugin-utils": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^25.6.2",
38
+ "@typescript/native-preview": "7.0.0-dev.20260509.2",
39
+ "typescript": "^6.0.3",
40
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
41
+ "vite-plus": "0.2.2"
42
+ },
43
+ "scripts": {
44
+ "build": "vp pack",
45
+ "dev": "vp pack --watch",
46
+ "test": "vp test",
47
+ "check": "vp check"
48
+ }
49
+ }