@runforest/js
Version:
Javascript.
374 lines (321 loc) • 9.8 kB
JavaScript
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);
});
});
};