UNPKG

json.macro

Version:

Directly load json files into your code via babel macros.

656 lines (562 loc) 19.4 kB
'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;