UNPKG

@babel/cli

Version:
736 lines (729 loc) 24.4 kB
#!/usr/bin/env node 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