fable-compiler-netcore
Version:
F# to JavaScript compiler
257 lines (256 loc) • 12.2 kB
JavaScript
;
var fs = require("fs");
var path = require("path");
var commandLineArgs = require("command-line-args");
var commandLineUsage = require("command-line-usage/lib/command-line-usage");
var semver = require("semver");
var json5 = require("json5");
var fableLib = require("./lib");
var constants = require("./constants");
var customPlugins = require("./babelPlugins");
var bundle_1 = require("./bundle");
// Don't use default values as they would block options from fableconfig.json
var optionDefinitions = [
{ name: 'projFile', defaultOption: true, multiple: true, description: "The F# project (.fsproj) or script (.fsx) to compile." },
{ name: 'outDir', alias: 'o', description: "Where to put compiled JS files. Defaults to project directory." },
{ name: 'module', alias: 'm', description: "Specify module code generation: `commonjs`, `umd`, `amd` or `es2015` (default)." },
{ name: 'sourceMaps', alias: 's', description: "Generate source maps: `false` (default), `true` or `inline`." },
{ name: 'watch', alias: 'w', multiple: 'true', description: "Recompile project much faster on file modifications." },
{ name: 'ecma', description: "Specify ECMAScript target version: `es5` (default) or `es2015`." },
{ name: 'verbose', type: Boolean, description: "Print more information about the compilation process." },
{ name: 'symbols', multiple: true, description: "F# symbols for conditional compilation, like `DEBUG`." },
{ name: 'dll', type: Boolean, description: "Generate a `.dll` assembly when creating libraries." },
{ name: 'rollup', description: "Bundle files and dependencies with Rollup." },
{ name: 'includeJs', type: Boolean, description: "Compile with Babel and copy to `outDir` relative imports (starting with '.')." },
{ name: 'plugins', multiple: true, description: "Paths to Fable plugins." },
{ name: 'babelPlugins', multiple: true, description: "Additional Babel plugins (without `babel-plugin-` prefix). Must be installed in the project directory." },
{ name: 'refs', multiple: true, description: "Alternative location for compiled JS files of referenced libraries." },
{ name: 'coreLib', description: "Shortcut for `--refs Fable.Core={VALUE}`." },
{ name: 'loose', type: Boolean, description: "Enable loose transformations for babel-preset-es2015 plugins." },
{ name: 'babelrc', type: Boolean, description: "Use a `.babelrc` file for Babel configuration (invalidates other Babel related options)." },
{ name: 'clamp', type: Boolean, description: "Compile unsigned byte arrays as Uint8ClampedArray." },
{ name: 'noTypedArrays', type: Boolean, description: "Don't compile numeric arrays as JS typed arrays." },
{ name: 'target', alias: 't', description: "Use options from a specific target in `fableconfig.json`." },
{ name: 'debug', alias: 'd', description: "Shortcut for `--target debug`." },
{ name: 'production', alias: 'p', description: "Shortcut for `--target production`." },
{ name: 'declaration', type: Boolean, description: "[EXPERIMENTAL] Generates corresponding ‘.d.ts’ file." },
{ name: 'extra', multiple: true, description: "Custom options for plugins in `{KEY}={VALUE}` format." },
{ name: 'help', alias: 'h', description: "Display usage guide." }
];
function getAppDescription() {
return [{ header: 'Fable ' + constants.PKG_VERSION, content: 'F# to JavaScript compiler' },
{ header: 'Options', optionList: optionDefinitions },
{ content: 'All arguments can be defined in a fableconfig.json file' }];
}
function resolvePath(optName, value, workingDir) {
function resolve(x) {
return fableLib.pathJoin(workingDir, x);
}
// Discard null values or empty strings
if (value) {
switch (optName) {
case "outDir":
return resolve(value);
// Multiple values
case "projFile":
case "plugins":
case "babelPlugins":
return value.map(resolve);
// Only resolve refs if they starts with '.'
case "refs":
var o = {};
for (var k in value) {
o[k] = value[k].startsWith('.') ? resolve(value[k]) : value[k];
}
return o;
}
}
return value;
}
/** Reads options from command line, requires command-line-args */
function readCommandLineOptions() {
function resolveKeyValuePairs(kvs) {
var o = {};
for (var i = 0; i < kvs.length; i++) {
var kv = kvs[i].split("=");
o[kv[0]] = kv[1] || true;
}
return o;
}
var opts = commandLineArgs(optionDefinitions);
if (opts.help) {
fableLib.stdoutLog(commandLineUsage(getAppDescription()));
fableLib.finish(0);
}
if (opts.refs) {
opts.refs = resolveKeyValuePairs(opts.refs);
}
if (opts.coreLib) {
opts.refs = Object.assign(opts.refs || {}, { "Fable.Core": opts.coreLib });
delete opts.coreLib;
}
if (opts.extra) {
opts.extra = resolveKeyValuePairs(opts.extra);
}
return opts;
}
/** Reads options from fableconfig.json, requires json5 */
function readFableConfigOptions(opts) {
opts.workingDir = path.resolve(opts.workingDir || process.cwd());
if (typeof opts.projFile === "string") {
opts.projFile = [opts.projFile];
}
var cfgFile = fableLib.pathJoin(opts.workingDir, constants.FABLE_CONFIG_FILE);
if (Array.isArray(opts.projFile) && opts.projFile.length === 1) {
var fullProjFile = fableLib.pathJoin(opts.workingDir, opts.projFile[0]);
var projDir = fs.statSync(fullProjFile).isDirectory()
? fullProjFile
: path.dirname(fullProjFile);
cfgFile = fableLib.pathJoin(projDir, constants.FABLE_CONFIG_FILE);
// Delete projFile from opts if it isn't a true F# project
if (!fableLib.isFSharpProject(fullProjFile)) {
delete opts.projFile;
}
}
if (fs.existsSync(cfgFile)) {
// Change workingDir to where fableconfig.json is if necessary
if (opts.workingDir !== path.dirname(cfgFile)) {
for (var key in opts) {
opts[key] = resolvePath(key, opts[key], opts.workingDir);
}
opts.workingDir = path.dirname(cfgFile);
}
var cfg = json5.parse(fs.readFileSync(cfgFile).toString());
for (var key in cfg) {
if (key in opts === false)
opts[key] = cfg[key];
}
// Check if a target is requested
if (opts.debug) {
opts.target = "debug";
}
if (opts.production) {
opts.target = "production";
}
if (opts.target) {
if (!opts.targets || !opts.targets[opts.target]) {
throw "Target " + opts.target + " is missing";
}
cfg = opts.targets[opts.target];
for (key in cfg) {
if ((typeof cfg[key] === "object") && !Array.isArray(cfg[key]) &&
(typeof opts[key] === "object") && !Array.isArray(opts[key])) {
for (var key2 in cfg[key])
opts[key][key2] = cfg[key][key2];
}
else {
opts[key] = cfg[key];
}
}
}
}
return opts;
}
/** Reads Babel options: plugins and presets */
function readBabelOptions(opts) {
var babelPresets = [],
// Add plugins to emit .d.ts files if necessary
babelPlugins = opts.declaration
? [[require("babel-dts-generator"),
{
"packageName": "",
"typings": fableLib.pathJoin(opts.workingDir, opts.outDir),
"suppressAmbientDeclaration": true,
"ignoreEmptyInterfaces": false
}],
require("babel-plugin-transform-flow-strip-types"),
require("babel-plugin-transform-class-properties")]
: [];
// Add custom plugins
babelPlugins = babelPlugins.concat(customPlugins.transformMacroExpressions,
// removeUnneededNulls must come after transformMacroExpressions (see #377)
customPlugins.removeUnneededNulls, customPlugins.removeFunctionExpressionNames);
// If opts.babelrc is true, let Babel read plugins and presets from .babelrc
// Babel will automatically read configuration from .babelrc if we don't pass `babelrc: false`
if (opts.babelrc) {
opts.babel = { presets: babelPresets, plugins: babelPlugins };
return opts;
}
// ECMAScript target
if (opts.ecma != "es2015" && opts.ecma != "es6") {
var module_1 = opts.module;
if (opts.module === "es2015" || opts.module === "es6") {
module_1 = false;
}
else if (opts.module in constants.JS_MODULES === false) {
throw "Unknown module target: " + opts.module;
}
babelPresets.push([require.resolve("babel-preset-es2015"), {
"loose": opts.loose,
"modules": opts.rollup ? false : module_1
}]);
}
else if (!opts.rollup && opts.module in constants.JS_MODULES) {
babelPlugins.push(require("babel-plugin-transform-es2015-modules-" + opts.module));
}
// Extra Babel plugins
if (opts.babelPlugins) {
babelPlugins = babelPlugins.concat(fableLib.resolvePlugins(opts.babelPlugins, opts.workingDir, "babel-plugin-"));
}
opts.babel = { presets: babelPresets, plugins: babelPlugins };
return opts;
}
/** Prepares options: read from command line, fableconfig.json, etc */
function readOptions(opts) {
opts = opts || readCommandLineOptions();
opts = readFableConfigOptions(opts);
opts.projFile = Array.isArray(opts.projFile) ? opts.projFile : [opts.projFile];
if (!opts.projFile[0]) {
throw "--projFile is empty";
}
for (var i = 0; i < opts.projFile.length; i++) {
var fullProjFile = fableLib.pathJoin(opts.workingDir, opts.projFile[i] || '');
if (!fableLib.isFSharpProject(fullProjFile)) {
throw "Not an F# project (.fsproj) or script (.fsx): " + fullProjFile;
}
if (!fs.existsSync(fullProjFile)) {
throw "Cannot find file: " + fullProjFile;
}
}
// Default values & option processing
opts.ecma = opts.ecma || "es5";
opts.outDir = opts.outDir ? opts.outDir : (opts.projFile.length === 1 ? path.dirname(opts.projFile[0]) : ".");
if (opts.module == null) {
opts.module = opts.rollup ? "iife" : "es2015";
}
if (opts.coreLib) {
opts.refs = Object.assign(opts.refs || {}, { "Fable.Core": opts.coreLib });
delete opts.coreLib;
}
if (opts.refs) {
for (var k in opts.refs) {
var k2 = k.replace(/\.dll$/, "");
if (k !== k2) {
opts.refs[k2] = opts.refs[k];
delete opts.refs[k];
}
}
}
// Check version
var curNpmCfgPath = fableLib.pathJoin(opts.workingDir, "package.json");
if (!(opts.extra && opts.extra.noVersionCheck) && fs.existsSync(curNpmCfgPath)) {
var curNpmCfg = JSON.parse(fs.readFileSync(curNpmCfgPath).toString());
if (curNpmCfg.engines && (curNpmCfg.engines.fable || curNpmCfg.engines["fable-compiler"])) {
var fableRequiredVersion = curNpmCfg.engines.fable || curNpmCfg.engines["fable-compiler"];
if (!semver.satisfies(constants.PKG_VERSION, fableRequiredVersion)) {
throw "Fable version: " + constants.PKG_VERSION + "\n" +
"Required: " + fableRequiredVersion + "\n" +
"Please upgrade fable-compiler package";
}
}
}
opts = readBabelOptions(opts);
opts = bundle_1.readRollupOptions(opts);
return opts;
}
exports.readOptions = readOptions;