json.macro
Version:
Directly load json files into your code via babel macros.
656 lines (562 loc) • 19.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var _classCallCheck = _interopDefault(require('@babel/runtime/helpers/classCallCheck'));
var _inherits = _interopDefault(require('@babel/runtime/helpers/inherits'));
var _possibleConstructorReturn = _interopDefault(require('@babel/runtime/helpers/possibleConstructorReturn'));
var _getPrototypeOf = _interopDefault(require('@babel/runtime/helpers/getPrototypeOf'));
var parser = require('@babel/parser');
var is = _interopDefault(require('@sindresorhus/is'));
var babelPluginMacros = require('babel-plugin-macros');
var fs = require('fs');
var glob = _interopDefault(require('globby'));
var JSON5 = _interopDefault(require('json5'));
var get = _interopDefault(require('lodash.get'));
var path = require('path');
var findPackageJson = _interopDefault(require('pkg-up'));
var semver = require('semver');
var tsconfigResolver = require('tsconfig-resolver');
function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
/**
* @typedef { import('babel-plugin-macros').MacroParams } MacroParams
* @typedef { import('@babel/core').Node } Node
* @typedef { import('@babel/core').NodePath<Node> } NodePath
* @typedef {{ reference: NodePath, babel: MacroParams['babel'], state:
* MacroParams['state'] }} MethodParams
* @typedef { (options: MethodParams) => void } MethodCall
* @typedef {Object} CheckReferenceExistsParameter
* @property {string} name - The reference name to check for
* @property {MethodCall} method - the method called for each instance of the
* reference.
* @property {MacroParams} macroParameter - the reference to check.
*/
/**
* Provides a custom error for this macro.
*/
var JsonMacroError = /*#__PURE__*/function (_MacroError) {
_inherits(JsonMacroError, _MacroError);
var _super = /*#__PURE__*/_createSuper(JsonMacroError);
/**
* @param {string} message
*/
function JsonMacroError(message) {
var _this;
_classCallCheck(this, JsonMacroError);
_this = _super.call(this, message);
_this.name = 'JsonMacroError';
_this.stack = '';
return _this;
}
return JsonMacroError;
}(babelPluginMacros.MacroError);
/**
* Checks if a value is a string or is undefined.
*
* @param {unknown} value
* @returns {value is string}
*/
function isStringOrUndefined(value) {
return is.string(value) || is.undefined(value);
}
/**
* Check whether the directory structure for a file path exists, and create it
* if it doesn't.
*
* @param {string} filePath
*
* @returns {void}
*/
function ensureDirectoryExists(filePath) {
var dir = path.dirname(filePath);
if (fs.existsSync(dir)) {
return;
}
ensureDirectoryExists(dir);
fs.mkdirSync(dir);
}
/**
* Prints readable error messages for when loading a json file fails.
* @param {NodePath} path
* @param {string} message
*
* @returns {never}
*/
function frameError(path, message) {
throw path.buildCodeFrameError("\n\n".concat(message, "\n\n"), JsonMacroError);
}
/**
* Evaluates the value matches the provided `predicate`.
* @template Type
* @param {Object} options
* @param {NodePath | undefined} options.node
* @param {NodePath} options.parentPath
* @param {(value: unknown) => value is Type } options.predicate
*
* @returns {Type}
*/
function evaluateNodeValue(_ref) {
var parentPath = _ref.parentPath,
node = _ref.node,
predicate = _ref.predicate;
var value;
try {
value = node === null || node === void 0 ? void 0 : node.evaluate().value;
} catch (_unused) {
/* istanbul ignore next */
frameError(parentPath, "There was a problem evaluating the value of the argument for the code: ".concat(parentPath.getSource(), ". If the value is dynamic, please make sure that its value is statically deterministic."));
}
if (!predicate(value)) {
frameError(parentPath, "Invalid argument passed to function call. Received unsupported type '".concat(is(value), "'."));
}
return value;
}
/**
* Get the node for the first argument of a function call. Will throw an error
* if more than one argument.
*
* @param {Object} options
* @param {NodePath} options.parentPath
* @param {boolean} [options.required=true] - whether the argument must be provided
* @param {number} [options.index=0] - the argument index to get
* @param {number} [options.maxArguments=1] - maximum number of arguments accepted
*
* @returns {NodePath | undefined}
*/
function getArgumentNode(_ref2) {
var parentPath = _ref2.parentPath,
_ref2$required = _ref2.required,
required = _ref2$required === void 0 ? true : _ref2$required,
_ref2$index = _ref2.index,
index = _ref2$index === void 0 ? 0 : _ref2$index,
_ref2$maxArguments = _ref2.maxArguments,
maxArguments = _ref2$maxArguments === void 0 ? 1 : _ref2$maxArguments;
var nodes = parentPath.get('arguments');
var nodeArray = Array.isArray(nodes) ? nodes : [nodes];
if (nodeArray.length > maxArguments) {
frameError(parentPath, "Too many arguments provided to the function call: ".concat(parentPath.getSource(), ". This method only supports one or less."));
}
var node = nodeArray === null || nodeArray === void 0 ? void 0 : nodeArray[index];
if (node === undefined && required) {
frameError(parentPath, "No arguments were provided when one is required: ".concat(parentPath.getSource(), "."));
}
return node;
}
/**
* Loads a file, parses it's as a json string and throws an error if there is
* problem doing do.
*
* @template Type
*
* @param {Object} options
* @param {string} options.filePath
* @param {NodePath} options.parentPath
*
* @returns {Type}
*/
function loadAndParseJsonFile(_ref3) {
var filePath = _ref3.filePath,
parentPath = _ref3.parentPath;
var jsonValue;
try {
var fileContent = fs.readFileSync(filePath, {
encoding: 'utf-8'
});
jsonValue = JSON5.parse(fileContent);
} catch (_unused2) {
frameError(parentPath, "There was a problem loading the provided JSON file: '".concat(filePath, "'. Please make sure the file exists and you have provided valid JSON content."));
}
return jsonValue;
}
/**
* @param {any} state
*
* @returns {string}
*/
function getFileName(state) {
var fileName = state.file.opts.filename;
if (!fileName) {
throw new JsonMacroError('json.macro methods can only be used on files and no filename was found');
}
return fileName;
}
/**
* Loads the nearest `package.json` file throws an error if there is
* problem doing do.
*
* @param {Object} options
* @param {string} options.cwd - the current working directory
* @param {NodePath} options.parentPath
*
* @returns {import('type-fest').PackageJson}
*/
function loadAndParsePackageJsonFile(options) {
var cwd = options.cwd,
parentPath = options.parentPath;
var filePath = findPackageJson.sync({
cwd: cwd
});
if (!filePath) {
frameError(parentPath, "No package.json file could be loaded from your current directory. '".concat(cwd, "'"));
}
return loadAndParseJsonFile({
filePath: filePath,
parentPath: parentPath
});
}
/**
* @param {Object} options
* @param {unknown} options.value
* @param {MacroParams['babel']} options.babel - the babel object
* @param {NodePath} options.parentPath
*/
function replaceParentExpression(options) {
var babel = options.babel,
parentPath = options.parentPath,
value = options.value;
var expression = babel.types.parenthesizedExpression(parser.parseExpression("[".concat(JSON.stringify(value), "][0]"), {}));
parentPath.replaceWith(expression);
}
/**
* Loads the version from the nearest package.json file.
*
* @param {MethodParams} options
*/
function getVersion(_ref4) {
var reference = _ref4.reference,
state = _ref4.state,
babel = _ref4.babel;
var filename = getFileName(state);
var parentPath = reference.parentPath;
var cwd = path.dirname(filename);
var node = getArgumentNode({
parentPath: parentPath,
required: false
});
var shouldLoadObject = node && (node === null || node === void 0 ? void 0 : node.evaluate().value) === true;
var jsonValue = loadAndParsePackageJsonFile({
cwd: cwd,
parentPath: parentPath
});
var stringVersion = jsonValue.version;
if (!stringVersion) {
frameError(parentPath, 'No version found for the resolved `package.json` file.');
}
/** @type {string | import('../types').SemanticVersion} */
var value = stringVersion;
/** @type {import('../types').SemanticVersion | null} */
var semver$1 = semver.parse(stringVersion);
if (!semver$1) {
frameError(parentPath, "A semantic versioning object could not be parsed from the invalid string: '".concat(stringVersion, "'"));
}
if (shouldLoadObject) {
value = {
build: semver$1.build,
loose: semver$1.loose,
major: semver$1.major,
minor: semver$1.minor,
patch: semver$1.patch,
prerelease: semver$1.prerelease,
raw: semver$1.raw,
version: semver$1.version
};
}
replaceParentExpression({
babel: babel,
parentPath: parentPath,
value: value
});
}
/**
* Loads the nearest package.json file.
*
* @param {MethodParams} options
*/
function loadPackageJson(_ref5) {
var _jsonValue$key;
var reference = _ref5.reference,
state = _ref5.state,
babel = _ref5.babel;
var filename = getFileName(state);
var parentPath = reference.parentPath;
var cwd = path.dirname(filename);
var node = getArgumentNode({
parentPath: parentPath,
required: false
});
var key = node ? evaluateNodeValue({
node: node,
parentPath: parentPath,
predicate: isStringOrUndefined
}) : undefined;
var jsonValue = loadAndParsePackageJsonFile({
cwd: cwd,
parentPath: parentPath
});
var value = key ? (_jsonValue$key = jsonValue[key]) !== null && _jsonValue$key !== void 0 ? _jsonValue$key : null : jsonValue;
replaceParentExpression({
babel: babel,
parentPath: parentPath,
value: value
});
}
/**
* Loads the nearest package.json file.
*
* @param {MethodParams} options
*/
function loadTsConfigJson(_ref6) {
var reference = _ref6.reference,
state = _ref6.state,
babel = _ref6.babel;
var filename = getFileName(state);
var parentPath = reference.parentPath;
var cwd = path.dirname(filename);
var node = getArgumentNode({
parentPath: parentPath,
required: false
});
var searchName = node ? evaluateNodeValue({
node: node,
parentPath: parentPath,
predicate: isStringOrUndefined
}) : tsconfigResolver.DEFAULT_SEARCH_NAME;
var result = tsconfigResolver.tsconfigResolverSync({
cwd: cwd,
cache: tsconfigResolver.CacheStrategy.Directory,
searchName: searchName
});
if (!result.exists) {
frameError(parentPath, "No '".concat(searchName, "' file could be loaded from your current file. ").concat(filename));
}
replaceParentExpression({
babel: babel,
parentPath: parentPath,
value: result.config
});
}
/**
* Handles writing a single json file with an optional object path parameter.
*
* @param {MethodParams} options
*/
function writeJson(_ref7) {
var reference = _ref7.reference,
state = _ref7.state,
babel = _ref7.babel;
var filename = getFileName(state);
var parentPath = reference.parentPath;
var dir = path.dirname(filename);
var json = evaluateNodeValue({
node: getArgumentNode({
parentPath: parentPath,
required: true,
maxArguments: 2,
index: 0
}),
parentPath: parentPath,
predicate: is.plainObject
});
var relativeFilePath = evaluateNodeValue({
node: getArgumentNode({
parentPath: parentPath,
required: true,
maxArguments: 2,
index: 1
}),
parentPath: parentPath,
predicate: is.string
});
var filePath = path.resolve(dir, relativeFilePath); // Make sure the file path exists.
ensureDirectoryExists(filePath); // Write to the provided filePath.
fs.writeFileSync(filePath, JSON.stringify(json, null, 2), {
encoding: 'utf8'
});
replaceParentExpression({
babel: babel,
parentPath: parentPath,
value: json
});
}
/**
* Handles loading a single json file with an optional object path parameter.
*
* @param {MethodParams} options
*/
function loadJson(_ref8) {
var reference = _ref8.reference,
state = _ref8.state,
babel = _ref8.babel;
var filename = getFileName(state);
var parentPath = reference.parentPath;
var dir = path.dirname(filename);
var rawFilePath = evaluateNodeValue({
node: getArgumentNode({
parentPath: parentPath,
required: true,
maxArguments: 2,
index: 0
}),
parentPath: parentPath,
predicate: is.string
});
var path$1 = evaluateNodeValue({
node: getArgumentNode({
parentPath: parentPath,
required: false,
maxArguments: 2,
index: 1
}),
parentPath: parentPath,
predicate: isStringOrUndefined
});
/** @type {string} */
var filePath;
try {
filePath = require.resolve(rawFilePath, {
paths: [dir]
});
} catch (_unused3) {
frameError(parentPath, "The provided path: '".concat(rawFilePath, "' does not exist"));
}
var jsonValue = loadAndParseJsonFile({
filePath: filePath,
parentPath: parentPath
});
var value = path$1 ? get(jsonValue, path$1) : jsonValue;
replaceParentExpression({
babel: babel,
parentPath: parentPath,
value: value
});
}
/**
* Handles loading multiple json files by their glob pattern.
*
* @param {MethodParams} options
*/
function loadJsonFiles(_ref9) {
var reference = _ref9.reference,
state = _ref9.state,
babel = _ref9.babel;
var filename = getFileName(state);
var parentPath = reference.parentPath;
var callExpressionPath = reference.parentPath;
var dir = path.dirname(filename);
var args = callExpressionPath.get('arguments');
var argsArray = Array.isArray(args) ? args : [args];
if (argsArray.length === 0) {
frameError(parentPath, "You must provide at least one file pattern string to the function call: '".concat(parentPath.getSource(), "'. If the value is dynamic, please make sure that its value is statically deterministic."));
}
var globs = argsArray.map(function (node) {
return evaluateNodeValue({
node: node,
parentPath: parentPath,
predicate: is.string
});
});
var files = glob.sync(globs, {
cwd: dir
});
if (files.length === 0) {
frameError(parentPath, "The file patterns provided didn't match any files: '".concat(parentPath.getSource(), "'. If the value is dynamic, please make sure that its value is statically deterministic."));
}
var value = files.map(function (relativePath) {
return loadAndParseJsonFile({
filePath: path.resolve(dir, relativePath),
parentPath: parentPath
});
});
replaceParentExpression({
babel: babel,
parentPath: parentPath,
value: value
});
}
/**
* Check to see if the provided reference name is used in this file. When it's
* available call the function for every occurrence.
*
* @param {CheckReferenceExistsParameter} options
*
* @returns {void}
*/
function checkReferenceExists(options) {
var method = options.method,
name = options.name,
macroParameter = options.macroParameter;
var babel = macroParameter.babel,
references = macroParameter.references,
state = macroParameter.state;
var namedReferences = references[name];
if (!namedReferences) {
return;
}
var _iterator = _createForOfIteratorHelper(namedReferences),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var reference = _step.value;
var parentPath = reference.parentPath;
if (!parentPath.isCallExpression()) {
throw frameError(parentPath, "'".concat(name, "' called from 'json.macro' must be used as a function call."));
}
method({
babel: babel,
reference: reference,
state: state
});
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
/** The supported methods for this macro */
var supportedMethods = [{
name: 'writeJson',
method: writeJson
}, {
name: 'loadJson',
method: loadJson
}, {
name: 'loadJsonFiles',
method: loadJsonFiles
}, {
name: 'getVersion',
method: getVersion
}, {
name: 'loadPackageJson',
method: loadPackageJson
}, {
name: 'loadTsConfigJson',
method: loadTsConfigJson
}];
/**
* The macro which is created and exported for usage in your project.
*/
var macro = /*#__PURE__*/babelPluginMacros.createMacro(function (macroParameter) {
var _iterator2 = _createForOfIteratorHelper(supportedMethods),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var supportedMethod = _step2.value;
var name = supportedMethod.name,
method = supportedMethod.method;
checkReferenceExists({
name: name,
method: method,
macroParameter: macroParameter
});
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
});
exports.default = macro;