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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/rebundle.d.ts +25 -0
- package/dist/rebundle.js +162 -0
- package/package.json +30 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
}
|
package/dist/rebundle.js
ADDED
|
@@ -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
|
+
}
|