vite-plugin-rebundle 1.15.0 → 1.17.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.
@@ -13,18 +13,22 @@ declare class Rebundle {
13
13
  private bundleOptions;
14
14
  private config;
15
15
  private originals;
16
- private rebundled;
17
16
  private port;
18
17
  private ws;
18
+ private isRollupVite;
19
+ private isRolldownVite;
20
+ private ORIGINALS_DIR;
19
21
  constructor(commonOptions?: RolldownOptions | null, bundleOptions?: BundleOptions);
20
22
  get vite(): Plugin;
21
23
  private onConfig;
22
24
  private onConfigResolved;
25
+ private onGenerateBundle;
23
26
  private onWriteBundle;
24
27
  private get dist();
25
- private removeFromDist;
26
- private ensureWs;
27
- private removeDirectoryIfEmpty;
28
+ private prefixed;
29
+ private unprefixed;
30
+ private getChunks;
31
+ private getEntryChunks;
28
32
  private merge;
29
33
  }
30
34
  declare function rebundle(commonOptions?: RolldownOptions | null, bundleOptions?: BundleOptions): Plugin<any>;
package/dist/rebundle.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // src/rebundle.ts
2
- import { is, safe } from "@eposlabs/utils";
2
+ import { is } from "@eposlabs/utils";
3
3
  import chalk from "chalk";
4
4
  import { filesize } from "filesize";
5
- import { readdir, rmdir, stat, unlink, writeFile } from "fs/promises";
6
- import { dirname, extname, join } from "path";
5
+ import { rm, stat } from "fs/promises";
6
+ import { extname, join } from "path";
7
7
  import { getPort } from "portfinder";
8
8
  import { rolldown } from "rolldown";
9
9
  import { WebSocketServer } from "ws";
