UNPKG

@runforest/js

Version:

Javascript.

374 lines (321 loc) 9.8 kB
const path = require('path'); const chalk = require('chalk'); // Fix rollup watcher chokidar path issue. const tmpCWD = process.cwd(); process.chdir(__dirname); // Require rollup. const rollup = require('rollup'); // Revert rollup watcher chokidar path issue fix. process.chdir(tmpCWD); // Rollup dependencies. const { nodeResolve } = require('@rollup/plugin-node-resolve'); /** @type {import('@rollup/plugin-commonjs')['default']} */ // @ts-ignore const rollupCommonJS = require('@rollup/plugin-commonjs'); const { babel } = require('@rollup/plugin-babel'); /** * @type {import('@rollup/plugin-alias')['default']} */ // @ts-ignore const rollupAlias = require('@rollup/plugin-alias'); /** * @type {import('@rollup/plugin-replace')['default']} */ // @ts-ignore const rollupReplace = require('@rollup/plugin-replace'); const Observable = require('@runforest/run/utils/observable'); const minify = require('./rollup-minify'); /** * @typedef {{ * source: string; * destination: string; * globals?: Record<string, string>; * external?: string[]; * compress: boolean; * babelExclude?: (string|RegExp)[]; * useTypescript?: boolean; * mode: 'build' | 'watch'; * pluginOptions: { commonJS?: any } * typescriptOptions?: { tsconfig?: string | false } * }} ValidatedConfig * @typedef {Partial<Omit<ValidatedConfig, 'mode' | 'babelExclude'> * & { mode: string; babelExclude: string[]; }>} Config */ /** * Returns current local time string. * * @returns {String} */ function currentLocalTime() { return new Date().toLocaleTimeString(); } function logPrefix() { return chalk`{gray ${currentLocalTime()}} {bold [JS]}`; } /** * @param {string | Error | import('rollup').RollupError | Error & { formatted?: string }} error * @returns {string} */ function formatError(error) { if (typeof error === 'string') { return error; } if ('formatted' in error && error.formatted) { return chalk`{red ${error.formatted}}`; } if (error.message) { return chalk`{red ${error.message}}`; } return error.toString(); } /** * @param {Config} options * @returns {Promise<ValidatedConfig>} */ async function validate(options) { const { source, destination, globals, external, compress = true, babelExclude, mode = 'build', useTypescript = false, pluginOptions = {}, typescriptOptions, } = options; if (!source) { throw chalk`${logPrefix()} {red Source is required!}`; } if (!destination) { throw chalk`${logPrefix()} {red Destination is required!}`; } return { source, destination, globals, external, mode: mode !== 'build' ? 'watch' : 'build', compress, babelExclude, useTypescript, pluginOptions, typescriptOptions, }; } /** * @type {import('@rollup/plugin-typescript')['default'] | undefined} */ let cachedRollupTypescript; /** * @return {import('@rollup/plugin-typescript')['default']} */ function getRollupTypescript() { if (!cachedRollupTypescript) { // @ts-ignore // eslint-disable-next-line global-require cachedRollupTypescript = require('@rollup/plugin-typescript'); } // @ts-ignore return cachedRollupTypescript; } /** * @param {ValidatedConfig} config * @returns {(import('rollup').Plugin)[]} */ function getPluginsOption({ source, compress, babelExclude, useTypescript, pluginOptions, typescriptOptions }) { /** * @type {(import('rollup').Plugin)[]} */ const plugins = []; if (useTypescript) { plugins.push(getRollupTypescript()(typescriptOptions)); } plugins.push( rollupAlias({ entries: [{ find: 'core-js', replacement: path.join(__dirname, 'node_modules', 'core-js') }], }) ); // Use the Node.js resolution algorithm with Rollup plugins.push( nodeResolve({ mainFields: ['module', 'browser', 'main'], }) ); const defaultCommonJSOptions = { // Convert only imported npm modules include: /node_modules/, }; // Convert CommonJS modules to ES2015 plugins.push( rollupCommonJS( pluginOptions.commonJS ? Object.assign(defaultCommonJSOptions, pluginOptions.commonJS) : defaultCommonJSOptions ) ); plugins.push( rollupReplace({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ? process.env.NODE_ENV : 'production'), preventAssignment: true, }) ); const exclude = [/\/core-js\//].concat( Array.isArray(babelExclude) ? babelExclude.filter((rule) => typeof rule === 'string').map((rule) => new RegExp(rule)) : [] ); // Babel is a toolchain that is mainly used to convert ECMAScript 2015+ // code into a backwards compatible version of JavaScript in current // and older browsers or environments plugins.push( babel({ exclude, babelHelpers: 'bundled', presets: [ [ // @babel/preset-env is a smart preset that allows you to use // the latest JavaScript without needing to micromanage which // syntax transforms (and optionally, browser polyfills) are // needed by your target environment(s). // @ts-ignore // eslint-disable-next-line global-require require('@babel/preset-env'), { // The starting point where the config search for // browserslist will start, and ascend to the system root // until found configPath: source, // Disable transformation of ES6 module syntax because that // is what rollup do modules: false, // @babel/polyfill insertion is handled by the plugin above useBuiltIns: 'usage', corejs: 3, }, ], ], }) ); if (compress) { // ECMAScript 2015+ minifier plugins.push(minify()); } return plugins; } /** * @param {ValidatedConfig} config * @returns {import('rollup').InputOptions} */ function getInputOptions(config) { const { source, external } = config; return { input: source, external, plugins: getPluginsOption(config), }; } /** * @param {ValidatedConfig['source']} source * @param {ValidatedConfig['destination']} destination * @param {ValidatedConfig['globals']} globals * @returns {import('rollup').OutputOptions} */ function getOutputOptions(source, destination, globals) { let filename = path.basename(source); // check if the source filename ends with .ts and replace it with .js if (filename.substring(filename.length - 3).toLowerCase() === '.ts') { filename = filename.replace(/\.ts$/i, '.js'); } return { format: 'iife', file: path.resolve(destination, filename), globals, sourcemap: true, }; } /** * @param {Config} config */ module.exports = function jsTask(config) { return new Observable((observer) => { validate(config) .then( (validatedConfig) => new Promise((resolve, reject) => { const { mode } = validatedConfig; if (mode !== 'watch') { resolve(validatedConfig); return; } const { source, destination, globals } = validatedConfig; const watcher = rollup.watch({ ...getInputOptions(validatedConfig), output: getOutputOptions(source, destination, globals), watch: { exclude: 'node_modules/**', }, }); /** * @type {NodeJS.Timeout} */ let waitingMessageTimeout; watcher.on('event', (event) => { clearTimeout(waitingMessageTimeout); switch (event.code) { case 'START': observer.next('Start compiling...'); break; case 'BUNDLE_START': observer.next(chalk`Compiling {blue ${event.input}}...`); break; case 'BUNDLE_END': // eslint-disable-next-line no-case-declarations const destinations = event.output.map((dest) => path.relative(process.cwd(), dest)).join(', '); observer.next(chalk`{blue ${event.input}} is compiled to {green ${destinations}}.`); break; case 'END': waitingMessageTimeout = setTimeout(() => { observer.next('Waiting...'); }, 2000); break; case 'ERROR': if (event.error) { observer.next(formatError(event.error)); } else { observer.next( // @ts-ignore chalk`{red Unknown error!}\n` + formatError(event) ); } break; default: watcher.close(); // @ts-ignore if (event.error) { // @ts-ignore reject(event.error); } else { reject(new Error(chalk`{red Unknown unrecoverable error!}\n` + formatError(event))); } break; } }); }) ) .then(async (validatedConfig) => { const { mode } = validatedConfig; if (mode !== 'build') { return validatedConfig; } const { source, destination, globals } = validatedConfig; const bundle = await rollup.rollup(getInputOptions(validatedConfig)); return bundle.write(getOutputOptions(source, destination, globals)); }) .then(() => { observer.complete(); }) .catch((error) => { observer.error(error); }); }); };