UNPKG

gulp-rollup-2

Version:

Modern Gulp plugin for Rollup 4.x - Bundle JavaScript modules with tree-shaking, multiple output formats (UMD, ESM, CJS, IIFE), and full Rollup API support. Actively maintained.

245 lines (198 loc) 7.42 kB
/** * gulp-rollup-2 — Production-grade, Rollup 4+ compatible * Cache-safe + sourcemap path fix integrated */ const path = require('path'); const Vinyl = require('vinyl'); const applySourceMap = require('vinyl-sourcemaps-apply'); const through2 = require('through2'); const rollup = require('rollup'); const hash = require('object-hash'); const PLUGIN_NAME = 'gulp-rollup-2'; const CACHE = new Map(); // --- helpers --- const isArray = Array.isArray; const uniq = (arr) => [...new Set(arr)]; const deepEqual = (a, b) => { if (a === b) return true; if (a == null || b == null) return a === b; if (typeof a !== 'object' || typeof b !== 'object') return a === b; const keysA = Object.keys(a); if (keysA.length !== Object.keys(b).length) return false; for (const key of keysA) { if (!Object.hasOwn(b, key) || !deepEqual(a[key], b[key])) return false; } return true; }; const validFormats = ['es', 'amd', 'cjs', 'iife', 'umd', 'system']; const isValidFormat = (fmt) => validFormats.includes(fmt); const sanitizeConfig = (configs, requireInput = false) => { if (!configs) throw new Error(`${PLUGIN_NAME}: Missing Rollup config`); if (typeof configs === 'string') { configs = [{ output: { format: configs } }]; } if (!isArray(configs)) configs = [configs]; const result = []; for (const cfg of configs) { if (requireInput && !cfg.input) { throw new Error(`${PLUGIN_NAME}: 'input' option is required in src() mode`); } if (!cfg.output) throw new Error(`${PLUGIN_NAME}: 'output' is required`); const inputOpts = { ...cfg }; const outputs = isArray(cfg.output) ? cfg.output : [cfg.output]; delete inputOpts.output; for (const out of outputs) { if (!out.file || !out.format || !isValidFormat(out.format)) { throw new Error(`${PLUGIN_NAME}: Output must have 'file' and valid 'format'`); } } result.push({ input: inputOpts, outputs }); } // dedupe inputs for (let i = 0; i < result.length; i++) { for (let j = i + 1; j < result.length; j++) { if (deepEqual(result[i].input, result[j].input)) { throw new Error(`${PLUGIN_NAME}: Duplicate input configurations`); } } } // dedupe output files const files = result.flatMap((r) => r.outputs.map((o) => o.file)); if (files.length !== uniq(files).length) { throw new Error(`${PLUGIN_NAME}: Multiple outputs target the same file`); } return result; }; // --- cache key (plugins intentionally excluded) --- const createCacheKey = (input, outputs) => hash({ input: input.input, external: input.external, treeshake: input.treeshake, outputs: outputs.map((o) => o.file), }); // --- inside (gulp src pipe) --- const inside = (rollupConfigs) => { const configs = sanitizeConfig(rollupConfigs); return through2.obj(async function (file, enc, cb) { if (file.isStream()) { return cb(new Error(`${PLUGIN_NAME}: Streaming not supported`)); } const cwd = file.cwd; const inputPath = path.relative(cwd, file.path); try { const bundles = await Promise.all( configs.map(async ({ input, outputs }) => { const rollupInput = { ...input, input: input.input ? path.resolve(cwd, input.input) : file.path, }; const cacheKey = createCacheKey(rollupInput, outputs); rollupInput.cache = CACHE.get(cacheKey) ?? false; const bundle = await rollup.rollup(rollupInput); CACHE.set(cacheKey, bundle.cache); return { bundle, outputs }; }) ); for (const { bundle, outputs } of bundles) { for (const outputOpts of outputs) { if (['umd', 'iife'].includes(outputOpts.format) && !outputOpts.name) { outputOpts.name = path.basename(inputPath, path.extname(inputPath)); } if ( ['umd', 'amd'].includes(outputOpts.format) && (!outputOpts.amd || !outputOpts.amd.id) ) { outputOpts.amd = { ...(outputOpts.amd || {}), id: outputOpts.name }; } const { output } = await bundle.generate(outputOpts); for (const out of output) { if (out.type === 'asset') continue; const code = out.code ?? out.source; if (!code) continue; const outFile = new Vinyl({ cwd, base: cwd, path: path.resolve(cwd, outputOpts.file), contents: Buffer.from(code), }); if (out.map) { applySourceMap(outFile, out.map); } else if (file.sourceMap) { const sm = JSON.parse(JSON.stringify(file.sourceMap)); sm.file = path.basename(outputOpts.file); sm.sources = sm.sources.map((s) => path.relative(outFile.base, path.resolve(file.base, s)) ); applySourceMap(outFile, sm); } this.push(outFile); } } await bundle.close(); } cb(); } catch (err) { cb(err); } }); }; // --- outside (no njfs.root, caller controls cwd) --- const outside = async (rollupConfigs) => { const configs = sanitizeConfig(rollupConfigs, true); const stream = through2.obj(); const cwd = process.cwd(); (async () => { try { const bundles = await Promise.all( configs.map(async ({ input, outputs }) => { const rollupInput = { ...input, input: path.resolve(cwd, input.input), }; const cacheKey = createCacheKey(rollupInput, outputs); rollupInput.cache = CACHE.get(cacheKey) ?? false; const bundle = await rollup.rollup(rollupInput); CACHE.set(cacheKey, bundle.cache); return { bundle, outputs }; }) ); for (const { bundle, outputs } of bundles) { for (const outputOpts of outputs) { if (['umd', 'iife'].includes(outputOpts.format) && !outputOpts.name) { outputOpts.name = path.basename(outputOpts.file, path.extname(outputOpts.file)); } if ( ['umd', 'amd'].includes(outputOpts.format) && (!outputOpts.amd || !outputOpts.amd.id) ) { outputOpts.amd = { ...(outputOpts.amd || {}), id: outputOpts.name }; } const { output } = await bundle.generate(outputOpts); for (const out of output) { if (out.type === 'asset') continue; const code = out.code ?? out.source; if (!code) continue; const outFile = new Vinyl({ cwd, base: path.dirname(path.resolve(cwd, outputOpts.file)), path: path.resolve(cwd, outputOpts.file), contents: Buffer.from(code), }); if (out.map) applySourceMap(outFile, out.map); stream.push(outFile); } } await bundle.close(); } stream.push(null); } catch (err) { stream.emit('error', err); } })(); return stream; }; module.exports = { rollup: inside, src: outside, };