blob: ff67d2dc410abd9b65023eedd7bc12da1a795a15 [file] [log] [blame]
/**
* @license
* Copyright 2020 The Bazel Authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const path = require('path');
const rollup = require('rollup');
const loadConfigFile = require('rollup/dist/loadConfigFile');
const crypto = require('crypto')
const worker = require('@bazel/worker');
const MNEMONIC = 'Rollup';
const PID = process.pid;
// Store the cache forever to re-use on each build
let cacheMap = Object.create(null);
// Generate a unique cache ID based on the given json data
function computeCacheKey(cacheKeyData) {
const hash = crypto.createHash('sha256');
const hashContent = JSON.stringify(cacheKeyData);
return hash.update(hashContent).digest('hex');
}
async function runRollup(cacheKeyData, inputOptions, outputOptions) {
const cacheKey = computeCacheKey(cacheKeyData);
let cache = cacheMap[cacheKey];
const rollupStartTime = Date.now();
const bundle = await rollup.rollup({...inputOptions, cache});
const rollupEndTime = Date.now();
worker.debug(
`${MNEMONIC}[${PID}][${cacheKey}].rollup()`, (rollupEndTime - rollupStartTime) / 1000);
cacheMap[cacheKey] = bundle.cache;
try {
await bundle.write(outputOptions);
} catch (e) {
worker.log(e);
return false;
}
const bundleEndTime = Date.now();
worker.debug(`${MNEMONIC}[${PID}][${cacheKey}].write()`, (bundleEndTime - rollupEndTime) / 1000);
return true;
}
// Run rollup, will use + re-populate the cache
// Essentially the same as the rollup CLI but using bazel APIs
// for CLI arguments and FS watching
// See: https://github.com/rollup/rollup/blob/v2.23.1/cli/run/index.ts#L11
async function runRollupBundler(args /*, inputs */) {
const {inputOptions, outputOptions} = await parseCLIArgs(args);
const cacheKeyData = [
inputOptions.input,
// Include changes to externals in the cache key because rollup currently
// ignores such changes when using the caching API
// See https://github.com/rollup/rollup/issues/3874
inputOptions.external
];
return runRollup(cacheKeyData, inputOptions, outputOptions);
}
// Processing of --environment CLI options into environment vars
// https://github.com/rollup/rollup/blob/v1.31.0/cli/run/index.ts#L50-L57
function extractEnvironmentVariables(vars) {
vars.split(',').forEach(pair => {
const [key, ...value] = pair.split(':');
if (value.length) {
process.env[key] = value.join(':');
} else {
process.env[key] = String(true);
}
});
}
// Parse a subset of supported CLI arguments required for the rollup_bundle rule API.
// Returns input/outputOptions for the rollup.bundle/write() API
// input: https://rollupjs.org/guide/en/#inputoptions-object
// output: https://rollupjs.org/guide/en/#outputoptions-object
async function parseCLIArgs(args) {
// Options which the CLI args or config file can override
const defaultInputOptions = {
onwarn(...warnArgs) {
worker.log(...warnArgs);
},
};
// Options which can override the config file
let inputOptions = {};
let outputOptions = {};
let configFile = null;
// Input files to rollup
let inputs = [];
// Followed by suppported rollup CLI options
for (let i = 0; i < args.length; i++) {
const arg = args[i];
// Non-option is assumed to be an input file
if (!arg.startsWith('--')) {
inputs.push(arg);
continue;
}
const option = arg.slice(2);
switch (option) {
case 'config':
configFile = path.resolve(args[++i]);
break;
case 'silent':
inputOptions.onwarn = () => {};
break;
case 'format':
case 'output.dir':
case 'output.file':
case 'sourcemap':
outputOptions[option.replace('output.', '')] = args[++i];
break;
case 'preserveSymlinks':
inputOptions[option] = true;
break;
// Common rollup CLI args, but not required for use
case 'environment':
extractEnvironmentVariables(args[++i]);
break;
default:
throw new Error(`${MNEMONIC}: invalid or unsupported argument ${arg}`);
}
}
// If outputting a directory then rollup_bundle.bzl passed a series
// of name=path files as the input.
// TODO: do some not have the =?
if (outputOptions.dir) {
inputs = inputs.reduce((m, nameInput) => {
const [name, input] = nameInput.split('=', 2);
m[name] = input;
return m;
}, {});
}
// Additional options passed via config file
if (configFile) {
const {options, warnings} = await loadConfigFile(configFile);
// Flush any config file warnings to stderr
warnings.flush();
// Does NOT support (unlike rollup CLI):
// * multiple configs for multiple outputs
if (options.length !== 1) {
throw new Error('Array configs unsupported');
}
const config = options[0];
if (config.output) {
outputOptions = {...config.output[0], ...outputOptions};
}
inputOptions = {...config, ...inputOptions};
// Delete from our copied inputOptions, not the config which
// may be external and persisted across runs
delete inputOptions.output;
}
// Provide default inputOptions which can be overwritten
inputOptions = {...defaultInputOptions, ...inputOptions};
// The inputs are the rule entry_point[s]
inputOptions.input = inputs;
return {inputOptions, outputOptions};
}
async function main(args) {
// Bazel will pass a special argument to the program when it's running us as a worker
if (worker.runAsWorker(args)) {
worker.log(`Running ${MNEMONIC} as a Bazel worker`);
await worker.runWorkerLoop(runRollupBundler);
} else {
// Running standalone so stdout is available as usual
console.log(`Running ${MNEMONIC} as a standalone process`);
console.error(
`Started a new process to perform this action. Your build might be misconfigured, try
--strategy=${MNEMONIC}=worker`);
// Parse the options from the bazel-supplied options file.
// The first argument to the program is prefixed with '@'
// because Bazel does that for param files. Strip it first.
const paramFile = process.argv[2].replace(/^@/, '');
const args = require('fs').readFileSync(paramFile, 'utf-8').trim().split('\n');
return (await runRollupBundler(args)) ? 0 : 1;
}
}
if (require.main == module) {
(async () => process.exitCode = await main(process.argv.slice(2)))();
}