vite-plugin-rebundle 1.2.16 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-rebundle",
3
- "version": "1.2.16",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "imkost",
@@ -10,24 +10,22 @@
10
10
  "vite-plugin"
11
11
  ],
12
12
  "scripts": {
13
- "dev": "rm -rf dist && tsc --watch",
14
- "build": "rm -rf dist && tsc",
15
13
  "lint": "tsc --noEmit",
16
- "release": "sh -c 'npm version ${1:-patch} && npm run build && npm publish' --"
14
+ "release": "sh -c 'npm version ${1:-patch} && npm publish' --"
17
15
  },
18
16
  "exports": {
19
- "import": "./dist/index.js"
17
+ "import": "./src/index.ts"
20
18
  },
21
19
  "files": [
22
- "dist"
20
+ "src"
23
21
  ],
24
22
  "dependencies": {
25
23
  "@eposlabs/utils": "^1.0.13",
26
24
  "@types/ws": "^8.18.1",
27
25
  "chalk": "^5.6.2",
28
- "esbuild": "^0.25.9",
29
26
  "filesize": "^11.0.2",
30
27
  "portfinder": "^1.0.38",
28
+ "rolldown": "^1.0.0-beta.40",
31
29
  "rollup": "^4.50.1",
32
30
  "vite": "^7.1.5",
33
31
  "ws": "^8.18.3"
package/readme.md CHANGED
@@ -4,7 +4,7 @@ A Vite plugin that guarantees **one standalone file per entry point**. Each entr
4
4
 
5
5
  ## Why?
6
6
 
7
- Sometimes you need bundles without dynamic imports. Vite/Rollup don’t provide this option when building with multiple entries. This plugin solves it by rebundling Vite’s output with esbuild to enforce single-file output.
7
+ Sometimes you need bundles without dynamic imports. Vite/Rollup don’t provide this option when building with multiple entries. This plugin solves it by rebundling Vite’s output with rolldown to enforce single-file output.
8
8
 
9
9
  > ⚠️ This plugin runs **only during** `vite build`. It does not affect the Vite dev server.
10
10
 
@@ -38,21 +38,18 @@ export default defineConfig({
38
38
 
39
39
  ## Options
40
40
 
41
- You can provide **esbuild options per entry point**. This is useful, for example, to inject custom define variables into specific bundles:
41
+ You can provide **rolldown options per entry point**. This is useful, for example, to inject custom define variables into specific bundles:
42
42
 
43
43
  ```javascript
44
44
  export default defineConfig({
45
45
  plugins: [
46
46
  rebundle({
47
47
  app: {
48
- define: {
49
- BUNDLE_NAME: JSON.stringify('app'),
50
- },
48
+ output: { define: { BUNDLE_NAME: JSON.stringify('app') } },
51
49
  },
52
50
  libs: {
53
- define: {
54
- BUNDLE_NAME: JSON.stringify('libs'),
55
- },
51
+ input: { keepNames: true },
52
+ output: { define: { BUNDLE_NAME: JSON.stringify('libs') } },
56
53
  },
57
54
  }),
58
55
  ],
@@ -73,10 +70,10 @@ export default defineConfig({
73
70
  ## How it works
74
71
 
75
72
  When you run `vite build`, Rollup normally outputs multiple chunks per entry if code-splitting is needed.
76
- `vite-plugin-rebundle` hooks into the build and **rebundles each entry’s output with esbuild**, forcing a single self-contained file per entry.
73
+ `vite-plugin-rebundle` hooks into the build and **rebundles each entry’s output with rolldown**, forcing a single self-contained file per entry.
77
74
 
78
- - Rollup still handles the initial build (tree-shaking, asset pipeline, etc.).
79
- - Afterward, each entry is passed through esbuild.
75
+ - Vite still handles the initial build (tree-shaking, asset pipeline, etc.).
76
+ - Afterward, each entry is passed through rolldown.
80
77
  - The final result is one .js file per entry with no dynamic imports or shared chunks.
81
78
  - If sourcemaps are enabled, they are preserved — the rebundled files include correct mappings.
82
79
 
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { Rebundle, type Options } from './rebundle.ts'
2
+
3
+ export type RebundleOptions = Options[string]
4
+
5
+ export function rebundle(options: Options = {}) {
6
+ return new Rebundle(options).vite
7
+ }
8
+
9
+ export default rebundle
@@ -0,0 +1,236 @@
1
+ import { safe, Unit } from '@eposlabs/utils'
2
+ import chalk from 'chalk'
3
+ import { filesize } from 'filesize'
4
+ import { readdir, readFile, rmdir, stat, unlink, writeFile } from 'node:fs/promises'
5
+ import { dirname, extname, join } from 'node:path'
6
+ import { getPort } from 'portfinder'
7
+ import { rolldown, type InputOptions, type OutputOptions } from 'rolldown'
8
+ import type { NormalizedOutputOptions, OutputBundle, OutputChunk } from 'rollup'
9
+ import type { Plugin, ResolvedConfig } from 'vite'
10
+ import { WebSocketServer } from 'ws'
11
+
12
+ export const _code_ = Symbol('rebundle:code')
13
+ export const _sourcemap_ = Symbol('rebundle:sourcemap')
14
+
15
+ export type Options = {
16
+ [bundleName: string]: {
17
+ input: InputOptions
18
+ output: OutputOptions
19
+ }
20
+ }
21
+
22
+ export class Rebundle extends Unit {
23
+ private options: Options
24
+ private config: ResolvedConfig | null = null
25
+ private originalFiles: Record<string, string> = {}
26
+ private rebundledFiles: Record<string, string> = {}
27
+ private hasError = false
28
+ private port: number | null = null
29
+ private ws: WebSocketServer | null = null
30
+
31
+ constructor(options: Options) {
32
+ super()
33
+ this.options = options
34
+ }
35
+
36
+ get vite(): Plugin {
37
+ return {
38
+ name: 'vite-plugin-rebundle',
39
+ apply: 'build',
40
+ enforce: 'post',
41
+ config: this.onConfig,
42
+ configResolved: this.onConfigResolved,
43
+ buildEnd: this.onBuildEnd,
44
+ writeBundle: this.onWriteBundle,
45
+ }
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // VITE HOOKS
50
+ // ---------------------------------------------------------------------------
51
+
52
+ private onConfig = async () => {
53
+ this.port = await getPort({ port: 3100 })
54
+ return {
55
+ define: {
56
+ 'import.meta.env.REBUNDLE_PORT': JSON.stringify(this.port),
57
+ },
58
+ }
59
+ }
60
+
61
+ private onConfigResolved = async (config: ResolvedConfig) => {
62
+ // Save resolved config
63
+ this.config = config
64
+
65
+ // Hide js files from output logs (rollup only, not supported in rolldown)
66
+ const info = this.config.logger.info
67
+ this.config.logger.info = (message, options) => {
68
+ const path = message.split(/\s+/)[0]
69
+ if (extname(path) === '.js') return
70
+ info(message, options)
71
+ }
72
+ }
73
+
74
+ private onBuildEnd = (error?: Error) => {
75
+ this.hasError = !!error
76
+ }
77
+
78
+ private onWriteBundle = async (_output: NormalizedOutputOptions, bundle: OutputBundle) => {
79
+ if (this.hasError) return
80
+ if (!this.config) throw this.never
81
+
82
+ const chunks = Object.values(bundle).filter(chunkOrAsset => chunkOrAsset.type === 'chunk')
83
+ const entryChunks = chunks.filter(chunk => chunk.isEntry)
84
+ const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry)
85
+
86
+ // Rebundle entry chunks
87
+ const modifiedChunkNames: string[] = []
88
+ await Promise.all(
89
+ entryChunks.map(async chunk => {
90
+ const modified = await this.rebundleChunk(chunk, bundle)
91
+ if (modified) modifiedChunkNames.push(chunk.name)
92
+ }),
93
+ )
94
+
95
+ // Remove non-entry chunks
96
+ for (const chunk of nonEntryChunks) {
97
+ await this.removeChunk(chunk, bundle)
98
+ }
99
+
100
+ // Notify about modified chunks
101
+ if (this.config.build.watch && modifiedChunkNames.length > 0) {
102
+ const ws = await this.ensureWs()
103
+ ws.clients.forEach(client => client.send(JSON.stringify(modifiedChunkNames)))
104
+ }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // CHUNK METHODS
109
+ // ---------------------------------------------------------------------------
110
+
111
+ async rebundleChunk(chunk: OutputChunk, bundle: OutputBundle) {
112
+ if (!this.config) throw this.never
113
+
114
+ // Delete chunk from bundle to hide log for rolldown-vite. Call for rollup for consistency.
115
+ delete bundle[chunk.fileName]
116
+
117
+ const chunkPath = join(this.dist, chunk.fileName)
118
+ const chunkOptions = this.options[chunk.name] ?? {}
119
+
120
+ // Read chunk files, save their content
121
+ const chunkFiles = await this.readChunkFiles(chunk)
122
+
123
+ // Check if chunk was modified
124
+ const chunkFilePaths = Object.keys(chunkFiles)
125
+ const chunkModified = chunkFilePaths.some(path => chunkFiles[path] !== this.originalFiles[path])
126
+
127
+ // Chunk was not modified? -> Use previous content
128
+ if (!chunkModified) {
129
+ // Overwrite vite's output with previously rebundled code
130
+ const code = this.rebundledFiles[chunk.fileName]
131
+ await this.writeToDist(chunk.fileName, code)
132
+
133
+ // Overwrite vite's sourcemap
134
+ if (chunk.sourcemapFileName) {
135
+ const sourcemap = this.rebundledFiles[chunk.sourcemapFileName]
136
+ if (sourcemap) await this.writeToDist(chunk.sourcemapFileName, sourcemap)
137
+ }
138
+
139
+ return false
140
+ }
141
+
142
+ // Build with rolldown
143
+ try {
144
+ const build = await rolldown({ ...chunkOptions.input, input: chunkPath })
145
+ await build.write({ sourcemap: !!this.config.build.sourcemap, ...chunkOptions.output, file: chunkPath })
146
+ } catch (error) {
147
+ console.error(error)
148
+ return
149
+ }
150
+
151
+ // Log successful build
152
+ const { size } = await stat(chunkPath)
153
+ const _dist_ = chalk.dim(`${this.dist}/`)
154
+ const _fileName_ = chalk.cyan(chunk.fileName)
155
+ const _rebundle_ = chalk.dim.cyan('[rebundle]')
156
+ const _size_ = chalk.bold.dim(`${filesize(size)}`)
157
+ console.log(`${_dist_}${_fileName_} ${_rebundle_} ${_size_}`)
158
+
159
+ // Save code
160
+ const code = await this.readFromDist(chunk.fileName)
161
+ if (!code) throw this.never
162
+ this.rebundledFiles[chunk.fileName] = code
163
+
164
+ // Save sourcemap
165
+ if (chunk.sourcemapFileName) {
166
+ const sourcemap = await this.readFromDist(chunk.sourcemapFileName)
167
+ if (sourcemap) this.rebundledFiles[chunk.sourcemapFileName] = sourcemap
168
+ }
169
+
170
+ return true
171
+ }
172
+
173
+ private async removeChunk(chunk: OutputChunk, bundle: OutputBundle) {
174
+ await this.removeFromDist(chunk.fileName)
175
+ delete bundle[chunk.fileName]
176
+
177
+ if (chunk.sourcemapFileName) {
178
+ await this.removeFromDist(chunk.sourcemapFileName)
179
+ delete bundle[chunk.sourcemapFileName]
180
+ }
181
+
182
+ // Recursively remove containing directory if empty
183
+ const dir = dirname(join(this.dist, chunk.fileName))
184
+ await this.removeDirectoryIfEmpty(dir)
185
+ }
186
+
187
+ private async readChunkFiles(chunk: OutputChunk) {
188
+ const files: Record<string, string> = {}
189
+ const usedPaths = [chunk.fileName, ...chunk.imports]
190
+
191
+ await Promise.all(
192
+ usedPaths.map(async path => {
193
+ const content = await this.readFromDist(path)
194
+ files[path] = content ?? ''
195
+ }),
196
+ )
197
+
198
+ return files
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // HELPERS
203
+ // ---------------------------------------------------------------------------
204
+
205
+ private get dist() {
206
+ if (!this.config) throw this.never
207
+ return this.config.build.outDir
208
+ }
209
+
210
+ private async readFromDist(path: string) {
211
+ const [content] = await safe(readFile(join(this.dist, path), 'utf-8'))
212
+ return content
213
+ }
214
+
215
+ private async writeToDist(path: string, content: string) {
216
+ await writeFile(join(this.dist, path), content, 'utf-8')
217
+ }
218
+
219
+ private async removeFromDist(path: string) {
220
+ await safe(unlink(join(this.dist, path)))
221
+ }
222
+
223
+ private async ensureWs() {
224
+ if (this.ws) return this.ws
225
+ if (!this.port) throw this.never
226
+ this.ws = new WebSocketServer({ port: this.port })
227
+ return this.ws
228
+ }
229
+
230
+ private async removeDirectoryIfEmpty(dir: string) {
231
+ const files = await readdir(dir)
232
+ if (files.length > 0) return
233
+ await rmdir(dir)
234
+ await this.removeDirectoryIfEmpty(dirname(dir))
235
+ }
236
+ }
package/dist/index.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import { type Options } from './rebundle.js';
2
- import type { BuildOptions } from 'esbuild';
3
- export type { BuildOptions };
4
- export default function rebundle(options?: Options): import("vite").Plugin<any>;
5
- export { rebundle };
package/dist/index.js DELETED
@@ -1,6 +0,0 @@
1
- import { Rebundle } from './rebundle.js';
2
- export default function rebundle(options = {}) {
3
- const rb = new Rebundle(options);
4
- return rb.vite;
5
- }
6
- export { rebundle };
@@ -1,34 +0,0 @@
1
- import * as $utils from '@eposlabs/utils';
2
- import type { BuildOptions } from 'esbuild';
3
- import type { OutputBundle, OutputChunk } from 'rollup';
4
- import type { Plugin } from 'vite';
5
- export declare const _code_: unique symbol;
6
- export declare const _sourcemap_: unique symbol;
7
- export type Options = {
8
- [chunkName: string]: BuildOptions;
9
- };
10
- export declare class Rebundle extends $utils.Unit {
11
- private options;
12
- private config;
13
- private chunkFiles;
14
- private rebundledContent;
15
- private hasError;
16
- private port;
17
- private ws;
18
- constructor(options: Options);
19
- get vite(): Plugin;
20
- private onConfig;
21
- private onConfigResolved;
22
- private onBuildEnd;
23
- private onWriteBundle;
24
- rebundleChunk(chunk: OutputChunk, bundle: OutputBundle): Promise<boolean | undefined>;
25
- private removeChunk;
26
- private readChunkFiles;
27
- private get dist();
28
- private resolve;
29
- private read;
30
- private write;
31
- private remove;
32
- private ensureWs;
33
- private removeDirectoryIfEmpty;
34
- }
package/dist/rebundle.js DELETED
@@ -1,213 +0,0 @@
1
- import * as $utils from '@eposlabs/utils';
2
- import $chalk from 'chalk';
3
- import * as $esbuild from 'esbuild';
4
- import { filesize } from 'filesize';
5
- import * as $fs from 'node:fs/promises';
6
- import * as $path from 'node:path';
7
- import $portfinder from 'portfinder';
8
- import * as $ws from 'ws';
9
- export const _code_ = Symbol('rebundle:code');
10
- export const _sourcemap_ = Symbol('rebundle:sourcemap');
11
- export class Rebundle extends $utils.Unit {
12
- options;
13
- config = null;
14
- chunkFiles = {};
15
- rebundledContent = {};
16
- hasError = false;
17
- port = null;
18
- ws = null;
19
- constructor(options) {
20
- super();
21
- this.options = options;
22
- }
23
- get vite() {
24
- return {
25
- name: 'vite-plugin-rebundle',
26
- apply: 'build',
27
- enforce: 'post',
28
- config: this.onConfig,
29
- configResolved: this.onConfigResolved,
30
- buildEnd: this.onBuildEnd,
31
- writeBundle: this.onWriteBundle,
32
- };
33
- }
34
- // ---------------------------------------------------------------------------
35
- // VITE HOOKS
36
- // ---------------------------------------------------------------------------
37
- onConfig = async () => {
38
- this.port = await $portfinder.getPort({ port: 3100 });
39
- return {
40
- define: {
41
- 'import.meta.env.REBUNDLE_PORT': JSON.stringify(this.port),
42
- },
43
- };
44
- };
45
- onConfigResolved = async (config) => {
46
- // Save resolved config
47
- this.config = config;
48
- // Hide js files from output logs (rollup only, not supported in rolldown)
49
- const info = this.config.logger.info;
50
- this.config.logger.info = (message, options) => {
51
- const path = message.split(/\s+/)[0];
52
- if ($path.extname(path) === '.js')
53
- return;
54
- info(message, options);
55
- };
56
- };
57
- onBuildEnd = (error) => {
58
- this.hasError = !!error;
59
- };
60
- onWriteBundle = async (_output, bundle) => {
61
- if (this.hasError)
62
- return;
63
- if (!this.config)
64
- throw this.never;
65
- const chunks = Object.values(bundle).filter(chunkOrAsset => chunkOrAsset.type === 'chunk');
66
- const entryChunks = chunks.filter(chunk => chunk.isEntry);
67
- const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry);
68
- // Rebundle entry chunks
69
- const modifiedChunkNames = [];
70
- await Promise.all(entryChunks.map(async (chunk) => {
71
- const modified = await this.rebundleChunk(chunk, bundle);
72
- if (modified)
73
- modifiedChunkNames.push(chunk.name);
74
- }));
75
- // Remove non-entry chunks
76
- for (const chunk of nonEntryChunks) {
77
- await this.removeChunk(chunk, bundle);
78
- }
79
- // Notify about modified chunks
80
- if (this.config.build.watch && modifiedChunkNames.length > 0) {
81
- const ws = await this.ensureWs();
82
- ws.clients.forEach(client => client.send(JSON.stringify(modifiedChunkNames)));
83
- }
84
- };
85
- // ---------------------------------------------------------------------------
86
- // CHUNK METHODS
87
- // ---------------------------------------------------------------------------
88
- async rebundleChunk(chunk, bundle) {
89
- if (!this.config)
90
- throw this.never;
91
- // Delete chunk from bundle to hide log for vite-rolldown.
92
- // Call for rollup as well for consistency.
93
- delete bundle[chunk.fileName];
94
- const chunkPath = this.resolve(chunk.fileName);
95
- const chunkBuildOptions = this.options[chunk.name] ?? {};
96
- const chunkFiles = await this.readChunkFiles(chunk);
97
- const chunkFilePaths = Object.keys(chunkFiles);
98
- const chunkChanged = chunkFilePaths.some(path => chunkFiles[path] !== this.chunkFiles[path]);
99
- // Update files cache
100
- Object.assign(this.chunkFiles, chunkFiles);
101
- // Not modified? -> Use pervious content
102
- if (!chunkChanged) {
103
- // Overwrite vite's code
104
- const code = this.rebundledContent[chunk.fileName];
105
- await this.write(chunk.fileName, code);
106
- // Overwrite vite's sourcemap
107
- if (chunk.sourcemapFileName) {
108
- const sourcemap = this.rebundledContent[chunk.sourcemapFileName];
109
- if (sourcemap)
110
- await this.write(chunk.sourcemapFileName, sourcemap);
111
- }
112
- return false;
113
- }
114
- // Build with esbuild
115
- let result;
116
- try {
117
- result = await $esbuild.build({
118
- sourcemap: Boolean(this.config.build.sourcemap),
119
- format: 'esm',
120
- ...chunkBuildOptions,
121
- banner: { ...chunkBuildOptions.banner, js: ';(async () => {' + (chunkBuildOptions.banner?.js ?? '') },
122
- footer: { ...chunkBuildOptions.footer, js: (chunkBuildOptions.footer?.js ?? '') + '})();' },
123
- outfile: chunkPath,
124
- entryPoints: [chunkPath],
125
- bundle: true,
126
- allowOverwrite: true,
127
- });
128
- }
129
- catch (err) {
130
- return;
131
- }
132
- // Errors? -> Ignore, esbuild will show errors in console
133
- if (result.errors.length > 0)
134
- return;
135
- // Log successful build
136
- const { size } = await $fs.stat(chunkPath);
137
- const _dist_ = $chalk.dim(`${this.dist}/`);
138
- const _fileName_ = $chalk.cyan(chunk.fileName);
139
- const _rebundle_ = $chalk.dim.cyan('[rebundle]');
140
- const _size_ = $chalk.bold.dim(`${filesize(size)}`);
141
- console.log(`${_dist_}${_fileName_} ${_rebundle_} ${_size_}`);
142
- // Save code
143
- const code = await this.read(chunk.fileName);
144
- if (!code)
145
- throw this.never;
146
- this.rebundledContent[chunk.fileName] = code;
147
- // Save sourcemap
148
- if (chunk.sourcemapFileName) {
149
- const sourcemap = await this.read(chunk.sourcemapFileName);
150
- if (sourcemap)
151
- this.rebundledContent[chunk.sourcemapFileName] = sourcemap;
152
- }
153
- return true;
154
- }
155
- async removeChunk(chunk, bundle) {
156
- await this.remove(chunk.fileName);
157
- delete bundle[chunk.fileName];
158
- if (chunk.sourcemapFileName) {
159
- await this.remove(chunk.sourcemapFileName);
160
- delete bundle[chunk.sourcemapFileName];
161
- }
162
- // Recursively remove containing directory if empty
163
- const dir = $path.dirname(this.resolve(chunk.fileName));
164
- await this.removeDirectoryIfEmpty(dir);
165
- }
166
- async readChunkFiles(chunk) {
167
- const files = {};
168
- const usedPaths = [chunk.fileName, ...chunk.imports];
169
- await Promise.all(usedPaths.map(async (path) => {
170
- const content = await this.read(path);
171
- if (!content)
172
- return;
173
- files[path] = content;
174
- }));
175
- return files;
176
- }
177
- // ---------------------------------------------------------------------------
178
- // HELPERS
179
- // ---------------------------------------------------------------------------
180
- get dist() {
181
- if (!this.config)
182
- throw this.never;
183
- return this.config.build.outDir;
184
- }
185
- resolve(path) {
186
- return $path.join(this.dist, path);
187
- }
188
- async read(path) {
189
- const [content] = await $utils.safe($fs.readFile(this.resolve(path), 'utf-8'));
190
- return content;
191
- }
192
- async write(path, content) {
193
- await $fs.writeFile(this.resolve(path), content, 'utf-8');
194
- }
195
- async remove(path) {
196
- await $utils.safe($fs.unlink(this.resolve(path)));
197
- }
198
- async ensureWs() {
199
- if (this.ws)
200
- return this.ws;
201
- if (!this.port)
202
- throw this.never;
203
- this.ws = new $ws.WebSocketServer({ port: this.port });
204
- return this.ws;
205
- }
206
- async removeDirectoryIfEmpty(dir) {
207
- const files = await $fs.readdir(dir);
208
- if (files.length > 0)
209
- return;
210
- await $fs.rmdir(dir);
211
- await this.removeDirectoryIfEmpty($path.dirname(dir));
212
- }
213
- }