vite-plugin-rebundle 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.
@@ -0,0 +1,2 @@
1
+ import { type Options } from './rebundle.js';
2
+ export default function rebundle(options?: Options): import("vite").Plugin<any>;
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { Rebundle } from './rebundle.js';
2
+ export default function rebundle(options = {}) {
3
+ return new Rebundle(options).plugin;
4
+ }
@@ -0,0 +1,25 @@
1
+ import type { BuildOptions } from 'esbuild';
2
+ import type { Plugin } from 'vite';
3
+ export type Options = BuildOptions & {
4
+ bundles?: {
5
+ [chunkName: string]: BuildOptions;
6
+ };
7
+ };
8
+ export type ContentMap = {
9
+ [path: string]: string;
10
+ };
11
+ export declare class Rebundle {
12
+ private options;
13
+ private config;
14
+ private emptyOutDir;
15
+ private content;
16
+ constructor(options: Options);
17
+ get plugin(): Plugin;
18
+ private onConfig;
19
+ private onConfigResolved;
20
+ private onWriteBundle;
21
+ private get outDir();
22
+ private getChunkContentMap;
23
+ private without;
24
+ private get never();
25
+ }
@@ -0,0 +1,162 @@
1
+ import $chalk from 'chalk';
2
+ import * as $esbuild from 'esbuild';
3
+ import * as $merge from 'merge-anything';
4
+ import * as $fs from 'node:fs/promises';
5
+ import * as $path from 'node:path';
6
+ export class Rebundle {
7
+ options;
8
+ config = null;
9
+ emptyOutDir = true;
10
+ content = {};
11
+ constructor(options) {
12
+ this.options = options;
13
+ }
14
+ get plugin() {
15
+ return {
16
+ name: 'vite-plugin-rebundle',
17
+ apply: 'build',
18
+ enforce: 'post',
19
+ config: this.onConfig,
20
+ configResolved: this.onConfigResolved,
21
+ writeBundle: this.onWriteBundle,
22
+ };
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // HOOKS
26
+ // ---------------------------------------------------------------------------
27
+ onConfig = (config) => {
28
+ // Save user's `emptyOutDir` value
29
+ this.emptyOutDir = config.build?.emptyOutDir ?? true;
30
+ // Make `emptyOutDir = false` to prevent Vite from deleting rebundled output files
31
+ return {
32
+ build: {
33
+ emptyOutDir: false,
34
+ },
35
+ };
36
+ };
37
+ onConfigResolved = async (config) => {
38
+ // Save resolved config
39
+ this.config = config;
40
+ // Cleanup output directory if user's `emptyOutDir` is `true`
41
+ if (this.emptyOutDir) {
42
+ await $fs.rmdir(this.outDir, { recursive: true });
43
+ }
44
+ // Hide .js files from output logs
45
+ const info = this.config.logger.info;
46
+ this.config.logger.info = (message, options) => {
47
+ const path = message.split(/\s+/)[0];
48
+ if ($path.extname(path) === '.js')
49
+ return;
50
+ info(message, options);
51
+ };
52
+ };
53
+ onWriteBundle = async (output, bundle) => {
54
+ // Get entry js chunks
55
+ const entryJsChunks = Object.values(bundle)
56
+ .filter(chunkOrAsset => chunkOrAsset.type === 'chunk')
57
+ .filter(chunk => chunk.isEntry && $path.extname(chunk.fileName) === '.js');
58
+ // Get non-entry chunks (.js, .js.map)
59
+ const nonEntryChunks = Object.values(bundle)
60
+ .filter(chunkOrAsset => chunkOrAsset.type === 'chunk')
61
+ .filter(chunk => !chunk.isEntry);
62
+ // Rebundle entry js chunks with esbuild
63
+ await Promise.all(entryJsChunks.map(async (chunk) => {
64
+ if (!this.config)
65
+ throw this.never;
66
+ // Check if chunk has been modified
67
+ const content = await this.getChunkContentMap(chunk);
68
+ const usedPaths = Object.keys(content);
69
+ const changed = usedPaths.some(path => content[path] !== this.content[path]);
70
+ // Update content map
71
+ Object.assign(this.content, content);
72
+ // Not modified? -> Skip
73
+ if (!changed)
74
+ return;
75
+ // Prepare chunk path and build options
76
+ const chunkPath = $path.join(this.outDir, chunk.fileName);
77
+ const commonBuildOptions = this.without(this.options, 'bundles');
78
+ const chunkBuildOptions = this.options.bundles?.[chunk.name] ?? {};
79
+ // Build with esbuild
80
+ await $esbuild.build({
81
+ minify: Boolean(this.config.build.minify),
82
+ sourcemap: Boolean(this.config.build.sourcemap),
83
+ ...$merge.mergeAndConcat(commonBuildOptions, chunkBuildOptions),
84
+ outfile: chunkPath,
85
+ entryPoints: [chunkPath],
86
+ bundle: true,
87
+ allowOverwrite: true,
88
+ plugins: [
89
+ ...(commonBuildOptions.plugins ?? []),
90
+ ...(chunkBuildOptions.plugins ?? []),
91
+ {
92
+ name: 'logger',
93
+ setup: build => {
94
+ build.onEnd(result => {
95
+ if (result.errors.length > 0)
96
+ return;
97
+ const dir = $chalk.dim(`${this.outDir}/`);
98
+ const name = $chalk.cyan(chunk.fileName);
99
+ const tag = $chalk.dim.cyan('rebundle');
100
+ console.log(`${dir}${name} ${tag}`);
101
+ });
102
+ },
103
+ },
104
+ ],
105
+ });
106
+ // Update chunk and corresponding sourcemap chunk
107
+ chunk.code = await $fs.readFile(chunkPath, 'utf-8');
108
+ if (chunk.sourcemapFileName) {
109
+ const sourcemapChunk = bundle[chunk.sourcemapFileName];
110
+ if (sourcemapChunk.type !== 'asset')
111
+ throw this.never;
112
+ const sourcemapChunkPath = $path.join(this.outDir, chunk.sourcemapFileName);
113
+ sourcemapChunk.source = await $fs.readFile(sourcemapChunkPath, 'utf-8');
114
+ }
115
+ }));
116
+ // Remove all non-entry chunks
117
+ for (const chunk of nonEntryChunks) {
118
+ // Remove file itself
119
+ const path = $path.resolve(this.outDir, chunk.fileName);
120
+ await $fs.unlink(path);
121
+ delete bundle[chunk.fileName];
122
+ // Remove sourcemap if any
123
+ if (chunk.sourcemapFileName) {
124
+ const sourcemapPath = $path.resolve(this.outDir, chunk.sourcemapFileName);
125
+ await $fs.unlink(sourcemapPath);
126
+ delete bundle[chunk.sourcemapFileName];
127
+ }
128
+ // Remove containing directory if empty
129
+ const dir = $path.dirname(path);
130
+ const files = await $fs.readdir(dir);
131
+ if (files.length === 0) {
132
+ console.warn(await $fs.stat(dir));
133
+ await $fs.rmdir(dir);
134
+ }
135
+ }
136
+ };
137
+ // ---------------------------------------------------------------------------
138
+ // HELPERS
139
+ // ---------------------------------------------------------------------------
140
+ get outDir() {
141
+ if (!this.config)
142
+ throw this.never;
143
+ return this.config.build.outDir;
144
+ }
145
+ async getChunkContentMap(chunk) {
146
+ const content = {};
147
+ const usedPaths = [chunk.fileName, ...chunk.imports];
148
+ await Promise.all(usedPaths.map(async (path) => {
149
+ const fullPath = $path.join(this.outDir, path);
150
+ content[path] = await $fs.readFile(fullPath, 'utf-8');
151
+ }));
152
+ return content;
153
+ }
154
+ without(object, key) {
155
+ const result = { ...object };
156
+ delete result[key];
157
+ return result;
158
+ }
159
+ get never() {
160
+ throw new Error('never');
161
+ }
162
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "vite-plugin-rebundle",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "author": "imkost",
7
+ "description": "Vite plugin that forces single-file output per entry. Ensures each entry point is bundled into a standalone file without code-splitting.",
8
+ "scripts": {
9
+ "dev": "rm -rf dist && tsc --watch",
10
+ "build": "rm -rf dist && tsc",
11
+ "lint": "eslint src",
12
+ "release": "npm run build && npm publish",
13
+ "release:patch": "npm version patch && npm run release",
14
+ "release:minor": "npm version minor && npm run release"
15
+ },
16
+ "exports": {
17
+ "import": "./dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "vite",
24
+ "vite-plugin"
25
+ ],
26
+ "dependencies": {
27
+ "chalk": "^5.6.0",
28
+ "merge-anything": "^6.0.6"
29
+ }
30
+ }