@@ -12,9 +12,11 @@ var Rebundle = class {
12
12
  bundleOptions;
13
13
  config = null;
14
14
  originals = {};
15
- rebundled = {};
16
15
  port = null;
17
16
  ws = null;
17
+ isRollupVite = false;
18
+ isRolldownVite = false;
19
+ ORIGINALS_DIR = "__originals__";
18
20
  constructor(commonOptions, bundleOptions) {
19
21
  this.commonOptions = commonOptions ?? {};
20
22
  this.bundleOptions = bundleOptions ?? {};
@@ -26,74 +28,81 @@ var Rebundle = class {
26
28
  enforce: "post",
27
29
  config: this.onConfig,
28
30
  configResolved: this.onConfigResolved,
31
+ generateBundle: this.onGenerateBundle,
29
32
  writeBundle: this.onWriteBundle
30
33
  };
31
34
  }
32
35
  // ---------------------------------------------------------------------------
33
36
  // VITE HOOKS
34
37
  // ---------------------------------------------------------------------------
35
- onConfig = async () => {
36
- this.port = await getPort({ port: 3100 });
38
+ onConfig = async (config) => {
39
+ if (config.build?.watch) {
40
+ this.port = await getPort({ port: 3100 });
41
+ this.ws = new WebSocketServer({ port: this.port });
42
+ }
37
43
  return {
38
44
  define: { "import.meta.env.REBUNDLE_PORT": JSON.stringify(this.port) },
39
45
  build: { sourcemap: false }
40
46
  };
41
47
  };
42
48
  onConfigResolved = async (config) => {
49
+ this.isRollupVite = !("oxc" in config);
50
+ this.isRolldownVite = !this.isRollupVite;
43
51
  this.config = config;
44
- const info = this.config.logger.info;
45
- this.config.logger.info = (message, options) => {
46
- const path = message.split(/\s+/)[0];
47
- if (extname(path) === ".js") return;
48
- info(message, options);
49
- };
52
+ if (this.isRollupVite) {
53
+ const info = this.config.logger.info;
54
+ this.config.logger.info = (message, options) => {
55
+ const path = message.split(/\s+/)[0];
56
+ if (extname(path) === ".js") return;
57
+ info(message, options);
58
+ };
59
+ }
60
+ };
61
+ onGenerateBundle = async (_options, bundle) => {
62
+ for (const chunk of this.getChunks(bundle)) {
63
+ const originalFileName = chunk.fileName;
64
+ chunk.fileName = this.prefixed(originalFileName);
65
+ chunk.imports = chunk.imports.map((name) => this.prefixed(name));
66
+ if (this.isRollupVite) {
67
+ bundle[chunk.fileName] = chunk;
68
+ delete bundle[originalFileName];
69
+ }
70
+ }
50
71
  };
51
72
  onWriteBundle = async (_output, bundle) => {
52
- const modifiedChunkNames = [];
73
+ const modifiedEntryChunks = this.getEntryChunks(bundle).filter((chunk) => {
74
+ const usedPaths = [chunk.fileName, ...chunk.imports];
75
+ return usedPaths.some((path) => "code" in bundle[path] && bundle[path].code !== this.originals[path]);
76
+ });
77
+ if (modifiedEntryChunks.length === 0) return;
53
78
  await Promise.all(
54
- Object.values(bundle).map(async (chunkOrAsset) => {
55
- const chunk = chunkOrAsset.type === "chunk" && chunkOrAsset.isEntry ? chunkOrAsset : null;
56
- if (!chunk) return;
57
- const chunkFilePath = join(this.dist, chunk.fileName);
58
- const modified = [chunk.fileName, ...chunk.imports].some((name) => {
59
- return bundle[name].type === "chunk" && bundle[name].code !== this.originals[name];
60
- });
61
- if (!modified) {
62
- await writeFile(chunkFilePath, this.rebundled[chunk.fileName]);
63
- return;
64
- }
79
+ modifiedEntryChunks.map(async (chunk) => {
80
+ const originalFileName = this.unprefixed(chunk.fileName);
65
81
  const build = await rolldown({
66
82
  ...this.merge(this.commonOptions.input ?? {}, this.bundleOptions[chunk.name]?.input ?? {}),
67
- input: chunkFilePath
83
+ input: join(this.dist, chunk.fileName)
68
84
  });
69
- const result = await build.write({
85
+ await build.write({
70
86
  ...this.merge(this.commonOptions.output ?? {}, this.bundleOptions[chunk.name]?.output ?? {}),
71
87
  sourcemap: false,
72
- file: chunkFilePath
88
+ file: join(this.dist, originalFileName)
73
89
  });
74
- const { size } = await stat(join(this.dist, chunk.fileName));
75
- const _dist_ = chalk.dim(`${this.dist}/`);
76
- const _fileName_ = chalk.cyan(chunk.fileName);
77
- const _rebundle_ = chalk.dim.cyan("[rebundle]");
78
- const _size_ = chalk.bold.dim(`${filesize(size)}`);
79
- console.log(`${_dist_}${_fileName_} ${_rebundle_} ${_size_}`);
80
- modifiedChunkNames.push(chunk.name);
81
- this.rebundled[chunk.fileName] = result.output[0].code;
90
+ const { size } = await stat(join(this.dist, originalFileName));
91
+ const $dist = chalk.dim(`${this.dist}/`);
92
+ const $fileName = chalk.cyan(originalFileName);
93
+ const $rebundle = chalk.dim.cyan("[rebundle]");
94
+ const $size = chalk.bold.dim(`${filesize(size)}`);
95
+ console.log(`${$dist}${$fileName} ${$rebundle} ${$size}`);
82
96
  })
83
97
  );
84
- for (const chunkOrAsset of Object.values(bundle)) {
85
- const chunk = chunkOrAsset.type === "chunk" ? chunkOrAsset : null;
86
- if (!chunk) continue;
98
+ for (const chunk of this.getChunks(bundle)) {
87
99
  this.originals[chunk.fileName] = chunk.code;
88
- delete bundle[chunk.fileName];
89
- if (!chunk.isEntry) {
90
- await this.removeFromDist(chunk.fileName);
91
- }
100
+ if (this.isRolldownVite) delete bundle[chunk.fileName];
92
101
  }
93
- if (!this.config) throw "never";
94
- if (this.config.build.watch && modifiedChunkNames.length > 0) {
95
- const ws = await this.ensureWs();
96
- ws.clients.forEach((client) => client.send(JSON.stringify(modifiedChunkNames)));
102
+ await rm(join(this.dist, this.ORIGINALS_DIR), { recursive: true });
103
+ if (this.ws && modifiedEntryChunks.length > 0) {
104
+ const names = modifiedEntryChunks.map((chunk) => chunk.name);
105
+ this.ws.clients.forEach((client) => client.send(JSON.stringify(names)));
97
106
  }
98
107
  };
99
108
  // ---------------------------------------------------------------------------
@@ -103,23 +112,17 @@ var Rebundle = class {
103
112
  if (!this.config) throw "never";
104
113
  return this.config.build.outDir;
105
114
  }
106
- async removeFromDist(path) {
107
- path = join(this.dist, path);
108
- const dir = dirname(path);
109
- await safe(unlink(path));
110
- await this.removeDirectoryIfEmpty(dir);
115
+ prefixed(path) {
116
+ return join(this.ORIGINALS_DIR, path);
117
+ }
118
+ unprefixed(path) {
119
+ return path.replace(`${this.ORIGINALS_DIR}/`, "");
111
120
  }
112
- async ensureWs() {
113
- if (this.ws) return this.ws;
114
- if (!this.port) throw "never";
115
- this.ws = new WebSocketServer({ port: this.port });
116
- return this.ws;
121
+ getChunks(bundle) {
122
+ return Object.values(bundle).filter((item) => item.type === "chunk");
117
123
  }
118
- async removeDirectoryIfEmpty(dir) {
119
- const files = await readdir(dir);
120
- if (files.length > 0) return;
121
- await rmdir(dir);
122
- await this.removeDirectoryIfEmpty(dirname(dir));
124
+ getEntryChunks(bundle) {
125
+ return Object.values(bundle).filter((item) => item.type === "chunk").filter((chunk) => chunk.isEntry);
123
126
  }
124
127
  merge(obj1, obj2) {
125
128
  const result = { ...obj1 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-rebundle",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "imkost",
package/src/rebundle.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { is, safe } from '@eposlabs/utils'
1
+ import { is } from '@eposlabs/utils'
2
2
  import chalk from 'chalk'
3
3
  import { filesize } from 'filesize'
4
- import { readdir, rmdir, stat, unlink, writeFile } from 'node:fs/promises'
5
- import { dirname, extname, join } from 'node:path'
4
+ import { rm, stat } from 'node:fs/promises'
5
+ import { extname, join } from 'node:path'
6
6
  import { getPort } from 'portfinder'
7
7
  import { rolldown, type InputOptions, type OutputOptions } from 'rolldown'
8
8
  import type { NormalizedOutputOptions, OutputBundle } from 'rollup'
9
- import type { Plugin, ResolvedConfig } from 'vite'
9
+ import type { Plugin, ResolvedConfig, UserConfig } from 'vite'
10
10
  import { WebSocketServer } from 'ws'
11
11
 
12
12
  export type RolldownOptions = {
@@ -23,9 +23,11 @@ export class Rebundle {
23
23
  private bundleOptions: BundleOptions
24
24
  private config: ResolvedConfig | null = null
25
25
  private originals: Record<string, string> = {}
26
- private rebundled: Record<string, string> = {}
27
26
  private port: number | null = null
28
27
  private ws: WebSocketServer | null = null
28
+ private isRollupVite = false
29
+ private isRolldownVite = false
30
+ private ORIGINALS_DIR = '__originals__'
29
31
 
30
32
  constructor(commonOptions?: RolldownOptions | null, bundleOptions?: BundleOptions) {
31
33
  this.commonOptions = commonOptions ?? {}
@@ -39,6 +41,7 @@ export class Rebundle {
39
41
  enforce: 'post',
40
42
  config: this.onConfig,
41
43
  configResolved: this.onConfigResolved,
44
+ generateBundle: this.onGenerateBundle,
42
45
  writeBundle: this.onWriteBundle,
43
46
  }
44
47
  }
@@ -47,8 +50,12 @@ export class Rebundle {
47
50
  // VITE HOOKS
48
51
  // ---------------------------------------------------------------------------
49
52
 
50
- private onConfig = async () => {
51
- this.port = await getPort({ port: 3100 })
53
+ private onConfig = async (config: UserConfig) => {
54
+ if (config.build?.watch) {
55
+ this.port = await getPort({ port: 3100 })
56
+ this.ws = new WebSocketServer({ port: this.port })
57
+ }
58
+
52
59
  return {
53
60
  define: { 'import.meta.env.REBUNDLE_PORT': JSON.stringify(this.port) },
54
61
  build: { sourcemap: false },
@@ -56,89 +63,91 @@ export class Rebundle {
56
63
  }
57
64
 
58
65
  private onConfigResolved = async (config: ResolvedConfig) => {
66
+ // Detect Vite variant
67
+ this.isRollupVite = !('oxc' in config)
68
+ this.isRolldownVite = !this.isRollupVite
69
+
59
70
  // Save resolved config
60
71
  this.config = config
61
72
 
62
- // Hide js files from output logs (rollup only, not supported in rolldown)
63
- const info = this.config.logger.info
64
- this.config.logger.info = (message, options) => {
65
- const path = message.split(/\s+/)[0]
66
- if (extname(path) === '.js') return
67
- info(message, options)
73
+ // Hide js files from output logs for rollup Vite
74
+ if (this.isRollupVite) {
75
+ const info = this.config.logger.info
76
+ this.config.logger.info = (message, options) => {
77
+ const path = message.split(/\s+/)[0]
78
+ if (extname(path) === '.js') return
79
+ info(message, options)
80
+ }
81
+ }
82
+ }
83
+
84
+ private onGenerateBundle = async (_options: NormalizedOutputOptions, bundle: OutputBundle) => {
85
+ for (const chunk of this.getChunks(bundle)) {
86
+ const originalFileName = chunk.fileName
87
+
88
+ // Move all chunks to a temporary subfolder
89
+ chunk.fileName = this.prefixed(originalFileName)
90
+ chunk.imports = chunk.imports.map(name => this.prefixed(name))
91
+
92
+ // Use prefixed names as bundle keys for rollup Vite (rolldown Vite does this automatically)
93
+ if (this.isRollupVite) {
94
+ bundle[chunk.fileName] = chunk
95
+ delete bundle[originalFileName]
96
+ }
68
97
  }
69
98
  }
70
99
 
71
100
  private onWriteBundle = async (_output: NormalizedOutputOptions, bundle: OutputBundle) => {
72
- const modifiedChunkNames: string[] = []
101
+ // Get modified entry chunks
102
+ const modifiedEntryChunks = this.getEntryChunks(bundle).filter(chunk => {
103
+ const usedPaths = [chunk.fileName, ...chunk.imports]
104
+ return usedPaths.some(path => 'code' in bundle[path] && bundle[path].code !== this.originals[path])
105
+ })
73
106
 
74
- // Rebundle entry chunks
75
- await Promise.all(
76
- Object.values(bundle).map(async chunkOrAsset => {
77
- // Only process entry chunks
78
- const chunk = chunkOrAsset.type === 'chunk' && chunkOrAsset.isEntry ? chunkOrAsset : null
79
- if (!chunk) return
80
- const chunkFilePath = join(this.dist, chunk.fileName)
81
-
82
- // Check if chunk is modified
83
- const modified = [chunk.fileName, ...chunk.imports].some(name => {
84
- return bundle[name].type === 'chunk' && bundle[name].code !== this.originals[name]
85
- })
107
+ // No modified entry chunks? -> Skip rebundle
108
+ if (modifiedEntryChunks.length === 0) return
86
109
 
87
- // Not modified? -> Overwrite Vite's output with cached rebundled content
88
- if (!modified) {
89
- await writeFile(chunkFilePath, this.rebundled[chunk.fileName])
90
- return
91
- }
110
+ // Rebundle modified entry chunks
111
+ await Promise.all(
112
+ modifiedEntryChunks.map(async chunk => {
113
+ const originalFileName = this.unprefixed(chunk.fileName)
92
114
 
93
- // Modified? -> Rebundle with rolldown
115
+ // Build with rolldown
94
116
  const build = await rolldown({
95
117
  ...this.merge(this.commonOptions.input ?? {}, this.bundleOptions[chunk.name]?.input ?? {}),
96
- input: chunkFilePath,
118
+ input: join(this.dist, chunk.fileName),
97
119
  })
98
- const result = await build.write({
120
+ await build.write({
99
121
  ...this.merge(this.commonOptions.output ?? {}, this.bundleOptions[chunk.name]?.output ?? {}),
100
122
  sourcemap: false,
101
- file: chunkFilePath,
123
+ file: join(this.dist, originalFileName),
102
124
  })
103
125
 
104
126
  // Log successful build
105
- const { size } = await stat(join(this.dist, chunk.fileName))
106
- const _dist_ = chalk.dim(`${this.dist}/`)
107
- const _fileName_ = chalk.cyan(chunk.fileName)
108
- const _rebundle_ = chalk.dim.cyan('[rebundle]')
109
- const _size_ = chalk.bold.dim(`${filesize(size)}`)
110
- console.log(`${_dist_}${_fileName_} ${_rebundle_} ${_size_}`)
111
-
112
- // Keep track of modified chunks
113
- modifiedChunkNames.push(chunk.name)
114
-
115
- // Cache rebundled code
116
- this.rebundled[chunk.fileName] = result.output[0].code
127
+ const { size } = await stat(join(this.dist, originalFileName))
128
+ const $dist = chalk.dim(`${this.dist}/`)
129
+ const $fileName = chalk.cyan(originalFileName)
130
+ const $rebundle = chalk.dim.cyan('[rebundle]')
131
+ const $size = chalk.bold.dim(`${filesize(size)}`)
132
+ console.log(`${$dist}${$fileName} ${$rebundle} ${$size}`)
117
133
  }),
118
134
  )
119
135
 
120
- for (const chunkOrAsset of Object.values(bundle)) {
121
- // Process chunks only
122
- const chunk = chunkOrAsset.type === 'chunk' ? chunkOrAsset : null
123
- if (!chunk) continue
124
-
136
+ for (const chunk of this.getChunks(bundle)) {
125
137
  // Save original chunk code
126
138
  this.originals[chunk.fileName] = chunk.code
127
139
 
128
- // Delete chunk from the `bundle` to hide log for `rolldown-vite`. Call for `rollup` for consistency.
129
- delete bundle[chunk.fileName]
130
-
131
- // Non-entry chunk? -> Remove its file
132
- if (!chunk.isEntry) {
133
- await this.removeFromDist(chunk.fileName)
134
- }
140
+ // Delete chunk from the bundle to hide Vite's output log
141
+ if (this.isRolldownVite) delete bundle[chunk.fileName]
135
142
  }
136
143
 
144
+ // Remove folder with original chunks
145
+ await rm(join(this.dist, this.ORIGINALS_DIR), { recursive: true })
146
+
137
147
  // Notify about modified chunks
138
- if (!this.config) throw 'never'
139
- if (this.config.build.watch && modifiedChunkNames.length > 0) {
140
- const ws = await this.ensureWs()
141
- ws.clients.forEach(client => client.send(JSON.stringify(modifiedChunkNames)))
148
+ if (this.ws && modifiedEntryChunks.length > 0) {
149
+ const names = modifiedEntryChunks.map(chunk => chunk.name)
150
+ this.ws.clients.forEach(client => client.send(JSON.stringify(names)))
142
151
  }
143
152
  }
144
153
 
@@ -151,25 +160,22 @@ export class Rebundle {
151
160
  return this.config.build.outDir
152
161
  }
153
162
 
154
- private async removeFromDist(path: string) {
155
- path = join(this.dist, path)
156
- const dir = dirname(path)
157
- await safe(unlink(path))
158
- await this.removeDirectoryIfEmpty(dir)
163
+ private prefixed(path: string) {
164
+ return join(this.ORIGINALS_DIR, path)
165
+ }
166
+
167
+ private unprefixed(path: string) {
168
+ return path.replace(`${this.ORIGINALS_DIR}/`, '')
159
169
  }
160
170
 
161
- private async ensureWs() {
162
- if (this.ws) return this.ws
163
- if (!this.port) throw 'never'
164
- this.ws = new WebSocketServer({ port: this.port })
165
- return this.ws
171
+ private getChunks(bundle: OutputBundle) {
172
+ return Object.values(bundle).filter(item => item.type === 'chunk')
166
173
  }
167
174
 
168
- private async removeDirectoryIfEmpty(dir: string) {
169
- const files = await readdir(dir)
170
- if (files.length > 0) return
171
- await rmdir(dir)
172
- await this.removeDirectoryIfEmpty(dirname(dir))
175
+ private getEntryChunks(bundle: OutputBundle) {
176
+ return Object.values(bundle)
177
+ .filter(item => item.type === 'chunk')
178
+ .filter(chunk => chunk.isEntry)
173
179
  }
174
180
 
175
181
  private merge(obj1: Record<string, any>, obj2: Record<string, any>) {