|
| 1 | +import fs from 'node:fs' |
| 2 | +import path from 'node:path' |
| 3 | +import { |
| 4 | + type Plugin, |
| 5 | + type UserConfig, |
| 6 | + normalizePath, |
| 7 | +} from 'vite' |
| 8 | +import type { Configuration } from 'webpack' |
| 9 | +import { COLOURS } from 'vite-plugin-utils/function' |
| 10 | +import { |
| 11 | + createCjs, |
| 12 | + cjs2esm, |
| 13 | +} from './utils' |
| 14 | + |
| 15 | +export interface BundledRecord { |
| 16 | + name: string |
| 17 | + code: string |
| 18 | + filename: string |
| 19 | +} |
| 20 | + |
| 21 | +export interface PrebundleOptions { |
| 22 | + /** An array of module names that need to be pre-bundle. */ |
| 23 | + modules: string[] |
| 24 | + config?: (config: Configuration) => Configuration | undefined | Promise<Configuration | undefined> |
| 25 | +} |
| 26 | + |
| 27 | +const cjs = createCjs(import.meta.url) |
| 28 | +const TAG = '[vite-plugin-webpack-prebundle]' |
| 29 | +// `nativesMap` is placed in the global scope and can be effective for multiple builds. |
| 30 | +const bundledMap = new Map<string, BundledRecord> |
| 31 | +const IDPrefix = '\0webpack-prebundle' |
| 32 | +let output: string |
| 33 | + |
| 34 | +export default function native(options: PrebundleOptions): Plugin { |
| 35 | + return { |
| 36 | + name: 'vite-plugin-webpack-prebundle', |
| 37 | + enforce: 'pre', |
| 38 | + configResolved(config) { |
| 39 | + // Use the `build.outDir` for those C/C++ addons assets. |
| 40 | + output = normalizePath(path.join(config.root, config.build.outDir)) |
| 41 | + }, |
| 42 | + async resolveId(source) { |
| 43 | + if (options.modules.includes(source)) { |
| 44 | + const bundled = bundledMap.get(source) |
| 45 | + if (!bundled) { |
| 46 | + try { |
| 47 | + await webpackBundle(source, output, options.config) |
| 48 | + const filename = path.posix.join(output, source + '.js') |
| 49 | + const code = cjs2esm( |
| 50 | + // After bundle, it must be a cjs module. |
| 51 | + filename, |
| 52 | + fs.readFileSync(filename, 'utf8'), |
| 53 | + ) |
| 54 | + |
| 55 | + fs.writeFileSync(filename, code) |
| 56 | + bundledMap.set(source, { |
| 57 | + name: source, |
| 58 | + code, |
| 59 | + filename, |
| 60 | + }) |
| 61 | + } catch (error: any) { |
| 62 | + console.error(`\n${TAG}`, error) |
| 63 | + process.exit(1) |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + return IDPrefix + source |
| 68 | + } |
| 69 | + }, |
| 70 | + load(id) { |
| 71 | + if (id.startsWith(IDPrefix)) { |
| 72 | + const name = id.replace(IDPrefix, '') |
| 73 | + const bundled = bundledMap.get(name) |
| 74 | + if (bundled) { |
| 75 | + return bundled.code |
| 76 | + } |
| 77 | + } |
| 78 | + }, |
| 79 | + closeBundle() { |
| 80 | + if (!(process.env.DEBUG || process.env.NODE_ENV === 'test')) { |
| 81 | + for (const [, bundled] of bundledMap) { |
| 82 | + // Remove unused bundle files |
| 83 | + fs.unlinkSync(bundled.filename) |
| 84 | + fs.unlinkSync(bundled.filename + '.map') |
| 85 | + } |
| 86 | + } |
| 87 | + }, |
| 88 | + async config(config) { |
| 89 | + modifyCommonjs(config, options.modules) |
| 90 | + // Run build are not necessary. |
| 91 | + modifyOptimizeDeps(config, options.modules) |
| 92 | + }, |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +function modifyCommonjs(config: UserConfig, modules: string[]) { |
| 97 | + config.build ??= {} |
| 98 | + config.build.commonjsOptions ??= {} |
| 99 | + if (config.build.commonjsOptions.ignore) { |
| 100 | + if (typeof config.build.commonjsOptions.ignore === 'function') { |
| 101 | + const userIgnore = config.build.commonjsOptions.ignore |
| 102 | + config.build.commonjsOptions.ignore = id => { |
| 103 | + if (userIgnore?.(id) === true) { |
| 104 | + return true |
| 105 | + } |
| 106 | + return modules.includes(id) |
| 107 | + } |
| 108 | + } else { |
| 109 | + // @ts-ignore |
| 110 | + config.build.commonjsOptions.ignore.push(...modules) |
| 111 | + } |
| 112 | + } else { |
| 113 | + config.build.commonjsOptions.ignore = modules |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +function modifyOptimizeDeps(config: UserConfig, exclude: string[]) { |
| 118 | + config.optimizeDeps ??= {} |
| 119 | + config.optimizeDeps.exclude ??= [] |
| 120 | + for (const str of exclude) { |
| 121 | + if (!config.optimizeDeps.exclude.includes(str)) { |
| 122 | + // Avoid Vite secondary pre-bundle |
| 123 | + config.optimizeDeps.exclude.push(str) |
| 124 | + } |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +async function webpackBundle( |
| 129 | + name: string, |
| 130 | + output: string, |
| 131 | + webpackConfig: PrebundleOptions['config'] |
| 132 | +) { |
| 133 | + const { validate, webpack } = cjs.require('webpack') as typeof import('webpack') |
| 134 | + |
| 135 | + return new Promise<null>(async (resolve, reject) => { |
| 136 | + let options: Configuration = { |
| 137 | + mode: 'none', |
| 138 | + target: 'node14', |
| 139 | + entry: { [name]: name }, |
| 140 | + output: { |
| 141 | + library: { |
| 142 | + type: 'commonjs2', |
| 143 | + }, |
| 144 | + path: output, |
| 145 | + filename: '[name].js', |
| 146 | + }, |
| 147 | + module: { |
| 148 | + rules: [ |
| 149 | + // TODO: Add some commonly used loaders. |
| 150 | + ], |
| 151 | + }, |
| 152 | + devtool: 'source-map', |
| 153 | + } |
| 154 | + |
| 155 | + if (webpackConfig) { |
| 156 | + options = await webpackConfig(options) ?? options |
| 157 | + } |
| 158 | + |
| 159 | + try { |
| 160 | + validate(options) |
| 161 | + } catch (error: any) { |
| 162 | + reject(COLOURS.red(error.message)) |
| 163 | + return |
| 164 | + } |
| 165 | + |
| 166 | + webpack(options).run((error, stats) => { |
| 167 | + if (error) { |
| 168 | + reject(error) |
| 169 | + return |
| 170 | + } |
| 171 | + |
| 172 | + if (stats?.hasErrors()) { |
| 173 | + const errorMsg = stats.toJson().errors?.map(msg => msg.message).join('\n') |
| 174 | + |
| 175 | + if (errorMsg) { |
| 176 | + reject(COLOURS.red(errorMsg)) |
| 177 | + return |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + console.log(`${TAG}`, name, COLOURS.green('build success')) |
| 182 | + resolve(null) |
| 183 | + }) |
| 184 | + }) |
| 185 | +} |
0 commit comments