@babel/cli
Version:
Babel command line.
736 lines (729 loc) • 24.4 kB
JavaScript
import fs from 'node:fs';
import * as commander from 'commander';
import * as babel from '@babel/core';
import { DEFAULT_EXTENSIONS, version } from '@babel/core';
import * as glob from 'glob';
import path from 'node:path';
import { createRequire } from 'node:module';
import slash from 'slash';
import convertSourceMap from 'convert-source-map';
import { encodedMap, AnyMap } from '@jridgewell/trace-mapping';
const fileToDeps = new Map();
const depToFiles = new Map();
let isWatchMode = false;
let watcher;
const watchQueue = new Set();
let hasStarted = false;
function enable({
enableGlobbing
}) {
isWatchMode = true;
const {
FSWatcher
} = requireChokidar();
const options = {
disableGlobbing: !enableGlobbing,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 50,
pollInterval: 10
}
};
watcher = new FSWatcher(options);
watcher.on("unlink", unwatchFile);
}
function startWatcher() {
hasStarted = true;
for (const dep of watchQueue) {
watcher.add(dep);
}
watchQueue.clear();
watcher.on("ready", () => {
console.log("The watcher is ready.");
});
}
function watch(filename) {
if (!isWatchMode) {
throw new Error("Internal Babel error: .watch called when not in watch mode.");
}
if (!hasStarted) {
watchQueue.add(path.resolve(filename));
} else {
watcher.add(path.resolve(filename));
}
}
function onFilesChange(callback) {
if (!isWatchMode) {
throw new Error("Internal Babel error: .onFilesChange called when not in watch mode.");
}
watcher.on("all", (event, filename) => {
if (event !== "change" && event !== "add") return;
const absoluteFile = path.resolve(filename);
callback([absoluteFile, ...(depToFiles.get(absoluteFile) ?? [])], event, absoluteFile);
});
}
function updateExternalDependencies(filename, dependencies) {
if (!isWatchMode) return;
const absFilename = path.resolve(filename);
const absDependencies = new Set(Array.from(dependencies, dep => path.resolve(dep)));
const deps = fileToDeps.get(absFilename);
if (deps) {
for (const dep of deps) {
if (!absDependencies.has(dep)) {
removeFileDependency(absFilename, dep);
}
}
}
for (const dep of absDependencies) {
let deps = depToFiles.get(dep);
if (!deps) {
depToFiles.set(dep, deps = new Set());
if (!hasStarted) {
watchQueue.add(dep);
} else {
watcher.add(dep);
}
}
deps.add(absFilename);
}
fileToDeps.set(absFilename, absDependencies);
}
function removeFileDependency(filename, dep) {
const deps = depToFiles.get(dep);
deps.delete(filename);
if (deps.size === 0) {
depToFiles.delete(dep);
if (!hasStarted) {
watchQueue.delete(dep);
} else {
watcher.unwatch(dep);
}
}
}
function unwatchFile(filename) {
const deps = fileToDeps.get(filename);
if (!deps) return;
for (const dep of deps) {
removeFileDependency(filename, dep);
}
fileToDeps.delete(filename);
}
function requireChokidar() {
const require = createRequire(import.meta.url);
try {
return require("chokidar");
} catch (err) {
console.error("The optional dependency chokidar failed to install and is required for " + "--watch. Chokidar is likely not supported on your platform.");
throw err;
}
}
function chmod(src, dest) {
try {
fs.chmodSync(dest, fs.statSync(src).mode);
} catch (_) {
console.warn(`Cannot change permissions of ${dest}`);
}
}
function alphasort(a, b) {
return a.localeCompare(b, "en");
}
function readdir(dirname, includeDotfiles, filter) {
return fs.readdirSync(dirname, {
recursive: true,
withFileTypes: true
}).filter(dirent => {
if (dirent.isDirectory()) return false;
const filename = dirent.name;
return (includeDotfiles || !filename.startsWith(".")) && (!filter || filter(filename));
}).map(dirent => path.join(dirent.parentPath, dirent.name)).sort(alphasort);
}
function readdirForCompilable(dirname, includeDotfiles, altExts) {
return readdir(dirname, includeDotfiles, function (filename) {
return isCompilableExtension(filename, altExts);
});
}
function isCompilableExtension(filename, altExts) {
const exts = altExts || babel.DEFAULT_EXTENSIONS;
const ext = path.extname(filename);
return exts.includes(ext);
}
function addSourceMappingUrl(code, loc) {
return code + "\n//# sourceMappingURL=" + path.basename(loc);
}
function hasDataSourcemap(code) {
const pos = code.lastIndexOf("\n", code.length - 2);
return pos !== -1 && code.lastIndexOf("//# sourceMappingURL") < pos;
}
const REPL_CALLER = {
name: "@babel/cli",
supportsStaticESM: false,
supportsDynamicImport: false,
supportsExportNamespaceFrom: false
};
const COMPILE_CALLER = {
name: "@babel/cli"
};
function transformRepl(filename, code, opts) {
opts = {
...opts,
sourceMaps: opts.sourceMaps === "inline" ? true : opts.sourceMaps,
caller: REPL_CALLER,
filename
};
return new Promise((resolve, reject) => {
babel.transform(code, opts, (err, result) => {
if (err) reject(err);else resolve(result);
});
});
}
async function compile(filename, opts) {
opts = {
...opts,
caller: COMPILE_CALLER
};
const result = await babel.transformFileAsync(filename, opts);
if (result) {
updateExternalDependencies(filename, result.externalDependencies);
}
return result;
}
function deleteDir(path) {
fs.rmSync(path, {
force: true,
recursive: true
});
}
process.on("uncaughtException", function (err) {
console.error(err);
process.exitCode = 1;
});
function withExtension(filename, ext = ".js") {
const newBasename = path.basename(filename, path.extname(filename)) + ext;
return path.join(path.dirname(filename), newBasename);
}
function debounce(fn, time) {
let timer;
function debounced() {
clearTimeout(timer);
timer = setTimeout(fn, time);
}
debounced.flush = () => {
clearTimeout(timer);
fn();
};
return debounced;
}
const program = commander.program;
program.option("-f, --filename [filename]", "The filename to use when reading from stdin. This will be used in source-maps, errors etc.");
program.option("--presets [list]", "A comma-separated list of preset names.", collect);
program.option("--plugins [list]", "A comma-separated list of plugin names.", collect);
program.option("--config-file [path]", "Path to a .babelrc file to use.");
program.option("--env-name [name]", "The name of the 'env' to use when loading configs and plugins. " + "Defaults to the value of BABEL_ENV, or else NODE_ENV, or else 'development'.");
program.option("--root-mode [mode]", "The project-root resolution mode. " + "One of 'root' (the default), 'upward', or 'upward-optional'.");
program.option("--source-type [script|module]", "");
program.option("--no-babelrc", "Whether or not to look up .babelrc and .babelignore files.");
program.option("--ignore [list]", "List of glob paths to **not** compile.", collect);
program.option("--only [list]", "List of glob paths to **only** compile.", collect);
program.option("--no-highlight-code", "Enable or disable ANSI syntax highlighting of code frames. (on by default)");
program.option("--no-comments", "Write comments to generated output. (true by default)");
program.option("--retain-lines", "Retain line numbers. This will result in really ugly code.");
program.option("--compact [true|false|auto]", "Do not include superfluous whitespace characters and line terminators.", booleanify);
program.option("--minified", "Save as many bytes when printing. (false by default)");
program.option("--auxiliary-comment-before [string]", "Print a comment before any injected non-user code.");
program.option("--auxiliary-comment-after [string]", "Print a comment after any injected non-user code.");
program.option("-s, --source-maps [true|false|inline|both]", "", booleanify, undefined);
program.option("--source-map-target [string]", "Set `file` on returned source map.");
program.option("--source-file-name [string]", "Set `sources[0]` on returned source map.");
program.option("--source-root [filename]", "The root from which all sources are relative.");
program.option("-x, --extensions [extensions]", "List of extensions to compile when a directory has been the input. [" + DEFAULT_EXTENSIONS.join() + "]", collect);
program.option("--keep-file-extension", "Preserve the file extensions of the input files.");
program.option("-w, --watch", "Recompile files on changes.");
program.option("--skip-initial-build", "Do not compile files before watching.");
program.option("-o, --out-file [out]", "Compile all input files into a single file.");
program.option("-d, --out-dir [out]", "Compile an input directory of modules into an output directory.");
program.option("--relative", "Compile into an output directory relative to input directory or file. Requires --out-dir [out]");
program.option("-D, --copy-files", "When compiling a directory copy over non-compilable files.");
program.option("--include-dotfiles", "Include dotfiles when compiling and copying non-compilable files.");
program.option("--no-copy-ignored", "Exclude ignored files when copying non-compilable files.");
program.option("--verbose", "Log everything. This option conflicts with --quiet");
program.option("--quiet", "Don't log anything. This option conflicts with --verbose");
program.option("--delete-dir-on-start", "Delete the out directory before compilation.");
program.option("--out-file-extension [string]", "Use a specific extension for the output files");
program.argument("[files...]", "List of files to compile.");
program.version("8.0.1" + " (@babel/core " + version + ")");
program.usage("[options] [files...]");
function parseArgv(args) {
program.parse(args);
const opts = program.opts();
const errors = [];
let filenames = program.args.reduce(function (globbed, input) {
let files = glob.sync(input, {
dotRelative: true
}).sort(alphasort);
if (!files.length) files = [input];
globbed.push(...files);
return globbed;
}, []);
filenames = Array.from(new Set(filenames));
filenames.forEach(function (filename) {
if (!fs.existsSync(filename)) {
errors.push(filename + " does not exist");
}
});
if (opts.outDir && !filenames.length) {
errors.push("--out-dir requires filenames");
}
if (opts.outFile && opts.outDir) {
errors.push("--out-file and --out-dir cannot be used together");
}
if (opts.relative && !opts.outDir) {
errors.push("--relative requires --out-dir usage");
}
if (opts.watch) {
if (!opts.outFile && !opts.outDir) {
errors.push("--watch requires --out-file or --out-dir");
}
if (!filenames.length) {
errors.push("--watch requires filenames");
}
}
if (opts.skipInitialBuild && !opts.watch) {
errors.push("--skip-initial-build requires --watch");
}
if (opts.deleteDirOnStart && !opts.outDir) {
errors.push("--delete-dir-on-start requires --out-dir");
}
if (opts.verbose && opts.quiet) {
errors.push("--verbose and --quiet cannot be used together");
}
if (!opts.outDir && filenames.length === 0 && typeof opts.filename !== "string" && opts.babelrc !== false) {
errors.push("stdin compilation requires either -f/--filename [filename] or --no-babelrc");
}
if (opts.keepFileExtension && opts.outFileExtension) {
errors.push("--out-file-extension cannot be used with --keep-file-extension");
}
if (errors.length) {
console.error("babel:");
errors.forEach(function (e) {
console.error(" " + e);
});
return null;
}
const babelOptions = {
presets: opts.presets,
plugins: opts.plugins,
rootMode: opts.rootMode,
configFile: opts.configFile,
envName: opts.envName,
sourceType: opts.sourceType,
ignore: opts.ignore,
only: opts.only,
retainLines: opts.retainLines,
compact: opts.compact,
minified: opts.minified,
auxiliaryCommentBefore: opts.auxiliaryCommentBefore,
auxiliaryCommentAfter: opts.auxiliaryCommentAfter,
sourceMaps: opts.sourceMaps,
sourceFileName: opts.sourceFileName,
sourceRoot: opts.sourceRoot,
babelrc: opts.babelrc === true ? undefined : opts.babelrc,
highlightCode: opts.highlightCode === true ? undefined : opts.highlightCode,
comments: opts.comments === true ? undefined : opts.comments
};
for (const key of Object.keys(babelOptions)) {
if (babelOptions[key] === undefined) {
delete babelOptions[key];
}
}
return {
babelOptions,
cliOptions: {
filename: opts.filename,
filenames,
extensions: opts.extensions,
keepFileExtension: opts.keepFileExtension,
outFileExtension: opts.outFileExtension,
watch: opts.watch,
skipInitialBuild: opts.skipInitialBuild,
outFile: opts.outFile,
outDir: opts.outDir,
relative: opts.relative,
copyFiles: opts.copyFiles,
copyIgnored: opts.copyFiles && opts.copyIgnored,
includeDotfiles: opts.includeDotfiles,
verbose: opts.verbose,
quiet: opts.quiet,
deleteDirOnStart: opts.deleteDirOnStart,
sourceMapTarget: opts.sourceMapTarget
}
};
}
function booleanify(val) {
if (val === "true" || val === "1") {
return true;
}
if (val === "false" || val === "0" || val === "") {
return false;
}
return val;
}
function collect(value, previousValue) {
if (typeof value !== "string") return previousValue;
const values = value.split(",");
if (previousValue) {
previousValue.push(...values);
return previousValue;
}
return values;
}
const FILE_TYPE = Object.freeze({
NON_COMPILABLE: "NON_COMPILABLE",
COMPILED: "COMPILED",
IGNORED: "IGNORED",
ERR_COMPILATION: "ERR_COMPILATION"
});
function outputFileSync(filePath, data) {
fs.mkdirSync(path.dirname(filePath), {
recursive: true
});
fs.writeFileSync(filePath, data);
}
async function dirCommand ({
cliOptions,
babelOptions
}) {
async function write(src, base) {
let relative = path.relative(base, src);
if (!isCompilableExtension(relative, cliOptions.extensions)) {
return FILE_TYPE.NON_COMPILABLE;
}
relative = withExtension(relative, cliOptions.keepFileExtension ? path.extname(relative) : cliOptions.outFileExtension);
const dest = getDest(relative, base);
try {
const res = await compile(src, {
...babelOptions,
sourceFileName: slash(path.relative(dest + "/..", src))
});
if (!res) return FILE_TYPE.IGNORED;
if (res.map) {
let outputMap = false;
if (babelOptions.sourceMaps && babelOptions.sourceMaps !== "inline") {
outputMap = "external";
} else if (babelOptions.sourceMaps == null) {
outputMap = hasDataSourcemap(res.code) ? "external" : "both";
}
if (outputMap) {
const mapLoc = dest + ".map";
if (outputMap === "external") {
res.code = addSourceMappingUrl(res.code, mapLoc);
}
res.map.file = path.basename(relative);
outputFileSync(mapLoc, JSON.stringify(res.map));
}
}
outputFileSync(dest, res.code);
chmod(src, dest);
if (cliOptions.verbose) {
console.log(path.relative(process.cwd(), src) + " -> " + dest);
}
return FILE_TYPE.COMPILED;
} catch (err) {
if (cliOptions.watch) {
console.error(err);
return FILE_TYPE.ERR_COMPILATION;
}
throw err;
}
}
function getDest(filename, base) {
if (cliOptions.relative) {
return path.join(base, cliOptions.outDir, filename);
}
return path.join(cliOptions.outDir, filename);
}
async function handleFile(src, base) {
const written = await write(src, base);
if (cliOptions.copyFiles && written === FILE_TYPE.NON_COMPILABLE || cliOptions.copyIgnored && written === FILE_TYPE.IGNORED) {
const filename = path.relative(base, src);
const dest = getDest(filename, base);
outputFileSync(dest, fs.readFileSync(src));
chmod(src, dest);
}
return written === FILE_TYPE.COMPILED;
}
async function handle(filenameOrDir) {
if (!fs.existsSync(filenameOrDir)) return 0;
const stat = fs.statSync(filenameOrDir);
if (stat.isDirectory()) {
const dirname = filenameOrDir;
let count = 0;
const files = readdir(dirname, cliOptions.includeDotfiles);
for (const filename of files) {
const written = await handleFile(filename, dirname);
if (written) count += 1;
}
return count;
} else {
const filename = filenameOrDir;
const written = await handleFile(filename, path.dirname(filename));
return written ? 1 : 0;
}
}
let compiledFiles = 0;
let startTime = null;
const logSuccess = debounce(function () {
if (startTime === null) {
return;
}
const diff = process.hrtime(startTime);
console.log(`Successfully compiled ${compiledFiles} ${compiledFiles !== 1 ? "files" : "file"} with Babel (${diff[0] * 1e3 + Math.round(diff[1] / 1e6)}ms).`);
compiledFiles = 0;
startTime = null;
}, 100);
if (cliOptions.watch) enable({
enableGlobbing: true
});
if (!cliOptions.skipInitialBuild) {
if (cliOptions.deleteDirOnStart) {
deleteDir(cliOptions.outDir);
}
fs.mkdirSync(cliOptions.outDir, {
recursive: true
});
startTime = process.hrtime();
for (const filename of cliOptions.filenames) {
compiledFiles += await handle(filename);
}
if (!cliOptions.quiet) {
logSuccess();
logSuccess.flush();
}
}
if (cliOptions.watch) {
let processing = 0;
const {
filenames
} = cliOptions;
let getBase;
if (filenames.length === 1) {
const base = filenames[0];
const absoluteBase = path.resolve(base);
getBase = filename => {
return filename === absoluteBase ? path.dirname(base) : base;
};
} else {
const filenameToBaseMap = new Map(filenames.map(filename => {
const absoluteFilename = path.resolve(filename);
return [absoluteFilename, path.dirname(filename)];
}));
const absoluteFilenames = new Map(filenames.map(filename => {
const absoluteFilename = path.resolve(filename);
return [absoluteFilename, filename];
}));
const {
sep
} = path;
getBase = filename => {
const base = filenameToBaseMap.get(filename);
if (base !== undefined) {
return base;
}
for (const [absoluteFilenameOrDir, relative] of absoluteFilenames) {
if (filename.startsWith(absoluteFilenameOrDir + sep)) {
filenameToBaseMap.set(filename, relative);
return relative;
}
}
return "";
};
}
filenames.forEach(filenameOrDir => {
watch(filenameOrDir);
});
startWatcher();
onFilesChange(async filenames => {
processing++;
if (startTime === null) startTime = process.hrtime();
try {
const written = await Promise.all(filenames.map(filename => handleFile(filename, getBase(filename))));
compiledFiles += written.filter(Boolean).length;
} catch (err) {
console.error(err);
}
processing--;
if (processing === 0 && !cliOptions.quiet) logSuccess();
});
}
}
async function fileCommand ({
cliOptions,
babelOptions
}) {
function buildResult(fileResults) {
const mapSections = [];
let code = "";
let offset = 0;
let hasRawMap = false;
for (const result of fileResults) {
if (!result) continue;
hasRawMap = !!result.map;
mapSections.push({
offset: {
line: offset,
column: 0
},
map: result.map || {
version: 3,
names: [],
sources: [],
mappings: []
}
});
code += result.code + "\n";
offset += countNewlines(result.code) + 1;
}
const map = new AnyMap({
version: 3,
file: cliOptions.sourceMapTarget || path.basename(cliOptions.outFile || "") || "stdout",
sections: mapSections
});
map.sourceRoot = babelOptions.sourceRoot;
if (babelOptions.sourceMaps === "inline" || !cliOptions.outFile && babelOptions.sourceMaps) {
code += "\n" + convertSourceMap.fromObject(encodedMap(map)).toComment();
}
return {
map: map,
code: code,
hasRawMap: hasRawMap
};
}
function countNewlines(code) {
let count = 0;
let index = -1;
while ((index = code.indexOf("\n", index + 1)) !== -1) {
count++;
}
return count;
}
function output(fileResults) {
const result = buildResult(fileResults);
if (cliOptions.outFile) {
fs.mkdirSync(path.dirname(cliOptions.outFile), {
recursive: true
});
let outputMap = false;
if (babelOptions.sourceMaps && babelOptions.sourceMaps !== "inline") {
outputMap = "external";
} else if (babelOptions.sourceMaps == null && result.hasRawMap) {
outputMap = hasDataSourcemap(result.code) ? "external" : "both";
}
if (outputMap) {
const mapLoc = cliOptions.outFile + ".map";
if (outputMap === "external") {
result.code = addSourceMappingUrl(result.code, mapLoc);
}
fs.writeFileSync(mapLoc, JSON.stringify(encodedMap(result.map)));
}
fs.writeFileSync(cliOptions.outFile, result.code);
} else {
process.stdout.write(result.code + "\n");
}
}
function readStdin() {
return new Promise((resolve, reject) => {
let code = "";
process.stdin.setEncoding("utf8");
process.stdin.on("readable", function () {
const chunk = process.stdin.read();
if (chunk !== null) code += chunk;
});
process.stdin.on("end", function () {
resolve(code);
});
process.stdin.on("error", reject);
});
}
async function stdin() {
const code = await readStdin();
const res = await transformRepl(cliOptions.filename, code, {
...babelOptions,
sourceFileName: "stdin"
});
output([res]);
}
async function walk(filenames) {
const _filenames = [];
filenames.forEach(function (filename) {
if (!fs.existsSync(filename)) return;
const stat = fs.statSync(filename);
if (stat.isDirectory()) {
_filenames.push(...readdirForCompilable(filename, cliOptions.includeDotfiles, cliOptions.extensions));
} else {
_filenames.push(filename);
}
});
const results = await Promise.all(_filenames.map(async function (filename) {
let sourceFilename = filename;
if (cliOptions.outFile) {
sourceFilename = path.relative(path.dirname(cliOptions.outFile), sourceFilename);
}
sourceFilename = slash(sourceFilename);
try {
return await compile(filename, {
...babelOptions,
sourceFileName: sourceFilename,
sourceMaps: babelOptions.sourceMaps === "inline" ? true : babelOptions.sourceMaps
});
} catch (err) {
if (!cliOptions.watch) {
throw err;
}
console.error(err);
return null;
}
}));
output(results);
}
async function files(filenames) {
if (cliOptions.watch) {
enable({
enableGlobbing: false
});
}
if (!cliOptions.skipInitialBuild) {
await walk(filenames);
}
if (cliOptions.watch) {
filenames.forEach(watch);
startWatcher();
onFilesChange((changes, event, cause) => {
const actionableChange = changes.some(filename => isCompilableExtension(filename, cliOptions.extensions) || filenames.includes(filename));
if (!actionableChange) return;
if (cliOptions.verbose) {
console.log(`${event} ${cause}`);
}
walk(filenames).catch(err => {
console.error(err);
});
});
}
}
if (cliOptions.filenames.length) {
await files(cliOptions.filenames);
} else {
await stdin();
}
}
const opts = parseArgv(process.argv);
if (opts) {
const fn = opts.cliOptions.outDir ? dirCommand : fileCommand;
fn(opts).catch(err => {
console.error(err);
process.exitCode = 1;
});
} else {
process.exitCode = 2;
}
//# sourceMappingURL=index.js.map